diff --git a/README.md b/README.md index a3ce807..fdf7460 100644 --- a/README.md +++ b/README.md @@ -317,6 +317,7 @@ struct RegistrationView: View { | `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")` | `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))` | `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")` +| `DependentValidationRule` | Validates another field's value to determine which rule to apply | ``DependentValidationRule(dependsOn: countryField, error: "Invalid", ruleProvider: { $0 == "US" ? USPhoneRule() : InternationalPhoneRule() })`` ## Custom Validators diff --git a/Sources/ValidatorCore/Classes/Rules/DependentValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/DependentValidationRule.swift new file mode 100644 index 0000000..75fc032 --- /dev/null +++ b/Sources/ValidatorCore/Classes/Rules/DependentValidationRule.swift @@ -0,0 +1,65 @@ +// +// Validator +// Copyright © 2025 Space Code. All rights reserved. +// + +/// A validation rule that depends on another field's value to determine which rule to apply. +/// +/// # Example: +/// ```swift +/// let countryField = FormField(value: "US") +/// let phoneField = FormField( +/// value: "", +/// rules: [ +/// DependentValidationRule( +/// dependsOn: countryField, +/// error: "Invalid phone number", +/// ruleProvider: { country in +/// if country == "US" { +/// return USPhoneRule() +/// } else { +/// return InternationalPhoneRule() +/// } +/// } +/// ) +/// ] +/// ) +/// ``` +public struct DependentValidationRule: IValidationRule { + // MARK: Properties + + /// The error message or error object to return if validation fails. + public let error: IValidationError + + /// A closure that returns the value of the field this rule depends on. + private let dependentField: () -> DependentInput + + /// A closure that takes the dependent field's value and returns the appropriate validation rule. + private let ruleProvider: (DependentInput) -> any IValidationRule + + // MARK: Initialization + + /// Creates a dependent validation rule. + /// + /// - Parameters: + /// - dependsOn: A closure that returns the value of the field this rule depends on. + /// - error: The error message or error object to return if validation fails. + /// - ruleProvider: A closure that takes the dependent field's value and returns the appropriate validation rule. + public init( + dependsOn: @escaping @autoclosure () -> DependentInput, + error: IValidationError, + ruleProvider: @escaping (DependentInput) -> any IValidationRule + ) { + dependentField = dependsOn + self.error = error + self.ruleProvider = ruleProvider + } + + // MARK: IValidationRule + + public func validate(input: Input) -> Bool { + let dependentValue = dependentField() + let rule = ruleProvider(dependentValue) + return rule.validate(input: input) + } +} diff --git a/Sources/ValidatorCore/Validator.docc/Overview.md b/Sources/ValidatorCore/Validator.docc/Overview.md index 410e44d..6abb8f5 100644 --- a/Sources/ValidatorCore/Validator.docc/Overview.md +++ b/Sources/ValidatorCore/Validator.docc/Overview.md @@ -40,6 +40,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for - ``IBANValidationRule`` - ``IPAddressValidationRule`` - ``PostalCodeValidationRule`` +- ``DependentValidationRule`` ### Articles diff --git a/Tests/ValidatorCoreTests/UnitTests/Rules/DependentValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/DependentValidationRuleTests.swift new file mode 100644 index 0000000..9bfe48b --- /dev/null +++ b/Tests/ValidatorCoreTests/UnitTests/Rules/DependentValidationRuleTests.swift @@ -0,0 +1,118 @@ +// +// Validator +// Copyright © 2025 Space Code. All rights reserved. +// + +@testable import ValidatorCore +import XCTest + +final class DependentValidationRuleTests: XCTestCase { + // MARK: - Mock Validation Rules + + struct AlwaysPassRule: IValidationRule { + let error: IValidationError = "Should not fail" + func validate(input _: T) -> Bool { true } + } + + struct AlwaysFailRule: IValidationRule { + let error: IValidationError = "Failed" + func validate(input _: T) -> Bool { false } + } + + // MARK: - Tests + + func test_dependentValidationRuleUsesCorrectValidationBasedOnDependentValue() { + // given + let country = "US" + + let rule = DependentValidationRule( + dependsOn: country, + error: "Invalid", + ruleProvider: { value in + if value == "US" { + AlwaysPassRule() + } else { + AlwaysFailRule() + } + } + ) + + // when + let isValid = rule.validate(input: "12345") + + // then + XCTAssertTrue(isValid) + } + + func test_dependentValidationRuleSwitchesBehaviorWhenDependentValueChanges() { + // given + var country = "US" + + let rule = DependentValidationRule( + dependsOn: country, + error: "Invalid", + ruleProvider: { value in + if value == "US" { + AlwaysPassRule() + } else { + AlwaysFailRule() + } + } + ) + + // then + XCTAssertTrue(rule.validate(input: "value")) + + country = "FR" + + XCTAssertFalse(rule.validate(input: "value")) + } + + func test_validationFails_whenInnerRuleFails() { + // given + let rule = DependentValidationRule( + dependsOn: "ANY", + error: "Invalid", + ruleProvider: { _ in AlwaysFailRule() } + ) + + // when + let result = rule.validate(input: "test") + + // then + XCTAssertFalse(result) + } + + func test_validationPasses_whenInnerRulePasses() { + // given + let rule = DependentValidationRule( + dependsOn: "ANY", + error: "Invalid", + ruleProvider: { _ in AlwaysPassRule() } + ) + + // when + let result = rule.validate(input: "test") + + // then + XCTAssertTrue(result) + } + + func test_canWorkWithDifferentInputTypes() { + // given + let rule = DependentValidationRule( + dependsOn: true, + error: "Invalid", + ruleProvider: { condition in + if condition { + AlwaysPassRule() + } else { + AlwaysFailRule() + } + } + ) + + // then + XCTAssertTrue(rule.validate(input: 123)) + } +}