| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126 |
- import Foundation
- enum PostDraftValidationError: LocalizedError {
- case emptyTopic
- case emptySubreddit
- case invalidSubreddit
- case emptyTitle
- case titleTooLong
- case bodyTooLong
- case missingImage
- case invalidLinkURL
- case emptyLinkURL
- case invalidVideoURL
- case emptyVideoURL
- case insufficientPollOptions
- case emptyPollOption
- var errorDescription: String? {
- switch self {
- case .emptyTopic:
- "Enter a topic or keywords for AI generation."
- case .emptySubreddit:
- "Enter a subreddit to tailor the post."
- case .invalidSubreddit:
- "Subreddit names must be 3–21 characters and use only letters, numbers, or underscores."
- case .emptyTitle:
- "Add a post title before exporting or submitting."
- case .titleTooLong:
- "Title must be 300 characters or fewer."
- case .bodyTooLong:
- "Post body must be 40,000 characters or fewer."
- case .missingImage:
- "Upload an image for image posts."
- case .invalidLinkURL:
- "Enter a valid http or https URL for the link."
- case .emptyLinkURL:
- "Enter a URL for link posts."
- case .invalidVideoURL:
- "Enter a valid http or https URL for the video."
- case .emptyVideoURL:
- "Enter a video URL for video posts."
- case .insufficientPollOptions:
- "Polls need at least two non-empty options."
- case .emptyPollOption:
- "Fill in all poll options or remove empty ones."
- }
- }
- }
- enum PostDraftValidator {
- static let maxTitleLength = 300
- static let maxBodyLength = 40_000
- static func normalizedSubreddit(_ raw: String) -> String {
- raw.trimmingCharacters(in: .whitespaces)
- .replacingOccurrences(of: "^r/", with: "", options: .regularExpression)
- .lowercased()
- }
- static func isValidSubreddit(_ raw: String) -> Bool {
- let name = normalizedSubreddit(raw)
- guard name.count >= 3, name.count <= 21 else { return false }
- let pattern = "^[a-z0-9_]+$"
- return name.range(of: pattern, options: .regularExpression) != nil
- }
- static func isValidHTTPURL(_ string: String) -> Bool {
- let trimmed = string.trimmingCharacters(in: .whitespaces)
- guard let url = URL(string: trimmed),
- let scheme = url.scheme?.lowercased(),
- scheme == "http" || scheme == "https",
- url.host != nil
- else { return false }
- return true
- }
- static func validateForGeneration(topic: String, subreddit: String) throws {
- guard !topic.trimmingCharacters(in: .whitespaces).isEmpty else {
- throw PostDraftValidationError.emptyTopic
- }
- guard !subreddit.trimmingCharacters(in: .whitespaces).isEmpty else {
- throw PostDraftValidationError.emptySubreddit
- }
- guard isValidSubreddit(subreddit) else {
- throw PostDraftValidationError.invalidSubreddit
- }
- }
- static func validateForExport(_ draft: PostDraft) throws {
- try validateForGeneration(topic: draft.topic, subreddit: draft.subreddit)
- let title = draft.title.trimmingCharacters(in: .whitespaces)
- guard !title.isEmpty else { throw PostDraftValidationError.emptyTitle }
- guard title.count <= maxTitleLength else { throw PostDraftValidationError.titleTooLong }
- guard draft.body.count <= maxBodyLength else { throw PostDraftValidationError.bodyTooLong }
- switch draft.postType {
- case .text:
- break
- case .image:
- guard draft.imageFileURL != nil else { throw PostDraftValidationError.missingImage }
- case .link:
- let url = draft.linkURL.trimmingCharacters(in: .whitespaces)
- guard !url.isEmpty else { throw PostDraftValidationError.emptyLinkURL }
- guard isValidHTTPURL(url) else { throw PostDraftValidationError.invalidLinkURL }
- case .video:
- let url = draft.videoURL.trimmingCharacters(in: .whitespaces)
- guard !url.isEmpty else { throw PostDraftValidationError.emptyVideoURL }
- guard isValidHTTPURL(url) else { throw PostDraftValidationError.invalidVideoURL }
- case .poll:
- let filled = draft.pollOptions.map(\.text).map {
- $0.trimmingCharacters(in: .whitespaces)
- }.filter { !$0.isEmpty }
- guard filled.count >= 2 else { throw PostDraftValidationError.insufficientPollOptions }
- }
- }
- static func canGenerate(from draft: PostDraft, isGenerating: Bool) -> Bool {
- guard !isGenerating else { return false }
- return (try? validateForGeneration(topic: draft.topic, subreddit: draft.subreddit)) != nil
- }
- static func canExport(_ draft: PostDraft) -> Bool {
- (try? validateForExport(draft)) != nil
- }
- }
|