diff --git a/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme b/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
index ec134cc..ea021af 100644
--- a/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
+++ b/.swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
@@ -65,13 +65,6 @@
BlueprintName = "ValidatorCore"
ReferencedContainer = "container:">
-
-
+
+
+
+
+
+
+
+
AnyValidationRule {
- AnyValidationRule(self)
- }
-}
diff --git a/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift
deleted file mode 100644
index fb6c60d..0000000
--- a/Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift
+++ /dev/null
@@ -1,27 +0,0 @@
-//
-// Validator
-// Copyright © 2023 Space Code. All rights reserved.
-//
-
-import Foundation
-
-public struct AnyValidationRule: IValidationRule {
- // MARK: Properties
-
- private let validationClosure: (Input) -> Bool
-
- public var error: IValidationError
-
- // MARK: Initialization
-
- public init(_ rule: Rule) where Rule.Input == Input {
- validationClosure = rule.validate
- error = rule.error
- }
-
- // MARK: IValidationRule
-
- public func validate(input: Input) -> Bool {
- validationClosure(input)
- }
-}
diff --git a/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift
index d59f20c..3974896 100644
--- a/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift
+++ b/Sources/ValidatorCore/Classes/Rules/LengthValidationRule.swift
@@ -24,7 +24,7 @@ public struct LengthValidationRule: IValidationRule {
// MARK: Initialization
- public init(min: Int, max: Int, error: IValidationError) {
+ public init(min: Int = .zero, max: Int = .max, error: IValidationError) {
self.min = min
self.max = max
self.error = error
diff --git a/Sources/ValidatorCore/IValidator.swift b/Sources/ValidatorCore/IValidator.swift
index c516259..2f06e09 100644
--- a/Sources/ValidatorCore/IValidator.swift
+++ b/Sources/ValidatorCore/IValidator.swift
@@ -15,25 +15,6 @@ public protocol IValidator {
/// - rule: The validation rule.
///
/// - Returns: A validation result.
- func validate(input: T.Input, rule: T) -> ValidationResult
-
- /// Validates an input value.
- ///
- /// - Parameters:
- /// - input: The input value.
- /// - rules: The validation rules array.
- ///
- /// - Returns: A validation result.
- func validate(input: T, rules: [AnyValidationRule]) -> ValidationResult
-
- /// Validates an input value.
- ///
- /// - Parameters:
- /// - input: The input value.
- /// - rule: The validation rule.
- ///
- /// - Returns: A validation result.
- @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
func validate(input: T, rule: some IValidationRule) -> ValidationResult
/// Validates an input value.
@@ -43,6 +24,5 @@ public protocol IValidator {
/// - rules: The validation rules array.
///
/// - Returns: A validation result.
- @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
func validate(input: T, rules: [any IValidationRule]) -> ValidationResult
}
diff --git a/Sources/ValidatorCore/Validator.swift b/Sources/ValidatorCore/Validator.swift
index fb1afa5..291eacc 100644
--- a/Sources/ValidatorCore/Validator.swift
+++ b/Sources/ValidatorCore/Validator.swift
@@ -14,19 +14,10 @@ public final class Validator {
// MARK: IValidator
extension Validator: IValidator {
- public func validate(input: T.Input, rule: T) -> ValidationResult where T: IValidationRule {
- validate(input: input, rules: [rule.eraseToAnyValidationRule()])
+ public func validate(input: T, rule: some IValidationRule) -> ValidationResult {
+ validate(input: input, rules: [rule])
}
- public func validate(input: T, rules: [AnyValidationRule]) -> ValidationResult {
- let errors = rules
- .filter { !$0.validate(input: input) }
- .map(\.error)
-
- return errors.isEmpty ? .valid : ValidationResult.invalid(errors: errors)
- }
-
- @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
public func validate(input: T, rules: [any IValidationRule]) -> ValidationResult {
let errors = rules
.filter { !self.validate(input: input, rule: $0) }
@@ -35,8 +26,7 @@ extension Validator: IValidator {
return errors.isEmpty ? .valid : ValidationResult.invalid(errors: errors)
}
- @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
- func validate(input: T, rule: some IValidationRule) -> Bool {
+ private func validate(input: T, rule: some IValidationRule) -> Bool {
rule.validate(input: input)
}
}
diff --git a/Sources/ValidatorUI/Classes/Extensions/UITextField+Validation.swift b/Sources/ValidatorUI/Classes/Extensions/UITextField+Validation.swift
new file mode 100644
index 0000000..837ce08
--- /dev/null
+++ b/Sources/ValidatorUI/Classes/Extensions/UITextField+Validation.swift
@@ -0,0 +1,29 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+#if os(iOS)
+ import UIKit
+
+ extension UITextField: IUIValidatable {
+ public var inputValue: String { text ?? "" }
+
+ public typealias Input = String
+
+ public func validateOnInputChange(isEnabled: Bool) {
+ if isEnabled {
+ addTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
+ } else {
+ removeTarget(self, action: #selector(textFieldDidChange(_:)), for: .editingChanged)
+ }
+ }
+
+ // MARK: Private
+
+ @objc
+ private func textFieldDidChange(_: UITextField) {
+ validate(rules: validationRules)
+ }
+ }
+#endif
diff --git a/Sources/ValidatorUI/Classes/IUIValidatable.swift b/Sources/ValidatorUI/Classes/IUIValidatable.swift
new file mode 100644
index 0000000..c7cb980
--- /dev/null
+++ b/Sources/ValidatorUI/Classes/IUIValidatable.swift
@@ -0,0 +1,89 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+// swiftlint:disable prefixed_toplevel_constant
+
+import Foundation
+import ValidatorCore
+
+// MARK: - IUIValidatable
+
+public protocol IUIValidatable: AnyObject {
+ associatedtype Input
+
+ /// The input value.
+ var inputValue: Input { get }
+
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - rule: The validation rule.
+ ///
+ /// - Returns: A validation result.
+ func validate(rule: some IValidationRule) -> ValidationResult where T == Input
+
+ /// Validates an input value.
+ ///
+ /// - Parameters:
+ /// - rules: The validation rules array.
+ ///
+ /// - Returns: A validation result.
+ func validate(rules: [any IValidationRule]) -> ValidationResult where T == Input
+
+ /// Validates an input value.
+ ///
+ /// - Parameter isEnabled: The
+ func validateOnInputChange(isEnabled: Bool)
+}
+
+private var kValidationRules: UInt8 = 0
+private var kValidationHandler: UInt8 = 0
+
+private let validator = Validator()
+
+public extension IUIValidatable {
+ @discardableResult
+ func validate(rule: some IValidationRule) -> ValidationResult where T == Input {
+ let result = validator.validate(input: inputValue, rule: rule)
+ validationHandler?(result)
+ return result
+ }
+
+ @discardableResult
+ func validate(rules: [any IValidationRule]) -> ValidationResult where T == Input {
+ let result = validator.validate(input: inputValue, rules: rules)
+ validationHandler?(result)
+ return result
+ }
+
+ func add(rule: some IValidationRule) where T == Input {
+ validationRules.append(rule)
+ }
+
+ var validationRules: [any IValidationRule] {
+ get {
+ (objc_getAssociatedObject(self, &kValidationRules) as? AnyObject) as? [any IValidationRule] ?? []
+ }
+ set {
+ objc_setAssociatedObject(
+ self,
+ &kValidationRules,
+ newValue as [any IValidationRule],
+ .OBJC_ASSOCIATION_RETAIN_NONATOMIC
+ )
+ }
+ }
+
+ var validationHandler: ((ValidationResult) -> Void)? {
+ get {
+ objc_getAssociatedObject(self, &kValidationHandler) as? ((ValidationResult) -> Void)
+ }
+ set {
+ if let newValue = newValue {
+ objc_setAssociatedObject(self, &kValidationHandler, newValue as AnyObject, .OBJC_ASSOCIATION_RETAIN_NONATOMIC)
+ }
+ }
+ }
+}
diff --git a/Sources/ValidatorUI/ValidatorUI.swift b/Sources/ValidatorUI/ValidatorUI.swift
deleted file mode 100644
index 63c2173..0000000
--- a/Sources/ValidatorUI/ValidatorUI.swift
+++ /dev/null
@@ -1,12 +0,0 @@
-//
-// Validator
-// Copyright © 2023 Space Code. All rights reserved.
-//
-
-import Foundation
-
-public struct ValidatorUI {
- public private(set) var text = "Hello, World!"
-
- public init() {}
-}
diff --git a/Tests/ValidatorTests/UnitTests/Rules/LengthValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/LengthValidationRuleTests.swift
similarity index 100%
rename from Tests/ValidatorTests/UnitTests/Rules/LengthValidationRuleTests.swift
rename to Tests/ValidatorCoreTests/UnitTests/Rules/LengthValidationRuleTests.swift
diff --git a/Tests/ValidatorTests/UnitTests/Rules/RegexValidationRuleTests.swift b/Tests/ValidatorCoreTests/UnitTests/Rules/RegexValidationRuleTests.swift
similarity index 100%
rename from Tests/ValidatorTests/UnitTests/Rules/RegexValidationRuleTests.swift
rename to Tests/ValidatorCoreTests/UnitTests/Rules/RegexValidationRuleTests.swift
diff --git a/Tests/ValidatorTests/UnitTests/Validator/ValidationResultTests.swift b/Tests/ValidatorCoreTests/UnitTests/Validator/ValidationResultTests.swift
similarity index 100%
rename from Tests/ValidatorTests/UnitTests/Validator/ValidationResultTests.swift
rename to Tests/ValidatorCoreTests/UnitTests/Validator/ValidationResultTests.swift
diff --git a/Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift b/Tests/ValidatorCoreTests/UnitTests/Validator/ValidatorTests.swift
similarity index 75%
rename from Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift
rename to Tests/ValidatorCoreTests/UnitTests/Validator/ValidatorTests.swift
index 55b8e70..0cce545 100644
--- a/Tests/ValidatorTests/UnitTests/Validator/ValidatorTests.swift
+++ b/Tests/ValidatorCoreTests/UnitTests/Validator/ValidatorTests.swift
@@ -29,7 +29,7 @@ final class ValidatorTests: XCTestCase {
func test_thatValidatorValidatesInput_whenLengthIsLessThanTenCharacters() {
// given
- let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error)
// when
let validationResult = validator.validate(input: String(String.text.prefix(.max)), rule: validationRule)
@@ -40,7 +40,7 @@ final class ValidatorTests: XCTestCase {
func test_thatValidatorValidatesInput_whenLengthIsGreaterThanTenCharacters() {
// given
- let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error)
// when
let validationResult = validator.validate(input: String(String.text.prefix(11)), rules: [validationRule])
@@ -56,7 +56,7 @@ final class ValidatorTests: XCTestCase {
func test_thatValidatorValidatesInput_whenLengthIsLessThanOneCharacter() {
// given
- let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule()
+ let validationRule = LengthValidationRule(min: .min, max: .max, error: String.error)
// when
let validationResult = validator.validate(input: .empty, rules: [validationRule])
@@ -70,27 +70,6 @@ final class ValidatorTests: XCTestCase {
}
}
- func test_thatValidatorValidatesInput_whenThereAreCoupleOfRules() {
- // given
- let rules: [AnyValidationRule] = [
- RegexValidationRule(pattern: .pattern, error: String.error).eraseToAnyValidationRule(),
- LengthValidationRule(min: .min, max: .max, error: String.error).eraseToAnyValidationRule(),
- ]
-
- // when
- let validationResult = validator.validate(input: .text, rules: rules)
-
- // then
- if case let .invalid(errors) = validationResult {
- XCTAssertEqual(errors.count, 2)
- XCTAssertEqual(errors[0].message, .error)
- XCTAssertEqual(errors[1].message, .error)
- } else {
- XCTFail("The input string must be empty")
- }
- }
-
- @available(macOS 13.0, iOS 16, tvOS 16, watchOS 9, *)
func test_thatValidatorValidatesInputWithAnyRules_whenThereAreCoupleOfRules() {
// given
let rules: [any IValidationRule] = [
diff --git a/Tests/ValidatorUITests/UnitTests/UITextFieldTests.swift b/Tests/ValidatorUITests/UnitTests/UITextFieldTests.swift
new file mode 100644
index 0000000..9eefe9c
--- /dev/null
+++ b/Tests/ValidatorUITests/UnitTests/UITextFieldTests.swift
@@ -0,0 +1,82 @@
+//
+// Validator
+// Copyright © 2023 Space Code. All rights reserved.
+//
+
+import ValidatorCore
+import ValidatorUI
+import XCTest
+
+#if canImport(UIKit)
+ import UIKit
+#endif
+
+#if os(iOS)
+ final class UITextFieldTests: XCTestCase {
+ // MARK: Properties
+
+ private var textField: UITextField!
+
+ // MARK: XCTestCase
+
+ override func setUp() {
+ super.setUp()
+ textField = UITextField()
+ }
+
+ override func tearDown() {
+ textField = nil
+ super.tearDown()
+ }
+
+ // MARK: Tests
+
+ func test_thatTextFieldValidationReturnsValid_whenInputValueIsValid() {
+ // given
+ textField.validateOnInputChange(isEnabled: true)
+ textField.add(rule: LengthValidationRule(max: .max, error: String.error))
+
+ // when
+ textField.text = String(String.text.prefix(.max))
+
+ var result: ValidationResult?
+
+ textField.validationHandler = { result = $0 }
+ textField.validate(rules: textField.validationRules)
+
+ // when
+ if case .valid = result {}
+ else { XCTFail("The result must be equal to the valid value") }
+ }
+
+ func test_thatTextFieldValidationReturnsInvalid_whenInputValueIsInvalid() {
+ // given
+ textField.validateOnInputChange(isEnabled: true)
+ textField.add(rule: LengthValidationRule(max: .max, error: String.error))
+
+ // when
+ textField.text = .text
+
+ var result: ValidationResult?
+
+ textField.validationHandler = { result = $0 }
+ textField.validate(rules: textField.validationRules)
+
+ // when
+ if case let .invalid(errors) = result {
+ XCTAssertEqual(errors.count, 1)
+ XCTAssertEqual(errors.first?.message, .error)
+ } else { XCTFail("The result must be equal to the invalid value") }
+ }
+ }
+#endif
+
+private extension String {
+ static let text: String = "lorem ipsum lorem ipsum lorem ipsum"
+ static let error: String = "error"
+}
+
+private extension Int {
+ static let min = 0
+ static let max = 10
+}