diff --git a/Sources/DiscogsService/Internal/Errors/InputValidationError.swift b/Sources/DiscogsService/Internal/Errors/InputValidationError.swift index 0254295e2..e7b63fce4 100644 --- a/Sources/DiscogsService/Internal/Errors/InputValidationError.swift +++ b/Sources/DiscogsService/Internal/Errors/InputValidationError.swift @@ -24,8 +24,10 @@ enum InputValidationError: Error { case inputNotConsumerKey /// An input does not comply with the consumer secret requirements. case inputNotConsumerSecret - /// An input does not comply with the semantic versioning requirements. + /// An input is not a semantic version. case inputNotSemanticVersion + /// An input is not a URL. + case inputNotURL /// An input does not comply with the user token requirements. case inputNotUserToken } diff --git a/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift b/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift index 5db2d474e..6726fecd5 100644 --- a/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift +++ b/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift @@ -1,8 +1,83 @@ +// ===----------------------------------------------------------------------=== // -// File.swift -// discogs-service +// This source file is part of the DiscogsService open source project // -// Created by Javier Cicchelli on 13/10/2025. +// Copyright (c) 2025 Röck+Cöde VoF. and the DiscogsService project authors +// Licensed under Apache license v2.0 // +// See LICENSE for license information +// See CONTRIBUTORS for the list of DiscogsService project authors +// +// SPDX-License-Identifier: Apache-2.0 +// +// ===----------------------------------------------------------------------=== -import Foundation +/// A validation rule type that checks whether an input is a URL or not. +/// +/// This validation rule doesn't necessarily follow the [RFC 3986](https://www.rfc-editor.org/rfc/rfc3986) standard. +/// Thus it doesn't implement a complex regular expression pattern such as [this one](https://rgxdb.com/r/5JXUI5A2). +/// Instead this validation implements a regular expression sufficient enough to satisfy the requirements for a [user agent definition](https://www.discogs.com/developers/#page:home,header:home-general-information). +struct URLValidationRule: InputValidationRule { + + // MARK: Functions + +#if swift(>=6.0) + func validate(_ input: String?) throws(InputValidationError) -> Bool { + try validate(input: input) + } +#else + func validate(_ input: String?) throws -> Bool { + try validate(input: input) + } +#endif + +} + +// MARK: - Definitions + +extension InputValidationRule where Self == URLValidationRule { + + // MARK: Constants + + /// A validation rule that checks whether an input is a URL or not. + static var url: Self { .init() } + +} + +// MARK: - Helpers + +private extension URLValidationRule { + + // MARK: Functions + + /// Validates a given input. + /// + /// > note: This helper function would not be necessary when support for *Swift 5.10* is discontinued. + /// + /// - Parameter input: An input to be validated. + /// - Returns: A flag that indicates whether a given input has been validated or not. + /// - Throws: An error of type ``InputValidatorError`` in case the validation failed. + func validate(input: String?) throws -> Bool { + guard let input else { + return false + } + + guard input.fullyMatch( + pattern: .init(format: .Pattern.url) + ) else { + throw InputValidationError.inputNotURL + } + + return true + } + +} + +// MARK: - Constants + +private extension String.Pattern { + /// A regular expression pattern that represents URL inputs. + /// + /// This regular expression is based on [this regular expression](https://regex101.com/r/3fYy3x/1) found while researching the topic. + static let url = "https?:\\/\\/(www\\.)?[-a-zA-Z0-9@:%._\\+~#=]{2,256}\\.[a-z]{2,4}\\b([-a-zA-Z0-9@:%_\\+.~#?&//=]*)" +} diff --git a/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift b/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift index 9e5b93ffb..1f85f856b 100644 --- a/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift +++ b/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift @@ -119,6 +119,20 @@ struct ValidateInputUseCaseTests { expects: error ) } + + @Test(arguments: zip( + Input.inputsURL, + Output.inputsURL + )) func `validate url`( + input: String?, + expects error: InputValidationError? + ) async throws { + try assertValidate( + rule: .url, + input: input, + expects: error + ) + } #else @Test("validate camel case", arguments: zip( Input.inputsCamelCase, @@ -217,6 +231,20 @@ struct ValidateInputUseCaseTests { expects: error ) } + + @Test("validate url", arguments: zip( + Input.inputsURL, + Output.inputsURL + )) func validateURL( + input: String?, + expects error: InputValidationError? + ) async throws { + try assertValidate( + rule: .url, + input: input, + expects: error + ) + } #endif } @@ -273,6 +301,8 @@ private extension Input { static let inputsSecureUserToken: [String] = ["aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStT", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsS", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQrRsStTuU", "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9qQrRs5t7"] /// A list of inputs to validate against the semantic version validation rule. static let inputsSemanticVersion: [String] = ["0.0.4","1.2.3","10.20.30","1.1.2-prerelease+meta","1.1.2+meta","1.1.2+meta-valid","1.0.0-alpha","1.0.0-beta","1.0.0-alpha.beta","1.0.0-alpha.beta.1","1.0.0-alpha.1","1.0.0-alpha0.valid","1.0.0-alpha.0valid","1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay","1.0.0-rc.1+build.1","2.0.0-rc.1+build.123","1.2.3-beta","10.2.3-DEV-SNAPSHOT","1.2.3-SNAPSHOT-123","1.0.0","2.0.0","1.1.7","2.0.0+build.1848","2.0.1-alpha.1227","1.0.0-alpha+beta","1.2.3----RC-SNAPSHOT.12.9.1--.12+788","1.2.3----R-S.12.9.1--.12+meta","1.2.3----RC-SNAPSHOT.12.9.1--.12","1.0.0+0.build.1-rc.10000aaa-kk-0.1","99999999999999999999999.999999999999999999.99999999999999999","1.0.0-0A.is.legal","1","1.2","1.2.3-0123","1.2.3-0123.0123","1.1.2+.123","+invalid","-invalid","-invalid+invalid","-invalid.01","alpha","alpha.beta","alpha.beta.1","alpha.1","alpha+beta","alpha_beta","alpha.","alpha..","beta","1.0.0-alpha_beta","-alpha.","1.0.0-alpha..","1.0.0-alpha..1","1.0.0-alpha...1","1.0.0-alpha....1","1.0.0-alpha.....1","1.0.0-alpha......1","1.0.0-alpha.......1","01.1.1","1.01.1","1.1.01","1.2","1.2.3.DEV","1.2-SNAPSHOT","1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788","1.2-RC-SNAPSHOT","-1.0.3-gamma+b7718","+justmeta","9.8.7+meta+meta","9.8.7-whatever+meta+meta","99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"] + /// A list of inputs to validate against the URL validation rule. + static let inputsURL: [String] = ["https://www.google.com", "http://www.google.com", "https://google.com/q=search", "http://google.com/q=search", "3333-768-0948", "1133.168.0248", "7678*999-8978", "httpq://google.com/q=search", "www.google.com", "www.google.com/?search=qppoao", "www . google.com/?search=qppoao", "https : //google.com/q=search", "htt://www.google.com", "://www.google.com", .empty] } private extension Output { @@ -290,4 +320,6 @@ private extension Output { static let inputsSecureUserToken: [InputValidationError?] = [nil, .inputNotUserToken, .inputNotUserToken, .inputNotUserToken] /// A list of expected input validation errors to be thrown after validating inputs against the semantic version validation rule. static let inputsSemanticVersion: [InputValidationError?] = [nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion, .inputNotSemanticVersion] + /// A list of expected input validation errors to be thrown after validating inputs against the URL validation rule. + static let inputsURL: [InputValidationError?] = [nil, nil, nil, nil, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL, .inputNotURL] }