diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml new file mode 100644 index 0000000..c8c964b --- /dev/null +++ b/.github/workflows/tests.yml @@ -0,0 +1,20 @@ +name: Tests + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: macos-15 + + steps: + - uses: actions/checkout@v3 + + - name: Build + run: swift build -v + + - name: Run tests + run: swift test -v \ No newline at end of file diff --git a/Package.swift b/Package.swift index 11faa59..cfa8913 100644 --- a/Package.swift +++ b/Package.swift @@ -16,7 +16,12 @@ let package = Package( // Targets are the basic building blocks of a package, defining a module or a test suite. // Targets can depend on other targets in this package and products from dependencies. .target( - name: "ValidationKit" + name: "ValidationKit", + resources: [.process("PrivacyInfo.xcprivacy")] ), + .testTarget( + name: "ValidationKitTests", + dependencies: ["ValidationKit"] + ) ] ) diff --git a/README.md b/README.md index 2dae2d6..7baa52c 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,8 @@ **ValidationKit** is a Swift framework designed for robust and flexible data validation. It provides type-safe validators, logical operators (`&&`, `||`, `!`) for composing complex validation rules, and type-erased mechanisms to seamlessly handle both optional and non-optional properties. ValidationKit simplifies ensuring data integrity across your Swift applications, making it easier to implement and maintain complex validation logic. +[![Tests](https://github.com/kubens/ValidationKit/actions/workflows/tests.yml/badge.svg)](https://github.com/kubens/ValidationKit/actions/workflows/tests.yml) + ## Features - **Type-Safe Validators:** Create and compose validators tailored to your data models. diff --git a/Sources/ValidationKit/PrivacyInfo.xcprivacy b/Sources/ValidationKit/PrivacyInfo.xcprivacy new file mode 100644 index 0000000..cf8d560 --- /dev/null +++ b/Sources/ValidationKit/PrivacyInfo.xcprivacy @@ -0,0 +1,14 @@ + + + + + NSPrivacyTracking + + NSPrivacyCollectedDataTypes + + NSPrivacyTrackingDomains + + NSPrivacyAccessedAPITypes + + + diff --git a/Sources/ValidationKit/Protocols/Validatable.swift b/Sources/ValidationKit/Protocols/Validatable.swift new file mode 100644 index 0000000..371462a --- /dev/null +++ b/Sources/ValidationKit/Protocols/Validatable.swift @@ -0,0 +1,47 @@ +// +// Validatable.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// A protocol that defines an object as capable of validation. +public protocol Validatable { + + /// Defines the validation rules for the object. + /// + /// Conforming types implement this method to specify validation rules. + /// The provided `Validations` collection is where rules should be added. + /// + /// - Parameter validations: A mutable reference to a `Validations` collection + /// where validation rules are defined. + func validations(_ validations: inout Validations) + + /// Executes all validation rules for the object. + /// + /// This method validates the object by applying all rules defined in + /// `validations(_:)`. If any rule fails, it throws an error, ensuring + /// that the object meets its constraints. + /// + /// - Throws: A validation error if any rule fails. + func validate() throws +} + +// MARK: - Default implementation +extension Validatable where Self: Sendable { + + /// Default implementation of the `validate()` method for `Validatable`. + /// + /// This method creates a new `Validations` collection, populates it by calling + /// `validations(_:)`, and then evaluates all defined validation rules for the object. + /// + /// - Throws: A validation error if any rule fails. + public func validate() throws { + var validations = Validations(of: Self.self) + self.validations(&validations) + + try validations.validate(self) + } +} diff --git a/Sources/ValidationKit/Protocols/ValidatorResult.swift b/Sources/ValidationKit/Protocols/ValidatorResult.swift new file mode 100644 index 0000000..33060a7 --- /dev/null +++ b/Sources/ValidationKit/Protocols/ValidatorResult.swift @@ -0,0 +1,41 @@ +// +// ValidatorResult.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +/// A protocol representing the result of a validation operation. +/// +/// Conforming types provide information about whether the validation succeeded or failed, +/// along with optional descriptive messages for both outcomes. +public protocol ValidatorResult: Sendable, CustomStringConvertible { + + /// Indicates whether the validation operation failed. + /// + /// - Returns: `true` if the validation failed; otherwise, `false`. + var isFailure: Bool { get } + + /// Provides a description of the validation success. + /// + /// - Returns: A string describing the success of the validation. + var successDescription: String? { get } + + /// Provides a description of the validation failure. + /// + /// - Returns: A string describing why the validation failed. + var failureDescription: String? { get } +} + +// MARK: - Default implementation + +extension ValidatorResult { + + /// Provides a textual representation of the validation result. + public var description: String { + switch isFailure { + case true: failureDescription ?? "validation failed" + case false: successDescription ?? "validation succeeded" + } + } +} diff --git a/Sources/ValidationKit/Support/AnyOptional.swift b/Sources/ValidationKit/Support/AnyOptional.swift new file mode 100644 index 0000000..8d86589 --- /dev/null +++ b/Sources/ValidationKit/Support/AnyOptional.swift @@ -0,0 +1,31 @@ +// +// Optional+AnyOptional.swift +// ValidationKit +// +// Created by kubens.com on 06/12/2024. +// + +/// A protocol that provides a standardized way to check if an Optional value is `nil`. +/// +/// `AnyOptional` allows for type-erased interactions with Optional types by exposing a common property +/// to determine the presence or absence of a value. This can be particularly useful in generic programming +/// scenarios where the specific type of the Optional is not known at compile time. +public protocol AnyOptional { + + /// Indicates whether the optional is `nil`. + /// + /// - Returns: `true` if the optional contains `nil`, otherwise `false`. + var isNil: Bool { get } +} + +/// Extends Swift's built-in `Optional` type to conform to the `AnyOptional` protocol. +/// +/// This extension allows all `Optional` types in Swift to utilize the `isNil` property, +/// providing a unified interface for checking the presence of a value. +extension Optional: AnyOptional { + + /// Returns `true` if the optional is `nil`, otherwise `false`. + public var isNil: Bool { + return self == nil + } +} diff --git a/Sources/ValidationKit/Support/AnyValidationRule.swift b/Sources/ValidationKit/Support/AnyValidationRule.swift new file mode 100644 index 0000000..8e2ee90 --- /dev/null +++ b/Sources/ValidationKit/Support/AnyValidationRule.swift @@ -0,0 +1,42 @@ +// +// AnyValidationRule.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// A generic validation rule that enables dynamic validation of objects. +/// +/// `AnyValidationRule` serves as a type-erased wrapper for specific ``ValidationRule`` instances, +/// allowing for flexible and dynamic validation logic. This is particularly useful when dealing +/// with collections of validation rules or when the specific types of properties to validate are not known at compile time. +public struct AnyValidationRule: Sendable where Object: Sendable { + + /// The key path identifying the property of the object to validate. + /// + /// This `AnyKeyPath` allows the validation rule to reference any property of the object, + /// regardless of its type. The `Sendable` conformance ensures that the key path can be safely + /// used in concurrent contexts. + public let keyPath: AnyKeyPath & Sendable + + /// A closure that encapsulates the validation logic for an object. + private let _validate: @Sendable (Object) -> ValidationResult + + /// Creates an `AnyValidationRule` by wrapping a specific `ValidationRule`. + /// + /// - Parameter rule: A `ValidationRule` for a specific property of the object. + public init(_ rule: ValidationRule) { + self.keyPath = rule.keyPath + self._validate = { rule.validate($0) } + } + + /// Validates the given object using the encapsulated validation logic. + /// + /// - Parameter object: The object to validate. + /// - Returns: A `ValidationResult` indicating whether the validation succeeded or failed. + public func validate(_ object: Object) -> ValidationResult { + return _validate(object) + } +} diff --git a/Sources/ValidationKit/Support/Operators+Validator.swift b/Sources/ValidationKit/Support/Operators+Validator.swift new file mode 100644 index 0000000..9453a64 --- /dev/null +++ b/Sources/ValidationKit/Support/Operators+Validator.swift @@ -0,0 +1,103 @@ +// +// Operators+Validator.swift +// ValidationKit +// +// Created by kubens.com on 06/12/2024. +// + +/// Applies a logical NOT operation to the given validator. +/// +/// This prefix operator inverts the result of the provided validator. If the original validator +/// succeeds, the resulting validator will fail, and vice versa. +/// +/// - Parameter validator: The validator to invert. +/// - Returns: A new `Validator` instance representing the logical NOT of the original validator. +public prefix func ! (_ validator: Validator) -> Validator { + Validator.not(validator) +} + +// MARK: - OR + +/// Combines two validators using a logical OR operation. +/// +/// The resulting validator succeeds if **either** the left-hand side (lhs) or the right-hand side (rhs) +/// validator succeeds. It fails only if **both** validators fail. +/// +/// - Parameters: +/// - lhs: The first validator to combine. +/// - rhs: The second validator to combine. +/// - Returns: A new `Validator` representing the logical OR of `lhs` and `rhs`. +public func || (lhs: Validator, rhs: Validator) -> Validator { + Validator.or(lhs, rhs) +} + +/// Combines an optional validator and a non-optional validator using a logical OR operation. +/// +/// The resulting validator succeeds if **either** the left-hand side (lhs) optional validator succeeds +/// or the right-hand side (rhs) non-optional validator succeeds when applied to the unwrapped value. +/// It fails only if **both** validators fail. +/// +/// - Parameters: +/// - lhs: The first optional validator to combine. +/// - rhs: The second non-optional validator to combine. +/// - Returns: A new `Validator` representing the logical OR of `lhs` and `rhs`. +public func || (lhs: Validator, rhs: Validator) -> Validator { + Validator.or(lhs, rhs) +} + +/// Combines a non-optional validator and an optional validator using a logical OR operation. +/// +/// The resulting validator succeeds if **either** the left-hand side (lhs) non-optional validator succeeds +/// or the right-hand side (rhs) optional validator succeeds when applied to the unwrapped value. +/// It fails only if **both** validators fail. +/// +/// - Parameters: +/// - lhs: The first non-optional validator to combine. +/// - rhs: The second optional validator to combine. +/// - Returns: A new `Validator` representing the logical OR of `lhs` and `rhs`. +public func || (lhs: Validator, rhs: Validator) -> Validator { + Validator.or(lhs, rhs) +} + +// MARK: - AND + +/// Combines two validators using a logical AND operation. +/// +/// The resulting validator succeeds only if **both** the left-hand side (lhs) and the right-hand side (rhs) +/// validators succeed. It fails if **either** validator fails. +/// +/// - Parameters: +/// - lhs: The first validator to combine. +/// - rhs: The second validator to combine. +/// - Returns: A new `Validator` representing the logical AND of `lhs` and `rhs`. +public func && (lhs: Validator, rhs: Validator) -> Validator { + Validator.and(lhs, rhs) +} + +/// Combines an optional validator and a non-optional validator using a logical AND operation. +/// +/// The resulting validator succeeds only if **both** the left-hand side (lhs) optional validator succeeds +/// and the right-hand side (rhs) non-optional validator succeeds when applied to the unwrapped value. +/// It fails if **either** validator fails. +/// +/// - Parameters: +/// - lhs: The first optional validator to combine. +/// - rhs: The second non-optional validator to combine. +/// - Returns: A new `Validator` representing the logical AND of `lhs` and `rhs`. +public func && (lhs: Validator, rhs: Validator) -> Validator { + Validator.and(lhs, rhs) +} + +/// Combines a non-optional validator and an optional validator using a logical AND operation. +/// +/// The resulting validator succeeds only if **both** the left-hand side (lhs) non-optional validator succeeds +/// and the right-hand side (rhs) optional validator succeeds when applied to the unwrapped value. +/// It fails if **either** validator fails. +/// +/// - Parameters: +/// - lhs: The first non-optional validator to combine. +/// - rhs: The second optional validator to combine. +/// - Returns: A new `Validator` representing the logical AND of `lhs` and `rhs`. +public func && (lhs: Validator, rhs: Validator) -> Validator { + Validator.and(lhs, rhs) +} diff --git a/Sources/ValidationKit/ValidationError.swift b/Sources/ValidationKit/ValidationError.swift new file mode 100644 index 0000000..aea18a0 --- /dev/null +++ b/Sources/ValidationKit/ValidationError.swift @@ -0,0 +1,30 @@ +// +// ValidationError.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// Represents an error that occurs during validation, containing a collection of validation failures. +public struct ValidationError: Error, CustomStringConvertible { + + /// An array of `ValidationResult` instances representing individual validation failures. + public let failures: [ValidationResult] + + /// A human-readable description of the validation errors. + public var description: String { + failures.map(\.description).joined(separator: ", ") + } + + /// Initializes a new instance of `ValidationError` with the provided validation failures. + /// + /// This initializer is marked as `internal`, restricting its usage to within the same module. + /// It assigns the provided array of `ValidationResult` to the `failures` property. + /// + /// - Parameter failures: An array of `ValidationResult` representing the validation failures. + internal init(failures: [ValidationResult]) { + self.failures = failures + } +} diff --git a/Sources/ValidationKit/ValidationResult.swift b/Sources/ValidationKit/ValidationResult.swift new file mode 100644 index 0000000..b199ee2 --- /dev/null +++ b/Sources/ValidationKit/ValidationResult.swift @@ -0,0 +1,42 @@ +// +// ValidationResult.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +/// Represents the result of validating a specific property of an object. +/// +/// This structure holds the `KeyPath` to the validated property and the result +/// of applying a validation rule to that property. It provides a way to track +/// which property was validated and the outcome of the validation. +/// +/// - Parameters: +/// - Object: The type of the object containing the property. +/// - Value: The type of the property's value being validated. +public struct ValidationResult: Sendable, CustomStringConvertible { + + /// The key path pointing to the validated property. + public let keyPath: AnyKeyPath & Sendable + + /// The result of the validation operation. + public let result: any ValidatorResult + + /// Provides a textual representation of the validation result. + public var description: String { + switch result.isFailure { + case true: "Property \(keyPath) is invalid, \(result.description)" + case false: "Property \(keyPath) is valid, \(result.description)" + } + } + + /// Initializes a new `ValidationResult` for a specific property. + /// + /// - Parameters: + /// - keyPath: A `KeyPath` pointing to the property that was validated. + /// - result: The result of the validation operation for the property. + internal init(_ keyPath: AnyKeyPath & Sendable, result: any ValidatorResult) { + self.keyPath = keyPath + self.result = result + } +} diff --git a/Sources/ValidationKit/ValidationRule.swift b/Sources/ValidationKit/ValidationRule.swift new file mode 100644 index 0000000..85e1db7 --- /dev/null +++ b/Sources/ValidationKit/ValidationRule.swift @@ -0,0 +1,46 @@ +// +// ValidationRule.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// A validation rule that associates a specific property with a validation logic. +/// +/// - Parameters: +/// - Object: The type of the object containing the property. +/// - Value: The type of the property's value. +public struct ValidationRule: Sendable where Object: Sendable, Value: Sendable { + + /// The key path pointing to the property to be validated. + public let keyPath: KeyPath & Sendable + + /// The validator that defines the validation logic for the property's value. + public let validator: Validator + + /// Initializes a new validation rule for a property. + /// + /// - Parameters: + /// - keyPath: A `KeyPath` pointing to the property to validate. + /// - validator: A `Validator` that contains the validation logic. + public init(_ keyPath: KeyPath & Sendable, validator: Validator) { + self.keyPath = keyPath + self.validator = validator + } + + /// Validates the value of the property specified by the key path. + /// + /// This method retrieves the value of the property from the given object using the `KeyPath`, + /// and applies the validation logic defined by the `Validator` to that value. + /// + /// - Parameter object: The object containing the property to validate. + /// - Returns: A `ValidationResult` indicating whether the validation succeeded or failed. + public func validate(_ object: Object) -> ValidationResult { + let value = object[keyPath: keyPath] + let result = validator.validate(value) + + return ValidationResult(keyPath, result: result) + } +} diff --git a/Sources/ValidationKit/Validations.swift b/Sources/ValidationKit/Validations.swift new file mode 100644 index 0000000..cdf49fa --- /dev/null +++ b/Sources/ValidationKit/Validations.swift @@ -0,0 +1,58 @@ +// +// Validations.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// A collection of validation rules for a specific object type. +/// +/// `Validations` allows you to define and execute validation rules for various +/// properties of an object. It aggregates multiple rules and applies them to +/// an object to ensure it satisfies the required constraints. +/// +/// - Parameter Object: The type of the object being validated. +public struct Validations where Object: Sendable { + + /// The storage of validation rules. + public var rules: [AnyValidationRule] + + /// Adds a validation rule for a specific property of the object. + /// + /// - Parameters: + /// - keyPath: A `KeyPath` pointing to the property to validate. + /// - validator: A `Validator` defining the validation logic for the property. + mutating public func add(_ keyPath: KeyPath & Sendable, is validator: Validator) { + let rule = ValidationRule(keyPath, validator: validator) + rules.append(AnyValidationRule(rule)) + } + + /// Validates the given object against all defined validation rules. + /// + /// This method iterates through all added validation rules, applies each + /// validator to the corresponding property of the object, and collects + /// any validation failures. If one or more validations fail, a `ValidationError` + /// containing all failure details is thrown. + /// + /// - Parameter object: The instance of `Object` to validate. + /// - Throws: A `ValidationError` if any of the validation rules fail. + public func validate(_ object: Object) throws { + let results = rules.map { $0.validate(object) } + let failures = results.filter(\.result.isFailure) + + if failures.count > 0 { + throw ValidationError(failures: failures) + } + } + + /// Initializes a new `Validations` instance for a specific object type. + /// + /// This initializer sets up an empty collection of validation rules for the given object type. + /// + /// - Parameter type: The type of the object being validated. + public init(of type: Object.Type) { + self.rules = [] + } +} diff --git a/Sources/ValidationKit/Validator.swift b/Sources/ValidationKit/Validator.swift new file mode 100644 index 0000000..6ec6707 --- /dev/null +++ b/Sources/ValidationKit/Validator.swift @@ -0,0 +1,26 @@ +// +// Validator.swift +// KBValidation +// +// Created by kubens.com on 01/12/2024. +// + +import Foundation + +/// A type that encapsulates validation logic for a specific type of value. +/// Allows the validation of any value and returns a `ValidatorResult` indicating success or failure. +/// +/// - Parameters: +/// - T: The type of value to be validated. +public struct Validator: Sendable where Value: Sendable { + + /// The closure that performs the validation. + public var validate: @Sendable (_ value: Value) -> any ValidatorResult + + /// Initializes a new `Validator`. + /// + /// - Parameter validate: A closure that defines the validation logic. + public init(validate: @Sendable @escaping (_: Value) -> any ValidatorResult) { + self.validate = validate + } +} diff --git a/Sources/ValidationKit/Validators/Validator+AllOf.swift b/Sources/ValidationKit/Validators/Validator+AllOf.swift new file mode 100644 index 0000000..10e695b --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+AllOf.swift @@ -0,0 +1,56 @@ +// +// Validator+AllOf.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of applying multiple validators to a single value. + /// + /// `AllValidatorResult` aggregates the outcomes of several `ValidatorResult` instances. + /// It determines the overall validation status based on whether any of the individual validations fail. + public struct AllOfValidatorResult: ValidatorResult { + + /// An array of individual validation results. + public let results: [any ValidatorResult] + + /// Indicates whether **any** of the validations have failed. + public var isFailure: Bool { + results.contains { $0.isFailure } + } + + /// A combined description of all validation successes. + public var successDescription: String? { + results + .filter { $0.isFailure == false } + .compactMap { $0.successDescription } + .joined(separator: " and ") + } + + /// A combined description of all validation failures. + public var failureDescription: String? { + results + .filter { $0.isFailure == true } + .compactMap { $0.failureDescription } + .joined(separator: " and ") + } + } + + /// Creates a validator that combines multiple validators using a logical AND operation. + /// + /// This method allows you to chain multiple validators so that a value must pass **all** validations to be considered valid. + /// If **any** of the validators fail, the combined validation fails, and all corresponding failure descriptions are aggregated. + /// + /// - Parameters: + /// - validators: A variadic list of `Validator` instances to combine. + /// - Returns: A new `Validator` that applies all provided validators to a value. + static public func allOf(_ validators: Validator...) -> Validator { + Validator { value in + AllOfValidatorResult(results: validators.map({ $0.validate(value) })) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+And.swift b/Sources/ValidationKit/Validators/Validator+And.swift new file mode 100644 index 0000000..2ce8e9b --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+And.swift @@ -0,0 +1,73 @@ +// +// Validator+And.swift +// ValidationKit +// +// Created by kubens.com on 07/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of combining two validators using logical AND. + /// + /// `AndValidatorResult` holds the results of two individual validators and determines + /// the overall validation outcome based on their combination. The validation fails + /// if either of the validators fails. + public struct AndValidatorResult: ValidatorResult { + + /// The result from the first validator. + public let left: ValidatorResult + + /// The result from the second validator. + public let right: ValidatorResult + + /// Indicates whether the combined validation has failed. + public var isFailure: Bool { + left.isFailure || right.isFailure + } + + /// Provides a description when the combined validation succeeds. + public var successDescription: String? { + switch (left.isFailure, right.isFailure) { + case (false, false): + left.successDescription.flatMap { leftDescription in + right.successDescription.map { rightDescription in + "\(leftDescription) and \(rightDescription)" + } + } + default: nil + } + } + + /// Provides a description when the combined validation fails. + public var failureDescription: String? { + switch (left.isFailure, right.isFailure) { + case (true, true): + left.failureDescription.flatMap { leftDescription in + right.failureDescription.map { right in + "\(leftDescription) and \(right)" + } + } + case (true, false): left.failureDescription + case (false, true): right.failureDescription + default: nil + } + } + } + + /// Combines two validators using logical AND. + /// + /// The combined validator succeeds only if both the `lhs` and `rhs` validators succeed. + /// If either validator fails, the combined validator fails. + /// + /// - Parameters: + /// - lhs: The first `Validator` to combine. + /// - rhs: The second `Validator` to combine. + /// - Returns: A new `Validator` that represents the logical AND of `lhs` and `rhs`. + public static func `and`(_ lhs: Validator, _ rhs: Validator) -> Validator { + Validator { value in + AndValidatorResult(left: lhs.validate(value), right: rhs.validate(value)) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Empty.swift b/Sources/ValidationKit/Validators/Validator+Empty.swift new file mode 100644 index 0000000..3ce21b2 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Empty.swift @@ -0,0 +1,42 @@ +// +// Validator+Empty.swift +// KBValidation +// +// Created by kubens.com on 03/12/2024. +// + +import Foundation + +extension Validator where Value: Collection { + + /// Represents the result of validating whether a collection is empty. + public struct EmptyValidatorResult: ValidatorResult { + + /// Indicates whether the collection is empty. + public let isEmpty: Bool + + /// Provides a description of the validation success. + public let successDescription: String? = "is empty" + + /// Provides a description of the validation failure. + public let failureDescription: String? = "is not empty" + + /// Indicates whether the validation failed. + public var isFailure: Bool { + !isEmpty + } + } + + /// Creates a validator that checks if a collection is empty. + /// + /// This static property provides a `Validator` instance that validates whether + /// a collection contains no elements. If the collection is not empty, + /// the validation fails with an appropriate failure description. + /// + /// - Returns: A `Validator` that ensures the collection is empty. + public static var empty: Validator { + Validator { value in + EmptyValidatorResult(isEmpty: value.isEmpty) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+In.swift b/Sources/ValidationKit/Validators/Validator+In.swift new file mode 100644 index 0000000..df9c5ab --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+In.swift @@ -0,0 +1,77 @@ +// +// Validator+In.swift +// ValidationKit +// +// Created by Jakub Łaptaś on 09/12/2024. +// + +import Foundation + +extension Validator where Value: Equatable, Value: CustomStringConvertible { + + /// Represents the result of validating whether a value is contained within a specified collection of values. + /// + /// The `InValidatorResult` struct checks if a given `item` exists within the provided `items` array. + /// It provides descriptive feedback based on the validation outcome, indicating success or failure along with appropriate messages. + /// + /// - Type Parameters: + /// - Value: The type of the value being validated. Must conform to `Equatable` for comparison and `CustomStringConvertible` for descriptive output. + public struct InValidatorResult: ValidatorResult { + + /// The value being validated. + public let item: Value + + /// The collection of allowed values against which the `item` is validated. + public let items: [Value] + + /// Indicates whether the validation has failed. + public var isFailure: Bool { + !items.contains(item) + } + + /// A descriptive message detailing the success of the validation. + public var successDescription: String? { + "is \(makeDescription(for: self.items))" + } + + /// A descriptive message detailing the failure of the validation. + public var failureDescription: String? { + "is not \(makeDescription(for: self.items))" + } + + /// Generates a descriptive string based on the number of valid values. + private func makeDescription(for items: [Value]) -> String { + switch items.count { + case 1: return items[0].description + case 2: return "\(items[0].description) or \(items[1].description)" + default: + let first = items[0..(_ sequence: S) -> Validator where S: Sequence & Sendable, S.Element == Value { + Validator { value in + InValidatorResult(item: value, items: .init(sequence)) + } + } + + /// Creates a validator that checks whether a value is contained within a specified list of values. + /// + /// This is a convenience method that allows you to pass a variadic list of values instead of a sequence. + /// + /// - Parameter array: A variadic list of values against which the input value will be validated. + /// - Returns: A `Validator` instance that performs the inclusion check. + public static func `in`(_ array: Value...) -> Validator { + .in(array) + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Max.swift b/Sources/ValidationKit/Validators/Validator+Max.swift new file mode 100644 index 0000000..d61b210 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Max.swift @@ -0,0 +1,62 @@ +// +// Validator+Max.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of a maximum value validation. + public struct MaxValidatorResult: ValidatorResult where T: Comparable & Sendable { + + /// The actual value being validated. + public let value: Value + + /// The maximum acceptable value for the validation. + public let max: T + + /// Determines if the validation has failed. + public let isFailure: Bool + + /// Provides a description when the validation succeeds. + public var successDescription: String? { + "value \(value) is less than or equal to \(max)" + } + + /// Provides a description when the validation fails. + public var failureDescription: String? { + "value \(value) is greater than \(max)" + } + } +} + +extension Validator where Value: Comparable { + + /// Creates a validator that checks if a value is less than or equal to a specified maximum. + /// + /// - Parameter max: The maximum acceptable value. + /// - Returns: A `Validator` that validates whether the value is less than or equal to `max`. + public static func max(_ max: Value) -> Validator { + Validator { value in + MaxValidatorResult(value: value, max: max, isFailure: value > max) + } + } +} + +extension Validator where Value: Collection { + + /// Creates a validator that checks if the collection's length is less than or equal to a specified maximum. + /// + /// This validator compares the count of elements in the collection against the provided maximum length. + /// + /// - Parameter length: The maximum acceptable number of elements in the collection. + /// - Returns: A `Validator` that validates whether the collection's length is less than or equal to `length`. + public static func max(length: Int) -> Validator { + Validator { value in + MaxValidatorResult(value: value, max: length, isFailure: value.count > length) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Min.swift b/Sources/ValidationKit/Validators/Validator+Min.swift new file mode 100644 index 0000000..5317316 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Min.swift @@ -0,0 +1,62 @@ +// +// Validator+Min.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of a minimum value validation. + public struct MinValidatorResult: ValidatorResult where T: Comparable & Sendable { + + /// The actual value being validated. + public let value: Value + + /// The minimum acceptable value for the validation. + public let min: T + + /// Indicates whether the validation has failed. + public let isFailure: Bool + + /// Provides a description when the validation succeeds. + public var successDescription: String? { + "value \(value) is greater than or equal to \(min)" + } + + /// Provides a description when the validation fails. + public var failureDescription: String? { + "value \(value) is less than \(min)" + } + } +} + +extension Validator where Value: Comparable { + + /// Creates a validator that checks if a value is greater than or equal to a specified minimum. + /// + /// - Parameter min: The minimum acceptable value. + /// - Returns: A `Validator` that validates whether the value is greater than or equal to `min`. + public static func min(_ min: Value) -> Validator { + Validator { value in + MinValidatorResult(value: value, min: min, isFailure: value < min) + } + } +} + +extension Validator where Value: Collection { + + /// Creates a validator that checks if the collection's length is less than or equal to a specified maximum. + /// + /// This validator compares the count of elements in the collection against the provided minimum length. + /// + /// - Parameter length: The minimum acceptable number of elements in the collection. + /// - Returns: A `Validator` that validates whether the collection's length is greater than or equal to `length`. + public static func min(length: Int) -> Validator { + Validator { value in + MinValidatorResult(value: value, min: length, isFailure: value.count < length) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Nil.swift b/Sources/ValidationKit/Validators/Validator+Nil.swift new file mode 100644 index 0000000..9770ec6 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Nil.swift @@ -0,0 +1,45 @@ +// +// Validator+Nil.swift +// KBValidation +// +// Created by kubens.com on 05/12/2024. +// + +import Foundation + +extension Validator where Value: AnyOptional { + + /// Represents the result of validating whether an optional value is `nil`. + /// + /// `NilValidatorResult` captures whether an optional value is `nil` or not. + /// It provides appropriate descriptions based on the presence or absence of a value. + public struct NilValidatorResult: ValidatorResult { + + /// Indicates whether the optional value is `nil`. + public let isNil: Bool + + /// A description provided when the validation succeeds (i.e., the value is `nil`). + public let successDescription: String? = "is nil" + + /// A description provided when the validation fails (i.e., the value is not `nil`). + public let failureDescription: String? = "is not nil" + + /// Determines if the validation has failed. + /// + /// - Returns: `true` if the optional value is not `nil`; otherwise, `false`. + public var isFailure: Bool { + !isNil + } + } + + /// Creates a validator that checks whether an optional value is `nil`. + /// + /// This validator succeeds if the optional value is `nil` and fails otherwise. + /// + /// - Returns: A `Validator` that validates whether an optional value is `nil`. + public static var `nil`: Validator { + Validator { value in + NilValidatorResult(isNil: value.isNil) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Not.swift b/Sources/ValidationKit/Validators/Validator+Not.swift new file mode 100644 index 0000000..d6dfd67 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Not.swift @@ -0,0 +1,49 @@ +// +// Validator+Not.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of applying a logical **NOT** operation to a validator. + /// + /// `NotValidatorResult` inverts the outcome of an existing `ValidatorResult`. + /// If the original validator passes, the `NotValidatorResult` fails, and vice versa. + public struct NotValidatorResult: ValidatorResult { + + /// The original validation result being inverted. + public let result: ValidatorResult + + /// Indicates whether the inverted validation has failed. + public var isFailure: Bool { + !result.isFailure + } + + /// Provides a description of the validation success. + public var successDescription: String? { + result.failureDescription + } + + /// Provides a description of the validation failure. + public var failureDescription: String? { + result.successDescription + } + } + + /// Creates a validator that inverts the result of another validator. + /// + /// This method allows you to define a validator that fails when the original validator passes, and passes when the original validator fails. + /// It is useful for scenarios where you need to ensure that a value does **not** satisfy certain conditions. + /// + /// - Parameter validator: The original `Validator` to invert. + /// - Returns: A new `Validator` that inversely applies the original validator's logic. + static public func not(_ validator: Validator) -> Validator { + Validator { value in + NotValidatorResult(result: validator.validate(value)) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Optional.swift b/Sources/ValidationKit/Validators/Validator+Optional.swift new file mode 100644 index 0000000..5d4de92 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Optional.swift @@ -0,0 +1,120 @@ +// +// Validator+Optional.swift +// ValidationKit +// +// Created by kubens.com on 07/12/2024. +// + +extension Validator { + + /// Represents the result of validating an optional value. + /// + /// `OptionalValidatorResult` encapsulates the result of validating an optional value. + /// It holds an optional `ValidatorResult` that represents the outcome of validating the unwrapped value. + /// If the optional is `nil`, the `result` will be `nil`, indicating that no further validation was performed. + public struct OptionalValidatorResult: ValidatorResult { + + /// The result of validating the unwrapped value. + /// + /// - Note: If the optional value is `nil`, this will be `nil`, indicating that the validation + /// related to the unwrapped value was not performed. + public let result: ValidatorResult? + + /// Indicates whether the validation has failed. + /// + /// - Returns: `true` if the unwrapped value failed validation; otherwise, `false`. + public var isFailure: Bool { + guard let result else { return true } + return result.isFailure + } + + /// Provides a description when the validation succeeds. + /// + /// - Returns: The success description from the unwrapped value's validation result, if available. + public var successDescription: String? { + result?.successDescription + } + + /// Provides a description when the validation fails. + /// + /// - Returns: The failure description from the unwrapped value's validation result, if available. + public var failureDescription: String? { + guard let result else { return "no validation performed" } + return result.failureDescription + } + } + + // MARK: - OR Validators + + /// Combines an optional validator and a non-optional validator using logical OR. + /// + /// The combined validator succeeds if either: + /// 1. The `lhs` optional validator succeeds (e.g., the value is `nil` if allowed). + /// 2. The `rhs` validator succeeds when applied to the unwrapped value. + /// + /// - Parameters: + /// - lhs: The first `Validator` for the optional value to combine. + /// - rhs: The second `Validator` to apply to the unwrapped value. + /// - Returns: A new `Validator` that represents the logical OR of `lhs` and `rhs`. + public static func or(_ lhs: Validator, _ rhs: Validator) -> Validator { + .or(lhs, Validator { value in + OptionalValidatorResult(result: value.flatMap(rhs.validate)) + }) + } + + /// Combines an optional validator and a non-optional validator using logical OR. + /// + /// The combined validator succeeds if either: + /// 1. The `lhs` validator succeeds when applied to the unwrapped value. + /// 2. The `rhs` optional validator succeeds (e.g., the value is `nil` if allowed). + /// + /// - Parameters: + /// - lhs: The first `Validator` for the optional value to combine. + /// - rhs: The second `Validator` to apply to the unwrapped value. + /// - Returns: A new `Validator` that represents the logical OR of `lhs` and `rhs`. + public static func or(_ lhs: Validator, _ rhs: Validator) -> Validator { + .or(Validator { value in + OptionalValidatorResult(result: value.flatMap(lhs.validate)) + }, rhs) + } + + // MARK: - AND Validators + + /// Combines an optional validator and a non-optional validator using logical AND. + /// + /// The combined validator succeeds only if both: + /// 1. The `lhs` optional validator succeeds (i.e., the optional is not `nil` if required). + /// 2. The `rhs` validator succeeds when applied to the unwrapped value of the optional. + /// + /// If the optional is `nil` and the `lhs` validator does not enforce non-nil, + /// the combined validator succeeds by default. + /// + /// - Parameters: + /// - lhs: The first `Validator` for the optional value to combine. + /// - rhs: The second `Validator` to apply to the unwrapped value. + /// - Returns: A new `Validator` that represents the logical AND of `lhs` and `rhs`. + public static func and(_ lhs: Validator, _ rhs: Validator) -> Validator { + .and(lhs, Validator { value in + OptionalValidatorResult(result: value.flatMap(rhs.validate)) + }) + } + + /// Combines an optional validator and a non-optional validator using logical AND. + /// + /// The combined validator succeeds only if both: + /// 1. The `lhs` validator succeeds when applied to the unwrapped value of the optional. + /// 2. The `rhs` optional validator succeeds (i.e., the optional is not `nil` if required). + /// + /// If the optional is `nil` and the `lhs` validator does not enforce non-nil, + /// the combined validator succeeds by default. + /// + /// - Parameters: + /// - lhs: The first `Validator` for the optional value to combine. + /// - rhs: The second `Validator` to apply to the unwrapped value. + /// - Returns: A new `Validator` that represents the logical AND of `lhs` and `rhs`. + public static func and(_ lhs: Validator, _ rhs: Validator) -> Validator { + .and(Validator { value in + OptionalValidatorResult(result: value.flatMap(lhs.validate)) + }, rhs) + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Or.swift b/Sources/ValidationKit/Validators/Validator+Or.swift new file mode 100644 index 0000000..8d6c314 --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Or.swift @@ -0,0 +1,77 @@ +// +// Validator+Or.swift +// ValidationKit +// +// Created by kubens.com on 06/12/2024. +// + +import Foundation + +extension Validator { + + /// Represents the result of combining two validators using logical OR. + /// + /// `OrValidatorResult` holds the results of two individual validators and determines + /// the overall validation outcome based on their combination. The validation succeeds + /// if at least one of the validators succeeds. + public struct OrValidatorResult: ValidatorResult { + + /// The result from the first validator. + public let left: ValidatorResult + + /// The result from the second validator. + public let right: ValidatorResult + + /// Indicates whether the combined validation has failed. + /// + /// The validation fails only if both the left and right validators fail. + public var isFailure: Bool { + left.isFailure && right.isFailure + } + + /// Provides a description when the combined validation succeeds. + public var successDescription: String? { + switch (left.successDescription, right.successDescription) { + case let (.some(left), .some(right)): "\(left) and \(right)" + case let (.some(left), .none): left + case let (.none, .some(right)): right + case (.none, .none): nil + } + } + + /// Provides a description when the combined validation fails. + public var failureDescription: String? { + switch (left.failureDescription, right.failureDescription) { + case let (.some(left), .some(right)): "\(left) and \(right)" + case let (.some(left), .none): left + case let (.none, .some(right)): right + case (.none, .none): nil + } + } + + /// Provides a textual representation of the validation result. + public var description: String { + switch (left.isFailure, right.isFailure) { + case (true, true): failureDescription ?? "validation failed" + case (true, false): right.successDescription ?? "validation succeeded" + case (false, false): successDescription ?? "validation succeeded" + case (false, true): left.successDescription ?? "validation succeeded" + } + } + } + + /// Combines two validators using logical OR. + /// + /// The combined validator succeeds if at least one of the `lhs` or `rhs` validators succeeds. + /// It fails only if both validators fail. + /// + /// - Parameters: + /// - lhs: The first `Validator` to combine. + /// - rhs: The second `Validator` to combine. + /// - Returns: A new `Validator` that represents the logical OR of `lhs` and `rhs`. + public static func or(_ lhs: Validator, _ rhs: Validator) -> Validator { + Validator { value in + OrValidatorResult(left: lhs.validate(value), right: rhs.validate(value)) + } + } +} diff --git a/Sources/ValidationKit/Validators/Validator+Pattern.swift b/Sources/ValidationKit/Validators/Validator+Pattern.swift new file mode 100644 index 0000000..6eef5ec --- /dev/null +++ b/Sources/ValidationKit/Validators/Validator+Pattern.swift @@ -0,0 +1,55 @@ +// +// Validator+Pattern.swift +// KBValidation +// +// Created by kubens.com on 02/12/2024. +// + +import Foundation + +extension Validator where Value == String { + + /// Represents the result of a regex pattern validation. + public struct PatternValidatorResult: ValidatorResult { + + /// The pattern used for validation. + public let pattern: String + + /// Indicates whether the string matches the regex pattern. + public let isValidPattern: Bool + + /// Indicates whether the validation failed. + public var isFailure: Bool { + !isValidPattern + } + + /// Provides a description of the validation success. + public var successDescription: String? { + "is a valid pattern '\(pattern)'" + } + + /// Provides a description of the validation failure. + public var failureDescription: String? { + "is not a valid pattern '\(pattern)'" + } + } + + /// Creates a validator that checks if a string matches a given regex pattern. + /// + /// This validator uses the provided regex pattern to validate the input string. + /// If the string matches the entire regex pattern, validation succeeds. + /// If the string does not match the pattern or if the pattern is invalid, validation fails. + /// + /// - Parameter pattern: The regex pattern used for validation. + /// - Returns: A `Validator` that checks if the string matches the pattern. + static public func pattern(_ pattern: String) -> Validator { + Validator { value in + if let range = value.range(of: pattern, options: [.regularExpression]) { + let isValidPattern = range.lowerBound == value.startIndex && range.upperBound == value.endIndex + return PatternValidatorResult(pattern: pattern, isValidPattern: isValidPattern) + } else { + return PatternValidatorResult(pattern: pattern, isValidPattern: false) + } + } + } +} diff --git a/Tests/ValidationKitTests/Helpers/Tag.swift b/Tests/ValidationKitTests/Helpers/Tag.swift new file mode 100644 index 0000000..fae13fb --- /dev/null +++ b/Tests/ValidationKitTests/Helpers/Tag.swift @@ -0,0 +1,13 @@ +// +// Tag.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing + +extension Tag { + + @Tag static var validator: Self +} diff --git a/Tests/ValidationKitTests/Helpers/Validator+Stub.swift b/Tests/ValidationKitTests/Helpers/Validator+Stub.swift new file mode 100644 index 0000000..4b61ae5 --- /dev/null +++ b/Tests/ValidationKitTests/Helpers/Validator+Stub.swift @@ -0,0 +1,29 @@ +// +// Validator+Stub.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +@testable import ValidationKit + +/// An extension of the `Validator` struct to provide stub validators for testing purposes. +internal extension Validator { + + /// A stub implementation of `ValidatorResult` for testing purposes. + struct StubValidatorResult: ValidatorResult { + /// Determines if the validation has failed. + let isFailure: Bool + + /// A description provided when the validation succeeds. + let successDescription: String? = "success description" + + /// A description provided when the validation fails. + let failureDescription: String? = "failure description" + } + + /// Creates a stub `Validator` that returns a predefined validation result. + static func stub(_ isValid: Bool) -> Validator { + Validator { _ in StubValidatorResult(isFailure: !isValid) } + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/AllOfValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/AllOfValidatorTests.swift new file mode 100644 index 0000000..dbbbd8a --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/AllOfValidatorTests.swift @@ -0,0 +1,37 @@ +// +// AllOfValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("AllOf Validator", .tags(.validator)) +struct AllOfValidatorTests { + + @Test("validate should pass when all validators successed") + func validateAllOfValidatorsSuccessed() { + let result = Validator.allOf(Validator.stub(true), Validator.stub(true)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "success description and success description") + } + + @Test("validate should not pass when any of the validators fail") + func validateAllOfValidatorsFailed() { + let result = Validator.allOf(Validator.stub(true), Validator.stub(false)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description") + } + + @Test("validate should not pass when all validators failure") + func validateAllOfValidatorsFailure() { + let result = Validator.allOf(Validator.stub(false), Validator.stub(false)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description and failure description") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/AndValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/AndValidatorTests.swift new file mode 100644 index 0000000..a1c31e6 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/AndValidatorTests.swift @@ -0,0 +1,45 @@ +// +// AndValidatorTests.swift +// ValidationKit +// +// Created by kubens.com on 08/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("And Validator") +struct AndValidatorTests { + + @Test("should pass when first and second validator pass") + func firstAndSecondValidatorPass() { + let result = Validator.and(.stub(true), .stub(true)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "success description and success description") + } + + @Test("should fail when first validator pass and second validator fail") + func firstValidatorPassAndSecondValidatorFail() { + let result = Validator.and(.stub(true), .stub(false)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description") + } + + @Test("should fail when first validator fail and second validator pass") + func firstValidatorFailAndSecondValidatorPass() { + let result = Validator.and(.stub(false), .stub(true)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description") + } + + @Test("should fail when first and second validator fail") + func firstAndSecondValidatorFail() { + let result = Validator.and(.stub(false), .stub(false)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description and failure description") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/EmptyValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/EmptyValidatorTests.swift new file mode 100644 index 0000000..8b555e0 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/EmptyValidatorTests.swift @@ -0,0 +1,49 @@ +// +// EmptyValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Empty Validator", .tags(.validator)) +struct EmptyValidatorTests { + + @Test("validate should pass when the string is empty") + func validateEmptyStringWithSuccess() { + let value = "" + let result = Validator.empty.validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is empty") + } + + @Test("validate should pass when the collection is empty") + func validateEmptyCollectionWithSuccess() { + let value: [Int] = [] + let result = Validator.empty.validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is empty") + } + + @Test("validate should fail when the string is not empty") + func validateNonEmptyStringWithSuccess() { + let value = "Hello" + let result = Validator.empty.validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not empty") + } + + @Test("validate should fail when the collection is not empty") + func validateNonEmptyCollectionWithFailure() { + let value = [1, 2] + let result = Validator.empty.validate(value) + + #expect(result.isFailure == true) + #expect(result.failureDescription == "is not empty") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/InValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/InValidatorTests.swift new file mode 100644 index 0000000..3613419 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/InValidatorTests.swift @@ -0,0 +1,40 @@ +// +// InValidatorTests.swift +// ValidationKit +// +// Created by Jakub Łaptaś on 09/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("In Validator") +struct InValidatorTests { + + enum TestEnum: String, Equatable, CaseIterable, CustomStringConvertible { + case one + case two + case three + + var description: String { + self.rawValue + } + } + + @Test("validate should pass when value is in allowed list", arguments: TestEnum.allCases) + func validateAllowedValues(value: TestEnum) throws { + let result = Validator.in(TestEnum.allCases).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is one, two or three") + } + + @Test("validate should fail when value is not in allowed list") + func validateNotAllowedValues() throws { + let value = TestEnum.two + let result = Validator.in(.one, .three).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not one or three") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/MaxValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/MaxValidatorTests.swift new file mode 100644 index 0000000..539067c --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/MaxValidatorTests.swift @@ -0,0 +1,73 @@ +// +// MaxValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Max Validator", .tags(.validator)) +struct MaxValidatorTests { + + @Test("validate should pass when value is equal to max") + func validateEqualToMax() { + let max = 1 + let value = 1 + let result = Validator.max(max).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is less than or equal to \(max)") + } + + @Test("validate should pass when String lenght is equal to max") + func validateEqualToMaxStringLenght() { + let max = 2 + let value = "ab" + let result = Validator.max(length: max).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is less than or equal to \(max)") + } + + @Test("validate should pass when value is less than max") + func validateLessThanMax() { + let max = 2 + let value = 1 + let result = Validator.max(max).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is less than or equal to \(max)") + } + + @Test("validate should pass when String lenght is less than max") + func validateLessThanMaxStringLenght() { + let max = 3 + let value = "abc" + let result = Validator.max(length: max).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is less than or equal to \(max)") + } + + @Test("validate should fail when value is greater than max") + func validateGreateThanMax() { + let max = 1 + let value = 2 + let result = Validator.max(max).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "value \(value) is greater than \(max)") + } + + @Test("validate should fail when String lenght is greate than max") + func validateGreateThanMaxStringLenght() { + let max = 2 + let value = "abcd" + let result = Validator.max(length: max).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "value \(value) is greater than \(max)") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/MinValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/MinValidatorTests.swift new file mode 100644 index 0000000..e5e9e22 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/MinValidatorTests.swift @@ -0,0 +1,73 @@ +// +// MinValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Min Validator", .tags(.validator)) +struct MinValidatorTests { + + @Test("validate should pass when value is equal to min") + func validateEqualToMin() { + let min = 1 + let value = 1 + let result = Validator.min(min).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is greater than or equal to \(min)") + } + + @Test("validate should pass when String lenght is qual to min") + func validateEqualToMinStringLength() { + let min = 1 + let value = "a" + let result = Validator.min(length: min).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is greater than or equal to \(min)") + } + + @Test("validate should pass when value is greater than min") + func validateGreaterThanMin() { + let min = 1 + let value = 2 + let result = Validator.min(min).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is greater than or equal to \(min)") + } + + @Test("validate should pass when String length is greater than min") + func validateGreaterThanMinStringLength() { + let min = 1 + let value = "ab" + let result = Validator.min(length: min).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "value \(value) is greater than or equal to \(min)") + } + + @Test("validate should fail when value is less than min") + func validateLessThanMin() { + let min = 1 + let value = 0 + let result = Validator.min(min).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "value \(value) is less than \(min)") + } + + @Test("validate should fail when String lenght is less than min") + func validateLessThanMinStringLength() { + let min = 2 + let value = "a" + let result = Validator.min(length: min).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "value \(value) is less than \(min)") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/NilValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/NilValidatorTests.swift new file mode 100644 index 0000000..235fe7b --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/NilValidatorTests.swift @@ -0,0 +1,31 @@ +// +// NilValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 05/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Nil Validator") +struct NilValidatorTests { + + @Test("validate should pass when value is nil") + func validateNilSuccessed() { + let value: Int? = nil + let result = Validator.nil.validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is nil") + } + + @Test("validate should not pass when value is not nil") + func validateNotNilFailed() { + let value: Int? = 1 + let result = Validator.nil.validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not nil") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/NotValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/NotValidatorTests.swift new file mode 100644 index 0000000..01311f3 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/NotValidatorTests.swift @@ -0,0 +1,29 @@ +// +// NotValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Not Validator", .tags(.validator)) +struct NotValidatorTests { + + @Test("validate should pass when the underlying validator fails") + func validateUnderlyingValidatorFails() { + let result = Validator.not(.stub(false)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "failure description") + } + + @Test("validate should fail when the underlying validator passes") + func validateUnderlyingValidatorPasses() { + let result = Validator.not(.stub(true)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "success description") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/OptionalValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/OptionalValidatorTests.swift new file mode 100644 index 0000000..62899b4 --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/OptionalValidatorTests.swift @@ -0,0 +1,157 @@ +// +// OptionalValidatorTests.swift +// ValidationKit +// +// Created by kubens.com on 07/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Optional Validator", .tags(.validator)) +struct OptionalValidatorTests { + + @Suite("OR Validator") + struct OrValidatorTests { + + @Suite("when value is nil") + struct whenValueIsNilTests { + + var value: Int? = nil + + @Test("validate should pass when first validator permits nil and omit second validator") + func validateFirstValidatorOmitSecondValidator() throws { + let result = Validator.or(.nil, .stub(true)).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is nil") + } + + @Test("validate should pass when second validator permits nil and omit first validator") + func validateSecondValidatorOmitFirstValidator() throws { + let result = Validator.or(.stub(true), .nil).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is nil") + } + } + + @Suite("when value is not nil") + struct whenValueIsNotNilTests { + + var value: Int? = 1 + + @Test("validate should pass when first validator premits nil and second validator pass") + func validateFirstValidatorPremitsNilPassSecondValidatorPass() throws { + let result = Validator.or(.nil, .stub(true)).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "success description") + } + + @Test("validate should pass when first validator pass and second premits nil") + func validateFirstValidatorPassSecondValidatorPremitsNil() throws { + let result = Validator.or(.stub(true), .nil).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "success description") + } + + @Test("validate should fail when first validator premits nil and second validator fail") + func validateFirstValidatorPremitsNilPassSecondValidatorFail() throws { + let result = Validator.or(.nil, .stub(false)).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not nil and failure description") + } + + @Test("validate should fail when first validator fail and second validator premits nil") + func validateFirstValidatorFailSecondValidatorPremitsNil() throws { + let result = Validator.or(.stub(false), .nil).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "failure description and is not nil") + } + } + } + + @Suite("AND Validator") + struct ANDValidatorTests { + + @Suite("when value is nil") + struct whenValueIsNil { + + var value: Int? = nil + + @Test("validate should fail when first validator not permits nil and second validator try validate") + func validateFirstValidatorNotPermitsNilPassSecondValidatorPass() { + let result = Validator.and(.not(.nil), .max(1)).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is nil and no validation performed") + } + + @Test("validate should fail when first validator try validate and second validator not permits nil") + func validateFirstValidatorPassSecondValidatorNotPermitsNil() { + let result = Validator.and(.max(1), .not(.nil)).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "no validation performed and is nil") + } + + @Test("validate should fail when first validator permits nil and second validator pass") + func validateFirstValidatorPremitsNilPassSecondValidatorPass() { + let result = Validator.and(.nil, .stub(true)).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "no validation performed") + } + + @Test("validate should fail when first validator validator pass and second validator permits nil") + func validateFirstValidatorPassSecondValidatorPremitsNil() { + let result = Validator.and(.stub(true), .nil).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "no validation performed") + } + } + + @Suite("when value is not nil") + struct whenValueIsNotNil { + + var value: Int? = 1 + + @Test("validate should fail when first validator premits nil and second validator pass") + func validateFirstValidatorPremitsNilPassSecondValidatorPass() { + let result = Validator.and(.nil, .stub(true)).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not nil") + } + + @Test("validate should fail when first validator pass and second validator premits nil") + func validateFirstValidatorPassSecondValidatorPremitsNil() { + let result = Validator.and(.stub(true), .nil).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not nil") + } + + @Test("validate should pass when first validator not premits nil and second validator pass") + func validateFirstValidatorNonPremitsNilPassSecondValidatorPass() { + let result = Validator.and(.not(.nil), .stub(true)).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is not nil and success description") + } + + @Test("validate should pass when first validator pass and second validator not premits nil") + func validateFirstValidatorPassSecondValidatorNonPremitsNil() { + let result = Validator.and(.stub(true), .not(.nil)).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "success description and is not nil") + } + } + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/OrValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/OrValidatorTests.swift new file mode 100644 index 0000000..ce55c7d --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/OrValidatorTests.swift @@ -0,0 +1,45 @@ +// +// OrValidatorTests.swift +// ValidationKit +// +// Created by kubens.com on 08/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Or Validator") +struct OrValidatorTests { + + @Test("validate should pass when first and second validator pass") + func validateFirstAndSecondPass() { + let result = Validator.or(.stub(true), .stub(true)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "success description and success description") + } + + @Test("validate should pass when first validator pass and second validator fail") + func validateFirstPassAndSecondFail() { + let result = Validator.or(.stub(true), .stub(false)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "success description") + } + + @Test("validate should pass when first validator fail and second validator pass") + func validateFirstFailAndSecondPass() { + let result = Validator.or(.stub(false), .stub(true)).validate("") + + #expect(result.isFailure == false) + #expect(result.description == "success description") + } + + @Test("validate should fail when first and second validator fail") + func validateFirstAndSecondFail() { + let result = Validator.or(.stub(false), .stub(false)).validate("") + + #expect(result.isFailure == true) + #expect(result.description == "failure description and failure description") + } +} diff --git a/Tests/ValidationKitTests/ValidatorsTests/PatternValidatorTests.swift b/Tests/ValidationKitTests/ValidatorsTests/PatternValidatorTests.swift new file mode 100644 index 0000000..5b59d6f --- /dev/null +++ b/Tests/ValidationKitTests/ValidatorsTests/PatternValidatorTests.swift @@ -0,0 +1,43 @@ +// +// PatternValidatorTests.swift +// KBValidation +// +// Created by kubens.com on 04/12/2024. +// + +import Testing +@testable import ValidationKit + +@Suite("Pattern Validator", .tags(.validator)) +struct PatternValidatorTests { + + @Test("validate should pass when the value matches the regex pattern") + func validateValueWithSuccess() { + let value = "ABC" + let pattern = "^[A-Z]{3}$" + let result = Validator.pattern(pattern).validate(value) + + #expect(result.isFailure == false) + #expect(result.description == "is a valid pattern '\(pattern)'") + } + + @Test("validate should fail when the value does not match the regex pattern") + func validateValueWithFailure() { + let value = "abcd" + let pattern = "^[A-Z]{3}$" + let result = Validator.pattern(pattern).validate(value) + + #expect(result.isFailure == true) + #expect(result.description == "is not a valid pattern '\(pattern)'") + } + + @Test("validate should fail when the regex pattern is invalid") + func validateWithInvalidRegexPattern() { + let value = "ABC" + let invalidPattern = "[A-Z{3}$" // Missing closing bracket + + let result = Validator.pattern(invalidPattern).validate(value) + #expect(result.isFailure == true) + #expect(result.description == "is not a valid pattern '\(invalidPattern)'") + } +}