diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml new file mode 100644 index 0000000..0380936 --- /dev/null +++ b/.github/workflows/docs.yml @@ -0,0 +1,76 @@ +name: Deploy DocC Documentation + +on: + push: + branches: ["main"] + workflow_dispatch: + inputs: + version: + description: 'Documentation version' + required: false + default: latest' + +permissions: + contents: write + +concurrency: + group: "pages" + cancel-in-progress: true + +jobs: + deploy: + runs-on: macos-14 + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Xcode + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: latest-stable + + - name: Get Version + id: version + run: | + if [ -n "${{ github.event.inputs.version }}" ]; then + VERSION="${{ github.event.inputs.version }}" + else + VERSION="latest" + fi + echo "VERSION=$VERSION" >> $GITHUB_OUTPUT + echo "Building documentation for version: $VERSION" + + - name: Build DocC + id: build + uses: space-code/build-docc@main + with: + schemes: '["ValidatorCore", "ValidatorUI"]' + version: ${{ steps.version.outputs.version }} + + - name: Generate Index Page + uses: space-code/generate-index@v1.0.0 + with: + version: ${{ steps.version.outputs.version }} + project-name: 'Validator' + project-description: 'Validator is a modern, lightweight Swift framework that provides elegant and type-safe input validation.' + modules: | + [ + { + "name": "ValidatorCore", + "path": "validatorcore", + "description": "Core validation functionality and rules for Swift applications. Contains base types, protocols, and validator implementations.", + "badge": "Core Module" + }, + { + "name": "ValidatorUI", + "path": "validatorui", + "description": "UI components and helpers for building validation interfaces. Ready-to-use solutions for SwiftUI and UIKit.", + "badge": "UI Module" + } + ] + + - name: Deploy + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./docs \ No newline at end of file diff --git a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift index 0863ae1..b3ad591 100644 --- a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift +++ b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationError.swift @@ -5,8 +5,16 @@ import Foundation -/// `IValidationError` is the error type returned by Validator. +/// `IValidationError` is the protocol representing a validation error in `ValidatorCore`. +/// Any type conforming to `IValidationError` can be returned when validation fails. +/// +/// Example usage: +/// ```swift +/// struct MyError: IValidationError { +/// var message: String { "Invalid input" } +/// } +/// ``` public protocol IValidationError: Error { - /// The error message. + /// A human-readable error message describing why validation failed. var message: String { get } } diff --git a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift index 1fe4726..4742518 100644 --- a/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Core/Interfaces/IValidationRule.swift @@ -5,18 +5,30 @@ import Foundation -/// A type that verifies the input data. +/// `IValidationRule` is a generic protocol representing a validation rule. +/// Types conforming to this protocol define the logic for validating input values of type `Input`. +/// +/// You can create custom validators by implementing this protocol. +/// +/// Example: +/// ```swift +/// struct NonEmptyRule: IValidationRule { +/// let error = "Field cannot be empty" +/// func validate(input: String) -> Bool { +/// !input.isEmpty +/// } +/// } +/// ``` public protocol IValidationRule { - /// The validation type. + /// The type of input this validator works with. associatedtype Input - /// The validation error. + /// The validation error that will be returned if validation fails. var error: IValidationError { get } - /// Validates an input value. + /// Validates the provided input. /// - /// - Parameter input: The input value. - /// - /// - Returns: A validation result. + /// - Parameter input: The value to validate. + /// - Returns: `true` if the input passes validation, `false` otherwise. func validate(input: Input) -> Bool } diff --git a/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift b/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift index 2730d1b..341c42f 100644 --- a/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift +++ b/Sources/ValidatorCore/Classes/Core/Models/ValidationResult.swift @@ -5,12 +5,14 @@ import Foundation +/// `ValidationResult` represents the outcome of a validation operation. +/// It can either be `.valid` when all checks pass or `.invalid` with a list of errors. public enum ValidationResult { - /// Indicates that the validation was successful. + /// Indicates that validation succeeded. case valid - /// Indicates that the validation failed with a list of errors. + /// Indicates that validation failed. /// - /// - Parameter errors: An array of validation errors. + /// - Parameter errors: An array of `IValidationError` instances describing each failure. case invalid(errors: [IValidationError]) } diff --git a/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift index 8fce1b0..5822a16 100644 --- a/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift +++ b/Sources/ValidatorCore/Classes/Extensions/String+IValidationError.swift @@ -6,11 +6,21 @@ import Foundation #if hasFeature(RetroactiveAttribute) + /// Retroactive conformance to `Error` for `String` if the feature is enabled. extension String: @retroactive Error {} #endif // MARK: - String + IValidationError +/// Makes `String` conform to `IValidationError`. +/// This allows simple string literals to be used as validation errors without creating a custom type. +/// +/// Example: +/// ```swift +/// let error: IValidationError = "Invalid input" +/// print(error.message) // "Invalid input" +/// ``` extension String: IValidationError { + /// Returns the string itself as the error message. public var message: String { self } } diff --git a/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift b/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift index 78ebc7e..ee76526 100644 --- a/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift +++ b/Sources/ValidatorCore/Classes/Extensions/ValidationResult+Equatable.swift @@ -5,13 +5,23 @@ import Foundation +/// Adds `Equatable` conformance to `ValidationResult`. +/// Validation results are considered equal if both are `.valid` +/// or if both are `.invalid` and all error messages match. +/// +/// Example: +/// ```swift +/// let r1: ValidationResult = .invalid(errors: ["Error 1", "Error 2"]) +/// let r2: ValidationResult = .invalid(errors: ["Error 1", "Error 2"]) +/// print(r1 == r2) // true +/// ``` extension ValidationResult: Equatable { public static func == (lhs: ValidationResult, rhs: ValidationResult) -> Bool { switch (lhs, rhs) { case (.valid, .valid): true - case let (.invalid(errors: lhs), .invalid(errors: rhs)): - lhs.map(\.message).joined() == rhs.map(\.message).joined() + case let (.invalid(errors: lhsErrors), .invalid(errors: rhsErrors)): + lhsErrors.map(\.message).joined() == rhsErrors.map(\.message).joined() default: false } diff --git a/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift index dd3f8f1..8009640 100644 --- a/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/CharactersValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// A characters validation rule. +/// Validates that a string contains only allowed characters. +/// +/// # Example: +/// ```swift +/// let rule = CharactersValidationRule(characterSet: .letters, error: "Only letters allowed") +/// rule.validate(input: "Hello") // true +/// rule.validate(input: "Hello123") // false +/// ``` public struct CharactersValidationRule: IValidationRule { // MARK: Types @@ -13,13 +20,19 @@ public struct CharactersValidationRule: IValidationRule { // MARK: Properties + /// The set of allowed characters. public let characterSet: CharacterSet - /// The validation error. + /// The validation error returned if input contains invalid characters. public let error: IValidationError // MARK: Initialization + /// Initializes a characters validation rule. + /// + /// - Parameters: + /// - characterSet: Allowed character set. + /// - error: The validation error to return if input fails validation. public init(characterSet: CharacterSet, error: IValidationError) { self.characterSet = characterSet self.error = error diff --git a/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift index 76c2c70..5271f33 100644 --- a/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/CreditCardValidationRule.swift @@ -3,25 +3,50 @@ // Copyright © 2025 Space Code. All rights reserved. // -/// A credit card validation rule. +/// Validates credit card numbers using card type prefixes and the Luhn algorithm. +/// Supports Visa, MasterCard, Amex, JCB, and UnionPay. +/// The validation checks both the prefix/length of the card and the Luhn checksum. +/// +/// # Example: +/// ```swift +/// let rule = CreditCardValidationRule(types: [.visa, .masterCard], error: "Invalid card") +/// rule.validate(input: "4111 1111 1111 1111") // true for Visa +/// ``` public struct CreditCardValidationRule: IValidationRule { // MARK: Types + /// Represents the supported credit card types. + /// Each type has specific prefix and length rules. public enum CardType: String, Sendable, CaseIterable { - case visa, masterCard, amex, jcb, unionPay + /// Visa cards start with 4 and have 13, 16, or 19 digits. + case visa + /// MasterCard cards start with 51–55 and have 16 digits. + case masterCard + /// American Express cards start with 34 or 37 and have 15 digits. + case amex + /// JCB cards start with 3528–3589 and have 16 digits. + case jcb + /// UnionPay cards start with 62 and have 16–19 digits. + case unionPay } public typealias Input = String // MARK: Properties + /// Allowed card types for validation. public let types: [CardType] - /// The validation error. + /// Validation error returned if the card is invalid. public let error: IValidationError // MARK: Initialization + /// Initializes a credit card validation rule. + /// + /// - Parameters: + /// - types: The allowed card types. Defaults to all supported card types. + /// - error: The validation error to return if input fails validation. public init(types: [CardType] = CardType.allCases, error: IValidationError) { self.types = types self.error = error diff --git a/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift index ff264fd..e21211b 100644 --- a/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/EmailValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// An email validation rule. +/// Validates that a string is a valid email address. +/// Uses a regular expression conforming to RFC 5322 (simplified). +/// +/// # Example: +/// ```swift +/// let rule = EmailValidationRule(error: "Invalid email") +/// rule.validate(input: "user@example.com") // true +/// ``` public struct EmailValidationRule: IValidationRule { // MARK: Types @@ -13,11 +20,15 @@ public struct EmailValidationRule: IValidationRule { // MARK: Properties - /// The validation error. + /// Validation error returned if the email is invalid. public let error: IValidationError // MARK: Initialization + /// Initializes an email validation rule. + /// + /// - Parameters: + /// - error: The validation error to return if input fails validation. public init(error: IValidationError) { self.error = error } diff --git a/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift index 3974896..b3a1017 100644 --- a/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift @@ -5,7 +5,17 @@ import Foundation -/// A length validation rule. +/// Validates that a string length is within the specified range. +/// +/// This rule checks whether the input string's length is greater than or equal to `min` +/// and less than or equal to `max`. If the string length falls outside this range, validation fails. +/// +/// # Example: +/// ```swift +/// let rule = LengthValidationRule(min: 3, max: 10, error: "Length out of range") +/// rule.validate(input: "Hello") // true +/// rule.validate(input: "Hi") // false +/// ``` public struct LengthValidationRule: IValidationRule { // MARK: Types @@ -13,17 +23,23 @@ public struct LengthValidationRule: IValidationRule { // MARK: Properties - /// The minimum length. + /// The minimum allowed length of the string. Defaults to `0`. public let min: Int - /// The maximum length. + /// The maximum allowed length of the string. Defaults to `Int.max`. public let max: Int - /// The validation error. + /// The validation error returned if the input does not satisfy the length constraints. public let error: IValidationError // MARK: Initialization + /// Initializes a length validation rule. + /// + /// - Parameters: + /// - min: The minimum allowed string length. Defaults to 0. + /// - max: The maximum allowed string length. Defaults to `Int.max`. + /// - error: The validation error returned if input is invalid. public init(min: Int = .zero, max: Int = .max, error: IValidationError) { self.min = min self.max = max diff --git a/Sources/ValidatorCore/Classes/Rules/NonEmptyValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/NonEmptyValidationRule.swift index 2a4dd40..1fb169d 100644 --- a/Sources/ValidatorCore/Classes/Rules/NonEmptyValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/NonEmptyValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// A non empty validation rule. +/// Validates that a string is not empty. +/// +/// # Example: +/// ```swift +/// let rule = NonEmptyValidationRule(error: "Field required") +/// rule.validate(input: "") // false +/// rule.validate(input: "Text") // true +/// ``` public struct NonEmptyValidationRule: IValidationRule { // MARK: Types diff --git a/Sources/ValidatorCore/Classes/Rules/PrefixValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/PrefixValidationRule.swift index 62a70a8..2bf45f8 100644 --- a/Sources/ValidatorCore/Classes/Rules/PrefixValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/PrefixValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// A prefix validation rule. +/// Validates that a string starts with a specific prefix. +/// +/// # Example: +/// ```swift +/// let rule = PrefixValidationRule(prefix: "https://", error: "Must start with https://") +/// rule.validate(input: "https://example.com") // true +/// rule.validate(input: "http://example.com") // false +/// ``` public struct PrefixValidationRule: IValidationRule { // MARK: Types @@ -13,7 +20,7 @@ public struct PrefixValidationRule: IValidationRule { // MARK: Properties - /// The prefix. + /// The prefix that the input must start with. public let prefix: Input /// The validation error. @@ -21,6 +28,11 @@ public struct PrefixValidationRule: IValidationRule { // MARK: Initialization + /// Initializes a characters validation rule. + /// + /// - Parameters: + /// - prefix: The string that the input must start with. + /// - error: The validation error to return if input fails validation. public init(prefix: Input, error: IValidationError) { self.prefix = prefix self.error = error diff --git a/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift index cc8d799..dc084ea 100644 --- a/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/RegexValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// A regular expression validation rule. +/// Validates a string against a regular expression pattern. +/// +/// # Example: +/// ```swift +/// let rule = RegexValidationRule(pattern: "^[A-Z]+$", error: "Only uppercase letters allowed") +/// rule.validate(input: "HELLO") // true +/// rule.validate(input: "Hello") // false +/// ``` public struct RegexValidationRule: IValidationRule { // MARK: Types @@ -13,13 +20,19 @@ public struct RegexValidationRule: IValidationRule { // MARK: Properties - /// The regular expression pattern. + /// The regular expression pattern to validate the input against. public let pattern: String - /// The validation error. + + /// The validation error returned if input does not match the pattern. public let error: IValidationError // MARK: Initialization + /// Initializes a regex validation rule. + /// + /// - Parameters: + /// - pattern: The regex pattern used for validation. + /// - error: The validation error returned if input fails validation. public init(pattern: String, error: IValidationError) { self.pattern = pattern self.error = error diff --git a/Sources/ValidatorCore/Classes/Rules/SuffixValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/SuffixValidationRule.swift index c6de6ef..29514e7 100644 --- a/Sources/ValidatorCore/Classes/Rules/SuffixValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/SuffixValidationRule.swift @@ -5,7 +5,14 @@ import Foundation -/// A suffix validation rule. +/// Validates that a string ends with a specific suffix. +/// +/// # Example: +/// ```swift +/// let rule = SuffixValidationRule(suffix: ".com", error: "Must end with .com") +/// rule.validate(input: "example.com") // true +/// rule.validate(input: "example.org") // false +/// ``` public struct SuffixValidationRule: IValidationRule { // MARK: Types @@ -13,14 +20,19 @@ public struct SuffixValidationRule: IValidationRule { // MARK: Properties - /// The suffix. + /// The required suffix. public let suffix: Input - /// The validation error. + /// The validation error returned if input does not end with the suffix. public let error: IValidationError // MARK: Initialization + /// Initializes a suffix validation rule. + /// + /// - Parameters: + /// - suffix: The required string suffix. + /// - error: The validation error returned if input fails validation. public init(suffix: Input, error: IValidationError) { self.suffix = suffix self.error = error diff --git a/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift index 184a7d9..02dbee2 100644 --- a/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift +++ b/Sources/ValidatorCore/Classes/Rules/URLValidationRule.swift @@ -5,7 +5,15 @@ import Foundation -/// A url validation rule. +/// Validates that a string represents a valid URL. +/// +/// # Example: +/// ```swift +/// let rule = URLValidationRule(error: "Invalid URL") +/// rule.validate(input: "https://example.com") // true +/// rule.validate(input: "not_a_url") // false +/// ``` + public struct URLValidationRule: IValidationRule { // MARK: Types @@ -13,11 +21,14 @@ public struct URLValidationRule: IValidationRule { // MARK: Properties - /// The validation error. + /// The validation error returned if the input is not a valid URL. public let error: IValidationError // MARK: Initialization + /// Initializes a URL validation rule. + /// + /// - Parameter error: The validation error returned if input fails validation. public init(error: IValidationError) { self.error = error } diff --git a/Sources/ValidatorCore/Validator.docc/Articles/custom-validation-rule.md b/Sources/ValidatorCore/Validator.docc/Articles/custom-validation-rule.md new file mode 100644 index 0000000..1583e94 --- /dev/null +++ b/Sources/ValidatorCore/Validator.docc/Articles/custom-validation-rule.md @@ -0,0 +1,90 @@ +# Creating Custom Validation Rules + +ValidatorCore is designed to be extensible. If the built-in validation rules do not meet your needs, you can create custom validation rules tailored to your application. + +This guide will show you how to create, use, and test a custom rule. + +## Step 1: Conform to IValidationRule + +All validation rules in ValidatorCore conform to the IValidationRule protocol. It requires specifying the input type and implementing a validate function. + +```swift +import ValidatorCore + +struct ContainsDigitRule: IValidationRule { + let error: String + + func validate(input: String) -> Bool { + if input.rangeOfCharacter(from: .decimalDigits) != nil { + return true + } else { + return false + } + } +} +``` + +Explanation: +- `IValidationRule` – our rule will validate strings. +- `error` – the message returned if validation fails. +- `validate(input:)` – the core logic. Here, we check if the input contains at least one digit. + +## Step 2: Use Your Custom Rule + +Once the rule is defined, you can use it with Validator just like any built-in rule: + +```swift +let validator = Validator() +let password = "Password123" + +let result = validator.validate( + input: password, + rule: ContainsDigitRule(error: "Password must contain at least one number") +) + +switch result { +case .success: + print("Password is valid ✅") +case .failure(let error): + print("Password is invalid ❌: \(error.description)") +} +``` + +Explanation: +- The custom rule integrates seamlessly with the existing validation workflow. +- Validation result handling remains the same as with built-in rules. + +## Step 3: Combining Custom and Built-in Rules + +You can combine custom rules with built-in rules for more complex validation logic: + +```swift +let password = "Password123" + +let rules: [any IValidationRule] = [ + NonEmptyValidationRule(error: "Password cannot be empty"), + LengthValidationRule(min: 8, max: 20, error: "Password must be 8-20 characters"), + ContainsDigitRule(error: "Password must contain at least one number") +] + +let validator = Validator() + +let result = validator.validate(input: password, rules: rules) +``` + +Explanation: +- Each rule validates one specific aspect of the input (e.g., non-empty, length, contains a digit). +- The Validator applies each rule independently. +- This approach keeps rules focused, reusable, and simple, while Validator handles iterating through multiple rules. +- You can freely mix custom and built-in rules to create comprehensive validation logic without combining rules inside the rules themselves. + +## Tips for Writing Custom Rules + +1. Keep rules focused – one rule should check one condition. +2. Provide meaningful error messages – users should understand why input is invalid. +3. Unit test your rules – write tests to ensure correct behavior under different input scenarios. +4. Reuse rules – if a rule is useful in multiple places, keep it generic and reusable. + +## Next Steps + +Experiment by creating rules for emails, passwords, usernames, or any custom format. diff --git a/Sources/ValidatorCore/Validator.docc/Articles/installation.md b/Sources/ValidatorCore/Validator.docc/Articles/installation.md new file mode 100644 index 0000000..6fab289 --- /dev/null +++ b/Sources/ValidatorCore/Validator.docc/Articles/installation.md @@ -0,0 +1,19 @@ +# Installation + +A guide to installing the Validator package into your Swift project using Swift Package Manager. + +## Swift Package Manager + +Add the Validator package to your project using Swift Package Manager: + +```swift +dependencies: [ + .package(url: "https://github.com/space-code/validator.git", from: "1.2.0") +] +``` + +Or add it through Xcode: + +1. File > Add Package Dependencies +2. Enter package URL: `https://github.com/space-code/validator.git` +3. Select version requirements diff --git a/Sources/ValidatorCore/Validator.docc/Articles/quick-start.md b/Sources/ValidatorCore/Validator.docc/Articles/quick-start.md new file mode 100644 index 0000000..5ce8649 --- /dev/null +++ b/Sources/ValidatorCore/Validator.docc/Articles/quick-start.md @@ -0,0 +1,90 @@ +# Quick Start + +This guide will walk you through the basics of using ValidatorCore for input validation. By the end, you’ll know how to validate common input types like strings and numbers, handle validation results, and get started with custom rules. + +## Basic Usage + +The core of the library is the Validator class. It allows you to validate any input against a set of predefined rules. + +```swift +let validator = Validator() +``` + +Here we create an instance of Validator. This object will be responsible for running validation rules on your inputs. + +## Validating a String + +Let’s validate a username to make sure it is not empty and has a valid length. + +```swift +let username = "john" + +let result = validator.validate( + input: username, + rule: NonEmptyValidationRule(error: "Username cannot be empty") +) +``` + +Explanation: +- `input` – the value you want to validate. +- `rule` – an instance of a validation rule. In this case, ``NonEmptyValidationRule`` ensures that the string is not empty. +- `result` – the output of the validation, of type ``ValidationResult``. + +## Handling Validation Result + +You can check if the validation passed or failed: + +```swift +switch result { +case .success: + print("Validation passed ✅") +case .failure(let error): + print("Validation failed ❌: \(error.description)") +} +``` + +This lets you react to validation errors and show messages to your users. + +## Combining Rules + +ValidatorCore allows you to combine multiple rules for a single input: + +```swift +let rules: [any IValidationRule] = [ + NonEmptyValidationRule(error: "Field cannot be empty"), + LengthValidationRule(min: 3, max: 20, error: "Length must be 3-20 characters") +] + +let validator = Validator() + +let result = validator.validate(input: username, rules: rules) +``` + +Explanation: +- Each rule checks one specific condition (e.g., non-empty, length). +- The Validator applies each rule independently. +- By iterating over multiple rules, you can implement comprehensive validation without combining rules inside the rule objects themselves. +- This keeps rules focused, reusable, and simple, while Validator handles the orchestration. + +## Quick Example: Email Validation + +```swift +let email = "example@example.com" + +let result = validator.validate( + input: email, + rule: EmailValidationRule(error: "Invalid email address") +) +``` + +ValidatorCore comes with a set of common rules, like ``EmailValidationRule``, to simplify standard input checks. + +## Next Steps + +You now know how to: +- Create a Validator +- Use built-in rules to validate input +- Combine multiple rules for complex validation +- Handle validation results + +To go further, you can create your own custom rules tailored to your specific needs. diff --git a/Sources/ValidatorCore/Validator.docc/Info.plist b/Sources/ValidatorCore/Validator.docc/Info.plist new file mode 100644 index 0000000..a794f51 --- /dev/null +++ b/Sources/ValidatorCore/Validator.docc/Info.plist @@ -0,0 +1,48 @@ + + + + + CDAppleDefaultAvailability + + ValidatorCore + + + name + visionOS + version + 1.0 + + + name + watchOS + version + 9.0 + + + name + iOS + version + 16.0 + + + name + visionOS + version + 1.0 + + + name + tvOS + version + 16.0 + + + name + macOS + version + 13.0 + + + + + diff --git a/Sources/ValidatorCore/Validator.docc/Overview.md b/Sources/ValidatorCore/Validator.docc/Overview.md new file mode 100644 index 0000000..5b933d3 --- /dev/null +++ b/Sources/ValidatorCore/Validator.docc/Overview.md @@ -0,0 +1,39 @@ +# ``ValidatorCore`` + + +@Metadata { + @TechnologyRoot +} + +## Overview + +Validator is a modern, lightweight Swift framework for type-safe input validation. It provides a flexible and elegant way to validate strings, numbers, and other data types across all Apple platforms. Built on Swift's type system, ValidatorCore forms the foundation for all validation logic, while ValidatorUI integrates seamlessly with UIKit and SwiftUI. + +ValidatorCore contains all core validation rules, utilities, and mechanisms for composing rules, while remaining lightweight, extensible, and testable. + +## Topics + +### Essentials + +- ``Validator`` +- ``ValidationResult`` +- ``IValidationError`` +- ``IValidationRule`` + +### Built-in Validators + +- ``CharactersValidationRule`` +- ``CreditCardValidationRule`` +- ``EmailValidationRule`` +- ``LengthValidationRule`` +- ``NonEmptyValidationRule`` +- ``PrefixValidationRule`` +- ``RegexValidationRule`` +- ``SuffixValidationRule`` +- ``URLValidationRule`` + +### Articles + +- +- +- diff --git a/Sources/ValidatorCore/Validator.swift b/Sources/ValidatorCore/Validator.swift index 291eacc..667382f 100644 --- a/Sources/ValidatorCore/Validator.swift +++ b/Sources/ValidatorCore/Validator.swift @@ -5,19 +5,58 @@ // MARK: - Validator -public final class Validator { +/// A type-safe validator that checks input values against one or more validation rules. +/// +/// Use `Validator` to perform validation on any value that conforms to a validation rule. +/// You can validate a single rule or multiple rules at once. Validation results are returned +/// as `ValidationResult`, which indicates either `.valid` or `.invalid` with associated errors. +/// +/// Example: +/// ```swift +/// let validator = Validator() +/// let result = validator.validate( +/// input: "user@example.com", +/// rule: RegexValidationRule( +/// pattern: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", +/// error: "Invalid email" +/// ) +/// ) +/// +/// switch result { +/// case .valid: +/// print("✅ Valid input") +/// case .invalid(let errors): +/// print("❌ Validation failed: \(errors.map(\.message))") +/// } +/// ``` +public struct Validator: Sendable { // MARK: Initialization + /// Initializes a new instance of `Validator`. public init() {} } // MARK: IValidator extension Validator: IValidator { + /// Validates a single input value with one validation rule. + /// + /// - Parameters: + /// - input: The input value to validate. + /// - rule: A single validation rule conforming to `IValidationRule`. + /// + /// - Returns: A `ValidationResult` indicating `.valid` or `.invalid` with associated errors. public func validate(input: T, rule: some IValidationRule) -> ValidationResult { validate(input: input, rules: [rule]) } + /// Validates a single input value with multiple validation rules. + /// + /// - Parameters: + /// - input: The input value to validate. + /// - rules: An array of validation rules conforming to `IValidationRule`. + /// + /// - Returns: A `ValidationResult` indicating `.valid` if all rules pass, or `.invalid` with all errors. public func validate(input: T, rules: [any IValidationRule]) -> ValidationResult { let errors = rules .filter { !self.validate(input: input, rule: $0) } diff --git a/Sources/ValidatorUI/Classes/IUIValidatable.swift b/Sources/ValidatorUI/Classes/IUIValidatable.swift index ff1fad9..283bbcf 100644 --- a/Sources/ValidatorUI/Classes/IUIValidatable.swift +++ b/Sources/ValidatorUI/Classes/IUIValidatable.swift @@ -8,42 +8,60 @@ import ValidatorCore // MARK: - IUIValidatable +/// A protocol defining the contract for UI components that can be validated. +/// +/// Conforming types are expected to provide an input value and support validation +/// using one or multiple rules. This protocol is intended to be used in a UI context, +/// where validation can optionally be triggered automatically when the input changes. +/// +/// - Note: This protocol is marked with `@MainActor` to ensure all validation-related +/// operations occur on the main thread, making it safe for UI updates. +/// +/// - Associated Type: +/// - `Input`: The type of the input value to be validated. @MainActor public protocol IUIValidatable: AnyObject { associatedtype Input - /// The input value. + /// The input value that needs to be validated. var inputValue: Input { get } - /// Validates an input value. + /// Validates the input value using a single rule. /// - /// - Parameters: - /// - rule: The validation rule. + /// - Parameter rule: A validation rule conforming to `IValidationRule`. /// - /// - Returns: A validation result. + /// - Returns: The result of the validation (`ValidationResult`). func validate(rule: some IValidationRule) -> ValidationResult - /// Validates an input value. + /// Validates the input value using multiple rules. /// - /// - Parameters: - /// - rules: The validation rules array. + /// - Parameter rules: An array of validation rules. /// - /// - Returns: A validation result. + /// - Returns: The result of the validation (`ValidationResult`). func validate(rules: [any IValidationRule]) -> ValidationResult where T == Input - /// Validates an input value. + /// Optionally triggers validation when the input changes. /// - /// - Parameter isEnabled: The + /// - Parameter isEnabled: Whether automatic validation on input change is enabled. func validateOnInputChange(isEnabled: Bool) } +// MARK: - Associated Object Keys + +// Keys used for storing associated objects (validation rules and handlers) private nonisolated(unsafe) var kValidationRules: UInt8 = 0 private nonisolated(unsafe) var kValidationHandler: UInt8 = 0 +// Validator instance shared for UI validation // swiftlint:disable:next prefixed_toplevel_constant -private nonisolated(unsafe) let validator = Validator() +private let validator = Validator() public extension IUIValidatable { + /// Validates the input with a single rule and triggers the validation handler if set. + /// + /// - Parameter rule: The validation rule. + /// + /// - Returns: The validation result. @discardableResult func validate(rule: some IValidationRule) -> ValidationResult { let result = validator.validate(input: inputValue, rule: rule) @@ -51,6 +69,11 @@ public extension IUIValidatable { return result } + /// Validates the input with multiple rules and triggers the validation handler if set. + /// + /// - Parameter rules: An array of validation rules. + /// + /// - Returns: The validation result. @discardableResult func validate(rules: [any IValidationRule]) -> ValidationResult { let result = validator.validate(input: inputValue, rules: rules) @@ -58,10 +81,14 @@ public extension IUIValidatable { return result } + /// Adds a new validation rule to the associated list of rules for this UI element. + /// + /// - Parameter rule: The validation rule to add. func add(rule: some IValidationRule) { validationRules.append(rule) } + /// The array of validation rules associated with this UI element. var validationRules: [any IValidationRule] { get { (objc_getAssociatedObject(self, &kValidationRules) as? AnyObject) as? [any IValidationRule] ?? [] @@ -76,13 +103,20 @@ public extension IUIValidatable { } } + /// The handler called after validation is performed. + /// Can be used to update UI (e.g., show error messages). var validationHandler: ((ValidationResult) -> Void)? { get { objc_getAssociatedObject(self, &kValidationHandler) as? ((ValidationResult) -> Void) } set { if let newValue { - objc_setAssociatedObject(self, &kValidationHandler, newValue as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC) + objc_setAssociatedObject( + self, + &kValidationHandler, + newValue as AnyObject, + .OBJC_ASSOCIATION_RETAIN_NONATOMIC + ) } } } diff --git a/Sources/ValidatorUI/Classes/SUI/Extensions/View+Validation.swift b/Sources/ValidatorUI/Classes/SUI/Extensions/View+Validation.swift index fa57fc2..c71ef0b 100644 --- a/Sources/ValidatorUI/Classes/SUI/Extensions/View+Validation.swift +++ b/Sources/ValidatorUI/Classes/SUI/Extensions/View+Validation.swift @@ -7,7 +7,7 @@ import SwiftUI import ValidatorCore // swiftlint:disable:next prefixed_toplevel_constant -private nonisolated(unsafe) let validator = Validator() +private let validator = Validator() public extension View { /// Validate a binding item using a set of validation rules and perform an action based on diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/FormField.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/FormField.swift index d7c273b..9883b24 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/FormField.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/FormField.swift @@ -7,24 +7,30 @@ import Combine import Foundation import ValidatorCore +/// A convenience typealias for a Combine publisher that emits validation results. public typealias ValidationPublisher = AnyPublisher // MARK: - FormField +/// A property wrapper representing a single field in a form. +/// +/// Encapsulates a value, its validation rules, and the validator used for checking the value. +/// Provides automatic integration with a form via `IFormField` protocol. @propertyWrapper public final class FormField: IFormField { // MARK: Properties - /// The value to validate. + /// The value stored in the field. Wrapped with `@Published` to observe changes. @Published private var value: Value - /// The validation. + /// The validator used to apply rules to the value. private let validator: IValidator - /// The validation rules. + /// The rules applied to the value during validation. private let rules: [any IValidationRule] + /// The wrapped property value. public var wrappedValue: Value { get { value } set { value = newValue } @@ -32,6 +38,12 @@ public final class FormField: IFormField { // MARK: Initialization + /// Creates a new `FormField` with a value, validator, and rules. + /// + /// - Parameters: + /// - wrappedValue: The initial value of the field. + /// - validator: The validator instance to use (defaults to `Validator()`). + /// - rules: The array of validation rules to apply to the value. public init( wrappedValue: Value, validator: IValidator = Validator(), @@ -44,6 +56,11 @@ public final class FormField: IFormField { // MARK: IFormField + /// Validates the field using its rules and connects it to a form manager. + /// + /// - Parameter manager: The form field manager that tracks this field. + /// + /// - Returns: A `IFormValidationContainer` which exposes a publisher of validation results. public func validate(manager: some IFormFieldManager) -> any IFormValidationContainer { let subject = CurrentValueSubject(value) diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/IFormField.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/IFormField.swift index e5e6f01..b98f35e 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/IFormField.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormField/IFormField.swift @@ -8,13 +8,17 @@ import Foundation import ValidatorCore /// A type that represents a field on a form. +/// +/// Each field knows how to validate itself using a set of rules and a validator. +/// Validation is performed via a `IFormValidationContainer` which publishes results +/// that can be observed by the UI. public protocol IFormField { - /// Performs field validation. + /// Performs validation of the form field. /// - /// - Note: Create a form validation container that keeps track of the validation. + /// - Note: To integrate with a form, create a form validation container that keeps + /// track of the validation state and publishes updates. /// - /// - Parameter manager: The form field manager. - /// - /// - Returns: A validation container. + /// - Parameter manager: The form field manager that tracks all fields in a form. + /// - Returns: A validation container which exposes a publisher of validation results. func validate(manager: some IFormFieldManager) -> any IFormValidationContainer } diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/FormFieldManager.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/FormFieldManager.swift index 7e2a01b..31105cf 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/FormFieldManager.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/FormFieldManager.swift @@ -5,35 +5,55 @@ import Combine import Foundation -import ValidatorCore +// MARK: - FormFieldManager + +/// A concrete implementation of `IFormFieldManager` that manages the validation state of a form. +/// +/// Tracks all registered form fields, observes their validation results, +/// and exposes a single `isValid` property that represents the overall form validity. public final class FormFieldManager: IFormFieldManager { // MARK: Properties + /// A Boolean value indicating whether all registered fields are valid. + /// + /// Published so SwiftUI views or other observers can reactively update based on form validity. @Published public var isValid = false + /// A set of cancellables for Combine subscriptions to field validation publishers. private var cancellables = Set() + + /// The collection of validation containers for all registered form fields. private var validators: [any IFormValidationContainer] = [] // MARK: Initialization + /// Creates a new instance of `FormFieldManager`. public init() {} // MARK: IFormFieldManager + /// Appends a new field validator to the manager. + /// + /// - Parameter validator: The validation container for a specific field. + /// + /// The manager subscribes to the validator's publisher so that any changes + /// in validation results automatically trigger re-evaluation of the form's overall validity. public func append(validator: some IFormValidationContainer) { + // Subscribe to validation updates for this field validator .publisher .sink(receiveValue: { [weak self] _ in self?.validate() - } - ) + }) .store(in: &cancellables) validators.append(validator) + validate() } + /// Recalculates the overall form validity by checking all registered validators. public func validate() { // swiftlint:disable:next contains_over_filter_is_empty isValid = validators diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/IFormFieldManager.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/IFormFieldManager.swift index 4791223..42049d7 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/IFormFieldManager.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormFieldManager/IFormFieldManager.swift @@ -5,15 +5,28 @@ import Combine import Foundation -import ValidatorCore + +// MARK: - IFormFieldManager /// A type that manages the validation state of a form. +/// +/// The form field manager keeps track of all fields and their validation containers, +/// provides a centralized way to query the overall form validity, and allows UI components +/// to reactively respond to validation changes. +/// +/// Conforms to `ObservableObject` so SwiftUI views can observe changes in form validation state. public protocol IFormFieldManager: ObservableObject { - /// A Boolean value that indicates whether all fields on a form are valid or not. + /// A Boolean value indicating whether all fields in the form are valid. + /// + /// Returns `true` only if every registered field is valid. var isValid: Bool { get } /// Appends a new validator to the manager. /// - /// - Parameter validator: The validation container that encompasses required validation logic. + /// - Parameter validator: The validation container that encompasses + /// the required validation logic for a specific field. + /// + /// After appending, the manager will track this validator and include it + /// in the computation of `isValid` and any publishers that expose validation results. func append(validator: some IFormValidationContainer) } diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/FormValidationContainter.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/FormValidationContainter.swift index 039bbe2..6ec84f1 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/FormValidationContainter.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/FormValidationContainter.swift @@ -7,16 +7,34 @@ import Combine import Foundation import ValidatorCore +/// A concrete implementation of `IFormValidationContainer` for a single form field. +/// +/// Wraps a value subject, a validator, and a set of validation rules, and exposes a +/// publisher that emits validation results whenever the value changes. public struct FormValidationContainter: IFormValidationContainer { // MARK: Properties + /// The value subject for the form field. public var value: FormValidatorValueSubject + + /// The publisher that emits validation results. public let publisher: ValidationPublisher + + /// The validator used to check the field's value against its rules. public let validator: IValidator + + /// The validation rules applied to the field. public let rules: [any IValidationRule] // MARK: Initialization + /// Creates a new form validation container. + /// + /// - Parameters: + /// - value: The subject holding the field's value. + /// - publisher: The publisher that emits validation results. + /// - validator: The validator instance used to apply rules. + /// - rules: The validation rules to apply. public init( value: FormValidatorValueSubject, publisher: ValidationPublisher, @@ -31,6 +49,9 @@ public struct FormValidationContainter: IFormValidationContainer { // MARK: IFormValidationContainer + /// Validates the current value using the associated rules and validator. + /// + /// - Returns: The `ValidationResult` of the validation. public func validate() -> ValidationResult { validator.validate(input: value.value, rules: rules) } diff --git a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/IFormValidationContainer.swift b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/IFormValidationContainer.swift index 84c1e96..dde856c 100644 --- a/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/IFormValidationContainer.swift +++ b/Sources/ValidatorUI/Classes/SUI/Managers/FormField/FormValidationContainer/IFormValidationContainer.swift @@ -7,28 +7,35 @@ import Combine import Foundation import ValidatorCore +/// A typealias for a `CurrentValueSubject` used to track form field values. +/// +/// This subject publishes the current value of a form field and emits new values +/// whenever the field is updated, enabling reactive validation in SwiftUI or other UI frameworks. public typealias FormValidatorValueSubject = CurrentValueSubject // MARK: - IFormValidationContainer -/// A container for form validation logic. +/// A container that encapsulates validation logic for a single form field. +/// +/// This protocol defines the core components required to perform field validation +/// and publish validation results to observers such as UI elements or a form manager. public protocol IFormValidationContainer { associatedtype Value - /// The value subject used for form validation. + /// The subject that holds the current value of the form field and publishes changes. var value: FormValidatorValueSubject { get } - /// The publisher responsible for emitting validation events. + /// A publisher that emits validation results whenever the field is updated. var publisher: ValidationPublisher { get } - /// The validator associated with this validation container. + /// The validator used to evaluate the field's value against the provided rules. var validator: IValidator { get } - /// An array of validation rules to apply to the form field. + /// The array of validation rules to apply to the field. var rules: [any IValidationRule] { get } - /// Performs form field validation. + /// Performs validation on the current value of the field. /// - /// - Returns: The result of the validation process. + /// - Returns: The `ValidationResult` indicating whether the field is valid or contains errors. func validate() -> ValidationResult } diff --git a/Sources/ValidatorUI/Classes/SUI/ViewModifiers/FormValidationViewModifier.swift b/Sources/ValidatorUI/Classes/SUI/ViewModifiers/FormValidationViewModifier.swift index 37939d9..a615870 100644 --- a/Sources/ValidatorUI/Classes/SUI/ViewModifiers/FormValidationViewModifier.swift +++ b/Sources/ValidatorUI/Classes/SUI/ViewModifiers/FormValidationViewModifier.swift @@ -6,20 +6,32 @@ import SwiftUI import ValidatorCore +/// A SwiftUI `ViewModifier` that displays validation messages for a form. +/// +/// This modifier observes a `validationContainer` and updates the UI automatically +/// whenever validation results change. It can display a custom error view for invalid inputs. +/// +/// - Parameter ErrorView: The type of view used to display validation errors. public struct FormValidationViewModifier: ViewModifier { // MARK: Properties - /// The result of the validation. + /// The current result of the validation. + /// Updated whenever the container publishes a new validation result. @State private var validationResult: ValidationResult = .valid - /// A container for form validation logic. + /// A container that holds form validation logic and publishes validation results. private let validationContainer: any IFormValidationContainer - /// A custom parameter attribute that constructs views from closures. + /// A closure that constructs a SwiftUI view from a list of validation errors. @ViewBuilder private let content: ([any IValidationError]) -> ErrorView // MARK: Initialization + /// Initializes the modifier with a validation container and a custom error view builder. + /// + /// - Parameters: + /// - validationContainer: The container holding the validation logic for the form. + /// - content: A closure that takes an array of errors and returns a view to display them. public init( validationContainer: any IFormValidationContainer, @ViewBuilder content: @escaping ([any IValidationError]) -> ErrorView @@ -30,6 +42,10 @@ public struct FormValidationViewModifier: ViewModifier { // MARK: ViewModifier + /// Modifies the view to include validation error messages below the content. + /// + /// - Parameter content: The original view content. + /// - Returns: A view containing the original content and validation messages. public func body(content: Content) -> some View { VStack(alignment: .leading) { content diff --git a/Sources/ValidatorUI/Classes/SUI/ViewModifiers/ValidationViewModifier.swift b/Sources/ValidatorUI/Classes/SUI/ViewModifiers/ValidationViewModifier.swift index be20466..73d4774 100644 --- a/Sources/ValidatorUI/Classes/SUI/ViewModifiers/ValidationViewModifier.swift +++ b/Sources/ValidatorUI/Classes/SUI/ViewModifiers/ValidationViewModifier.swift @@ -39,24 +39,25 @@ import ValidatorCore public struct ValidationViewModifier: ViewModifier { // MARK: Properties - /// The result of the validation. + /// The current result of the validation. + /// Updated whenever the value changes or rules are applied. @State private var validationResult: ValidationResult = .valid - /// The binding item to validate. + /// The value to validate, wrapped as a `Binding` so changes are observed automatically. @Binding private var item: T - /// A custom parameter attribute that constructs views from closures. + /// A closure that constructs a SwiftUI view from a list of validation errors. @ViewBuilder private let content: ([any IValidationError]) -> ErrorView - /// The array of validation rules to apply to the item's value. + /// The array of validation rules applied to the binding value. public let rules: [any IValidationRule] /// Creates a new instance of the `ValidationViewModifier`. /// /// - Parameters: - /// - item: The binding item to validate. - /// - rules: The array of validation rules to apply to the item's value. - /// - content: A custom parameter attribute that constructs an error view from closures. + /// - item: The binding value to validate. + /// - rules: The array of validation rules to apply. + /// - content: A closure that builds a custom error view from the validation errors. public init( item: Binding, rules: [any IValidationRule], @@ -69,6 +70,11 @@ public struct ValidationViewModifier: ViewModifier { // MARK: ViewModifier + /// Modifies the view to include validation logic and error messages below the content. + /// + /// - Parameter content: The original view content. + /// + /// - Returns: A view containing the original content and validation messages. public func body(content: Content) -> some View { VStack(alignment: .leading) { content diff --git a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift index 837ce08..790b385 100644 --- a/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift +++ b/Sources/ValidatorUI/Classes/UIKit/Extensions/UITextField+Validation.swift @@ -7,10 +7,17 @@ import UIKit extension UITextField: IUIValidatable { + /// The value of the text field to validate. + /// Returns an empty string if `text` is nil. public var inputValue: String { text ?? "" } + /// The type of input for validation. public typealias Input = String + /// Enables or disables automatic validation when the text changes. + /// + /// - Parameter isEnabled: If true, validation is triggered on every text change. + /// If false, the target-action is removed. public func validateOnInputChange(isEnabled: Bool) { if isEnabled { addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged) @@ -21,6 +28,10 @@ // MARK: Private + /// Called automatically when the text field changes (if `validateOnInputChange` is enabled). + /// Performs validation using all rules stored in `validationRules`. + /// + /// - Parameter textField: The text field triggering the action. @objc private func textFieldDidChange(_: UITextField) { validate(rules: validationRules) diff --git a/Sources/ValidatorUI/ValidatorUI.docc/Articles/quick-start.md b/Sources/ValidatorUI/ValidatorUI.docc/Articles/quick-start.md new file mode 100644 index 0000000..604fa9d --- /dev/null +++ b/Sources/ValidatorUI/ValidatorUI.docc/Articles/quick-start.md @@ -0,0 +1,208 @@ +# Quick Start + +This guide will help you quickly integrate ValidatorUI into your iOS projects. ValidatorUI is a lightweight and flexible framework designed to provide type-safe input validation for both UIKit and SwiftUI components. It supports single field validation, multiple rules, real-time validation, and full form validation, making it easy to maintain clean, reliable, and user-friendly forms. + +In this guide, you will learn how to: +- Add validation to a single field in UIKit and SwiftUI. +- Enable real-time validation for user input. +- Manage multiple fields in a form. +- Display validation errors with custom UI. + +By following this guide, you will have a fully functional validation system up and running in your app in just a few minutes. + +## UIKit Integration + +UIKit integration allows you to add validation directly to standard UI components such as UITextField. Validation rules can be applied individually, and you can also listen for validation results to update the UI dynamically. + +```swift +import UIKit +import ValidatorUI +import ValidatorCore + +class ViewController: UIViewController { + let emailField = UITextField() + + override func viewDidLoad() { + super.viewDidLoad() + + // Add validation rules + emailField.add( + rule: RegexValidationRule( + pattern: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", + error: "Please enter a valid email" + ) + ) + + // Enable real-time validation + emailField.validateOnInputChange(isEnabled: true) + + // Handle validation results + emailField.validationHandler = { result in + switch result { + case .valid: + self.updateUI(isValid: true) + case .invalid(let errors): + self.showErrors(errors) + } + } + } +} +``` + +Tips for UIKit: +- You can attach multiple rules to a single field; all rules will be evaluated when validating. +- `.validateOnInputChange(isEnabled:)` allows instant feedback to users without manually triggering validation. +- The `validationHandler` closure lets you update UI elements like borders, labels, or icons based on validation results. + +## SwiftUI Integration + +SwiftUI provides a more declarative approach. You can attach validation rules directly to your state variables and update the view automatically based on validation results. + +### Single Field Validation + +Use the `.validation()` modifier for simple field validation. This approach is perfect for short forms or single input fields. + +```swift +import SwiftUI +import ValidatorUI +import ValidatorCore + +struct LoginView: View { + @State private var email = "" + + var body: some View { + TextField("Email", text: $email) + .validation($email, rules: [ + RegexValidationRule( + pattern: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", + error: "Invalid email" + ) + ]) { result in + if case .invalid(let errors) = result { + print("Validation errors: \(errors)") + } + } + } +} +``` + +For more control over error display, you can use `.validate()` with a custom view: + +```swift +struct LoginView: View { + @State private var password = "" + + var body: some View { + VStack(alignment: .leading) { + SecureField("Password", text: $password) + .validate(item: $password, rules: [ + LengthValidationRule(min: 8, error: "Too short") + ]) { errors in + ForEach(errors, id: \.message) { error in + Text(error.message) + .foregroundColor(.red) + .font(.caption) + } + } + } + } +} +``` + +Tips for SwiftUI: +- You can chain multiple validation rules for a single field. +- Validation automatically updates the view whenever the bound state changes. +- Use custom error views to match your app’s design system. + +### Form Validation + +For larger forms with multiple fields, ``FormFieldManager`` helps manage all validations in one place. It tracks the state of each field and allows you to check overall form validity. + +```swift +import Combine +import SwiftUI +import ValidatorUI +import ValidatorCore + +class RegistrationForm: ObservableObject { + @Published var manager = FormFieldManager() + + @FormField(rules: [ + LengthValidationRule(min: 2, max: 50, error: "Invalid name length") + ]) + var firstName = "" + + @FormField(rules: [ + LengthValidationRule(min: 2, max: 50, error: "Invalid name length") + ]) + var lastName = "" + + @FormField(rules: [ + RegexValidationRule( + pattern: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,}", + error: "Invalid email" + ) + ]) + var email = "" + + lazy var firstNameContainer = _firstName.validate(manager: manager) + lazy var lastNameContainer = _lastName.validate(manager: manager) + lazy var emailContainer = _email.validate(manager: manager) +} + +struct RegistrationView: View { + @StateObject private var form = RegistrationForm() + @State private var isFormValid = false + + var body: some View { + Form { + Section("Personal Information") { + TextField("First Name", text: $form.firstName) + .validate(validationContainer: form.firstNameContainer) { errors in + ErrorView(errors: errors) + } + + TextField("Last Name", text: $form.lastName) + .validate(validationContainer: form.lastNameContainer) { errors in + ErrorView(errors: errors) + } + } + + Section("Contact") { + TextField("Email", text: $form.email) + .validate(validationContainer: form.emailContainer) { errors in + ErrorView(errors: errors) + } + } + + Section { + Button("Submit") { + form.manager.validate() + } + .disabled(!isFormValid) + } + } + .onReceive(form.manager.$isValid) { newValue in + isFormValid = newValue + } + } + + private func submitForm() { + print("✅ Form is valid, submitting...") + } +} +``` + +Form Validation Notes: +- Use ``FormField`` property wrapper to declare each field with its rules. +- ``FormFieldManager`` tracks all fields and exposes $isValid for reactive UI updates. +- Validation containers allow you to render errors inline or in custom views. +- Submitting the form should trigger `manager.validate()` to ensure all fields are checked. + +## Summary + +- UIKit: Add rules directly to fields, enable real-time validation, and handle results in closures. +- SwiftUI: Use `.validation()` or `.validate()` modifiers for reactive validation. +- Forms: ``FormFieldManager`` allows managing multiple fields, tracking validity, and handling submissions. + +ValidatorUI makes it easy to maintain consistent validation across your app while keeping the code clean, type-safe, and UI-friendly. diff --git a/Sources/ValidatorUI/ValidatorUI.docc/Info.plist b/Sources/ValidatorUI/ValidatorUI.docc/Info.plist new file mode 100644 index 0000000..fcb8736 --- /dev/null +++ b/Sources/ValidatorUI/ValidatorUI.docc/Info.plist @@ -0,0 +1,48 @@ + + + + + CDAppleDefaultAvailability + + ValidatorUI + + + name + visionOS + version + 1.0 + + + name + watchOS + version + 9.0 + + + name + iOS + version + 16.0 + + + name + visionOS + version + 1.0 + + + name + tvOS + version + 16.0 + + + name + macOS + version + 13.0 + + + + + diff --git a/Sources/ValidatorUI/ValidatorUI.docc/Overview.md b/Sources/ValidatorUI/ValidatorUI.docc/Overview.md new file mode 100644 index 0000000..61c49c2 --- /dev/null +++ b/Sources/ValidatorUI/ValidatorUI.docc/Overview.md @@ -0,0 +1,36 @@ +# ``ValidatorUI`` + +@Metadata { + @TechnologyRoot +} + +## Overview + +ValidatorUI is a Swift framework that builds on top of `ValidatorCore` to provide **seamless integration with user interfaces**. It simplifies input validation in **UIKit** and **SwiftUI** by connecting form fields directly to validation logic and automatically displaying validation errors. + +With ValidatorUI, you can: + +- Bind validation rules to UI components like `UITextField` or SwiftUI `TextField`. +- Observe validation results reactively and update the UI. +- Display validation messages in a flexible, customizable way. +- Compose complex forms with multiple fields and centralized validation management. + +ValidatorUI is designed to make form validation **safe, declarative, and easy to maintain**, while leveraging the powerful core validation logic from ValidatorCore. + +## Topics + +### Essentials + +- ``IUIValidatable`` +- ``ValidationViewModifier`` +- ``FormValidationViewModifier`` +- ``IFormField`` +- ``FormField`` +- ``IFormFieldManager`` +- ``FormFieldManager`` +- ``IFormValidationContainer`` +- ``FormValidationContainter`` + +### Articles + +-