Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
@@ -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
7 changes: 6 additions & 1 deletion Package.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
)
]
)
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
14 changes: 14 additions & 0 deletions Sources/ValidationKit/PrivacyInfo.xcprivacy
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>NSPrivacyTracking</key>
<false/>
<key>NSPrivacyCollectedDataTypes</key>
<array/>
<key>NSPrivacyTrackingDomains</key>
<array/>
<key>NSPrivacyAccessedAPITypes</key>
<array/>
</dict>
</plist>
47 changes: 47 additions & 0 deletions Sources/ValidationKit/Protocols/Validatable.swift
Original file line number Diff line number Diff line change
@@ -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<Self>)

/// 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)
}
}
41 changes: 41 additions & 0 deletions Sources/ValidationKit/Protocols/ValidatorResult.swift
Original file line number Diff line number Diff line change
@@ -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"
}
}
}
31 changes: 31 additions & 0 deletions Sources/ValidationKit/Support/AnyOptional.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
42 changes: 42 additions & 0 deletions Sources/ValidationKit/Support/AnyValidationRule.swift
Original file line number Diff line number Diff line change
@@ -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<Object>: 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<Value>(_ rule: ValidationRule<Object, Value>) {
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)
}
}
103 changes: 103 additions & 0 deletions Sources/ValidationKit/Support/Operators+Validator.swift
Original file line number Diff line number Diff line change
@@ -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 ! <Value: Sendable>(_ validator: Validator<Value>) -> Validator<Value> {
Validator<Value>.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 || <Value: Sendable>(lhs: Validator<Value>, rhs: Validator<Value>) -> Validator<Value> {
Validator<Value>.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 || <Value: Sendable>(lhs: Validator<Value?>, rhs: Validator<Value>) -> Validator<Value?> {
Validator<Value>.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 || <Value: Sendable>(lhs: Validator<Value>, rhs: Validator<Value?>) -> Validator<Value?> {
Validator<Value>.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 && <Value: Sendable>(lhs: Validator<Value>, rhs: Validator<Value>) -> Validator<Value> {
Validator<Value>.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 && <Value: Sendable>(lhs: Validator<Value?>, rhs: Validator<Value>) -> Validator<Value?> {
Validator<Value>.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 && <Value: Sendable>(lhs: Validator<Value>, rhs: Validator<Value?>) -> Validator<Value?> {
Validator<Value>.and(lhs, rhs)
}
30 changes: 30 additions & 0 deletions Sources/ValidationKit/ValidationError.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
Loading