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 +}