Skip to content

Commit 935de69

Browse files
committed
feat: add dependent validation rule
1 parent d4df5bd commit 935de69

File tree

4 files changed

+185
-0
lines changed

4 files changed

+185
-0
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,7 @@ struct RegistrationView: View {
317317
| `IBANValidationRule` | Validates that a string is a valid IBAN (International Bank Account Number) | `IBANValidationRule(error: "Invalid IBAN")`
318318
| `IPAddressValidationRule` | Validates that a string is a valid IPv4 or IPv6 address | `IPAddressValidationRule(version: .v4, error: ValidationError("Invalid IPv4"))`
319319
| `PostalCodeValidationRule` | Validates postal/ZIP codes for different countries | `PostalCodeValidationRule(country: .uk, error: "Invalid post code")`
320+
| `DependentValidationRule` | Validates another field's value to determine which rule to apply | ``DependentValidationRule<String, String>(dependsOn: countryField, error: "Invalid", ruleProvider: { $0 == "US" ? USPhoneRule() : InternationalPhoneRule() })``
320321

321322
## Custom Validators
322323

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
/// A validation rule that depends on another field's value to determine which rule to apply.
7+
///
8+
/// # Example:
9+
/// ```swift
10+
/// let countryField = FormField(value: "US")
11+
/// let phoneField = FormField(
12+
/// value: "",
13+
/// rules: [
14+
/// DependentValidationRule(
15+
/// dependsOn: countryField,
16+
/// error: "Invalid phone number",
17+
/// ruleProvider: { country in
18+
/// if country == "US" {
19+
/// return USPhoneRule()
20+
/// } else {
21+
/// return InternationalPhoneRule()
22+
/// }
23+
/// }
24+
/// )
25+
/// ]
26+
/// )
27+
/// ```
28+
public struct DependentValidationRule<DependentInput, Input>: IValidationRule {
29+
// MARK: Properties
30+
31+
/// The error message or error object to return if validation fails.
32+
public let error: IValidationError
33+
34+
/// A closure that returns the value of the field this rule depends on.
35+
private let dependentField: () -> DependentInput
36+
37+
/// A closure that takes the dependent field's value and returns the appropriate validation rule.
38+
private let ruleProvider: (DependentInput) -> any IValidationRule<Input>
39+
40+
// MARK: Initialization
41+
42+
/// Creates a dependent validation rule.
43+
///
44+
/// - Parameters:
45+
/// - dependsOn: A closure that returns the value of the field this rule depends on.
46+
/// - error: The error message or error object to return if validation fails.
47+
/// - ruleProvider: A closure that takes the dependent field's value and returns the appropriate validation rule.
48+
public init(
49+
dependsOn: @escaping @autoclosure () -> DependentInput,
50+
error: IValidationError,
51+
ruleProvider: @escaping (DependentInput) -> any IValidationRule<Input>
52+
) {
53+
dependentField = dependsOn
54+
self.error = error
55+
self.ruleProvider = ruleProvider
56+
}
57+
58+
// MARK: IValidationRule
59+
60+
public func validate(input: Input) -> Bool {
61+
let dependentValue = dependentField()
62+
let rule = ruleProvider(dependentValue)
63+
return rule.validate(input: input)
64+
}
65+
}

Sources/ValidatorCore/Validator.docc/Overview.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ ValidatorCore contains all core validation rules, utilities, and mechanisms for
4040
- ``IBANValidationRule``
4141
- ``IPAddressValidationRule``
4242
- ``PostalCodeValidationRule``
43+
- ``DependentValidationRule``
4344

4445
### Articles
4546

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
//
2+
// Validator
3+
// Copyright © 2025 Space Code. All rights reserved.
4+
//
5+
6+
@testable import ValidatorCore
7+
import XCTest
8+
9+
final class DependentValidationRuleTests: XCTestCase {
10+
// MARK: - Mock Validation Rules
11+
12+
struct AlwaysPassRule<T>: IValidationRule {
13+
let error: IValidationError = "Should not fail"
14+
func validate(input _: T) -> Bool { true }
15+
}
16+
17+
struct AlwaysFailRule<T>: IValidationRule {
18+
let error: IValidationError = "Failed"
19+
func validate(input _: T) -> Bool { false }
20+
}
21+
22+
// MARK: - Tests
23+
24+
func test_dependentValidationRuleUsesCorrectValidationBasedOnDependentValue() {
25+
// given
26+
let country = "US"
27+
28+
let rule = DependentValidationRule<String, String>(
29+
dependsOn: country,
30+
error: "Invalid",
31+
ruleProvider: { value in
32+
if value == "US" {
33+
AlwaysPassRule<String>()
34+
} else {
35+
AlwaysFailRule<String>()
36+
}
37+
}
38+
)
39+
40+
// when
41+
let isValid = rule.validate(input: "12345")
42+
43+
// then
44+
XCTAssertTrue(isValid)
45+
}
46+
47+
func test_dependentValidationRuleSwitchesBehaviorWhenDependentValueChanges() {
48+
// given
49+
var country = "US"
50+
51+
let rule = DependentValidationRule(
52+
dependsOn: country,
53+
error: "Invalid",
54+
ruleProvider: { value in
55+
if value == "US" {
56+
AlwaysPassRule<String>()
57+
} else {
58+
AlwaysFailRule<String>()
59+
}
60+
}
61+
)
62+
63+
// then
64+
XCTAssertTrue(rule.validate(input: "value"))
65+
66+
country = "FR"
67+
68+
XCTAssertFalse(rule.validate(input: "value"))
69+
}
70+
71+
func test_validationFails_whenInnerRuleFails() {
72+
// given
73+
let rule = DependentValidationRule(
74+
dependsOn: "ANY",
75+
error: "Invalid",
76+
ruleProvider: { _ in AlwaysFailRule<String>() }
77+
)
78+
79+
// when
80+
let result = rule.validate(input: "test")
81+
82+
// then
83+
XCTAssertFalse(result)
84+
}
85+
86+
func test_validationPasses_whenInnerRulePasses() {
87+
// given
88+
let rule = DependentValidationRule(
89+
dependsOn: "ANY",
90+
error: "Invalid",
91+
ruleProvider: { _ in AlwaysPassRule<String>() }
92+
)
93+
94+
// when
95+
let result = rule.validate(input: "test")
96+
97+
// then
98+
XCTAssertTrue(result)
99+
}
100+
101+
func test_canWorkWithDifferentInputTypes() {
102+
// given
103+
let rule = DependentValidationRule(
104+
dependsOn: true,
105+
error: "Invalid",
106+
ruleProvider: { condition in
107+
if condition {
108+
AlwaysPassRule<Int>()
109+
} else {
110+
AlwaysFailRule<Int>()
111+
}
112+
}
113+
)
114+
115+
// then
116+
XCTAssertTrue(rule.validate(input: 123))
117+
}
118+
}

0 commit comments

Comments
 (0)