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.
+[](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)'")
+ }
+}