From 28e3c68ce167e72d13c5e5ea53eb94cebaf0e18a Mon Sep 17 00:00:00 2001 From: Javier Cicchelli Date: Mon, 13 Oct 2025 00:41:29 +0200 Subject: [PATCH] Implemented the SemanticVersionValidationRule type in the library target. --- .../Errors/InputValidationError.swift | 2 + .../CamelCaseValidationRule.swift | 1 - .../SemanticVersionValidationRule.swift | 81 +++++++++++++++++++ .../Validation Rules/URLValidationRule.swift | 8 ++ .../Use Cases/ValidateInputUseCaseTests.swift | 32 ++++++++ 5 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 Sources/DiscogsService/Internal/Validation Rules/SemanticVersionValidationRule.swift create mode 100644 Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift diff --git a/Sources/DiscogsService/Internal/Errors/InputValidationError.swift b/Sources/DiscogsService/Internal/Errors/InputValidationError.swift index 8b1dca424..0254295e2 100644 --- a/Sources/DiscogsService/Internal/Errors/InputValidationError.swift +++ b/Sources/DiscogsService/Internal/Errors/InputValidationError.swift @@ -24,6 +24,8 @@ 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. + case inputNotSemanticVersion /// An input does not comply with the user token requirements. case inputNotUserToken } diff --git a/Sources/DiscogsService/Internal/Validation Rules/CamelCaseValidationRule.swift b/Sources/DiscogsService/Internal/Validation Rules/CamelCaseValidationRule.swift index 630df3057..e6e1326f8 100644 --- a/Sources/DiscogsService/Internal/Validation Rules/CamelCaseValidationRule.swift +++ b/Sources/DiscogsService/Internal/Validation Rules/CamelCaseValidationRule.swift @@ -74,5 +74,4 @@ private extension CamelCaseValidationRule { private extension String.Pattern { /// A regular expression pattern that represents camel-cased inputs. static let camelCase = "([A-Z]([a-z]|[0-9])+)+" - } diff --git a/Sources/DiscogsService/Internal/Validation Rules/SemanticVersionValidationRule.swift b/Sources/DiscogsService/Internal/Validation Rules/SemanticVersionValidationRule.swift new file mode 100644 index 000000000..37dfde7fb --- /dev/null +++ b/Sources/DiscogsService/Internal/Validation Rules/SemanticVersionValidationRule.swift @@ -0,0 +1,81 @@ +// ===----------------------------------------------------------------------=== +// +// This source file is part of the DiscogsService open source project +// +// 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 +// +// ===----------------------------------------------------------------------=== + +/// A validation rule type that checks whether an input is a semantic version or not. +/// +/// This validation rules follows the principles defined in the [Semantic Versioning 2.0.0 documentation](https://semver.org/spec/v2.0.0.html) +struct SemanticVersionValidationRule: 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 == SemanticVersionValidationRule { + + // MARK: Constants + + /// A validation rule that checks whether an input is semantic version or not. + static var semanticVersion: Self { .init() } + +} + +// MARK: - Helpers + +private extension SemanticVersionValidationRule { + + // 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.semanticVersioning) + ) else { + throw InputValidationError.inputNotSemanticVersion + } + + return true + } + +} + +// MARK: - Constants + +private extension String.Pattern { + /// A regular expression pattern that represents semantic version inputs. + /// + /// This regular expression is based on the [suggested regular expression](https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string) of the *Semantic Versioning 2.0.0* documentation. + static let semanticVersioning = "^(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)(?:-((?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9][0-9]*|[0-9]*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?$" +} diff --git a/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift b/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift new file mode 100644 index 000000000..5db2d474e --- /dev/null +++ b/Sources/DiscogsService/Internal/Validation Rules/URLValidationRule.swift @@ -0,0 +1,8 @@ +// +// File.swift +// discogs-service +// +// Created by Javier Cicchelli on 13/10/2025. +// + +import Foundation diff --git a/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift b/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift index 19fc17fa6..9e5b93ffb 100644 --- a/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift +++ b/Tests/DiscogsService/Cases/Internal/Use Cases/ValidateInputUseCaseTests.swift @@ -105,6 +105,20 @@ struct ValidateInputUseCaseTests { expects: error ) } + + @Test(arguments: zip( + Input.inputsSemanticVersion, + Output.inputsSemanticVersion + )) func `validate semantic version`( + input: String?, + expects error: InputValidationError? + ) async throws { + try assertValidate( + rule: .semanticVersion, + input: input, + expects: error + ) + } #else @Test("validate camel case", arguments: zip( Input.inputsCamelCase, @@ -189,6 +203,20 @@ struct ValidateInputUseCaseTests { expects: error ) } + + @Test("validate semantic version", arguments: zip( + Input.inputsSemanticVersion, + Output.inputsSemanticVersion + )) func validateSemanticVersion( + input: String?, + expects error: InputValidationError? + ) async throws { + try assertValidate( + rule: .semanticVersion, + input: input, + expects: error + ) + } #endif } @@ -243,6 +271,8 @@ private extension Input { static let inputsSecureConsumerSecret: [String] = ["aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpP", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoO", "aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPqQ", "a4bBcCdDe3fFg6hH1IjJkK1LmMnNo0p9"] /// A list of inputs to validate against the secure (user token) validation rule. 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"] } private extension Output { @@ -258,4 +288,6 @@ private extension Output { static let inputsSecureConsumerSecret: [InputValidationError?] = [nil, .inputNotConsumerSecret, .inputNotConsumerSecret, .inputNotConsumerSecret] /// A list of expected input validation errors to be thrown after validating inputs against the secure (user token) validation rule. 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] }