Skip to content

Commit

Permalink
Implement UIValidator package
Browse files Browse the repository at this point in the history
- Remove `AnyValidationRule`
- Get rid of support for older operating systems
  • Loading branch information
ns-vasilev committed Sep 22, 2023
1 parent 38287fa commit bb9a3ba
Show file tree
Hide file tree
Showing 15 changed files with 234 additions and 122 deletions.
27 changes: 20 additions & 7 deletions .swiftpm/xcode/xcshareddata/xcschemes/Validator-Package.xcscheme
Original file line number Diff line number Diff line change
Expand Up @@ -65,13 +65,6 @@
BlueprintName = "ValidatorCore"
ReferencedContainer = "container:">
</BuildableReference>
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ValidatorUI"
BuildableName = "ValidatorUI"
BlueprintName = "ValidatorUI"
ReferencedContainer = "container:">
</BuildableReference>
</CodeCoverageTargets>
<Testables>
<TestableReference
Expand All @@ -84,6 +77,26 @@
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ValidatorCoreTests"
BuildableName = "ValidatorCoreTests"
BlueprintName = "ValidatorCoreTests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
<TestableReference
skipped = "NO">
<BuildableReference
BuildableIdentifier = "primary"
BlueprintIdentifier = "ValidatorUITests"
BuildableName = "ValidatorUITests"
BlueprintName = "ValidatorUITests"
ReferencedContainer = "container:">
</BuildableReference>
</TestableReference>
</Testables>
</TestAction>
<LaunchAction
Expand Down
13 changes: 7 additions & 6 deletions Package.swift
Original file line number Diff line number Diff line change
@@ -1,15 +1,15 @@
// swift-tools-version: 5.5
// swift-tools-version: 5.7
// The swift-tools-version declares the minimum version of Swift required to build this package.

import PackageDescription

let package = Package(
name: "Validator",
platforms: [
.iOS(.v13),
.macOS(.v10_13),
.watchOS(.v7),
.tvOS(.v13),
.iOS(.v16),
.macOS(.v13),
.watchOS(.v9),
.tvOS(.v16),
],
products: [
.library(name: "ValidatorCore", targets: ["ValidatorCore"]),
Expand All @@ -19,6 +19,7 @@ let package = Package(
targets: [
.target(name: "ValidatorCore", dependencies: []),
.target(name: "ValidatorUI", dependencies: ["ValidatorCore"]),
.testTarget(name: "ValidatorTests", dependencies: ["ValidatorCore"]),
.testTarget(name: "ValidatorCoreTests", dependencies: ["ValidatorCore"]),
.testTarget(name: "ValidatorUITests", dependencies: ["ValidatorCore", "ValidatorUI"]),
]
)

This file was deleted.

27 changes: 0 additions & 27 deletions Sources/ValidatorCore/Classes/Rules/AnyValidationRule.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
20 changes: 0 additions & 20 deletions Sources/ValidatorCore/IValidator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -15,25 +15,6 @@ public protocol IValidator {
/// - rule: The validation rule.
///
/// - Returns: A validation result.
func validate<T: IValidationRule>(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<T>(input: T, rules: [AnyValidationRule<T>]) -> 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<T>(input: T, rule: some IValidationRule<T>) -> ValidationResult

/// Validates an input value.
Expand All @@ -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<T>(input: T, rules: [any IValidationRule<T>]) -> ValidationResult
}
16 changes: 3 additions & 13 deletions Sources/ValidatorCore/Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,10 @@ public final class Validator {
// MARK: IValidator

extension Validator: IValidator {
public func validate<T>(input: T.Input, rule: T) -> ValidationResult where T: IValidationRule {
validate(input: input, rules: [rule.eraseToAnyValidationRule()])
public func validate<T>(input: T, rule: some IValidationRule<T>) -> ValidationResult {
validate(input: input, rules: [rule])
}

public func validate<T>(input: T, rules: [AnyValidationRule<T>]) -> 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<T>(input: T, rules: [any IValidationRule<T>]) -> ValidationResult {
let errors = rules
.filter { !self.validate(input: input, rule: $0) }
Expand All @@ -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<T>(input: T, rule: some IValidationRule<T>) -> Bool {
private func validate<T>(input: T, rule: some IValidationRule<T>) -> Bool {
rule.validate(input: input)
}
}
Original file line number Diff line number Diff line change
@@ -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
89 changes: 89 additions & 0 deletions Sources/ValidatorUI/Classes/IUIValidatable.swift
Original file line number Diff line number Diff line change
@@ -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<T>(rule: some IValidationRule<T>) -> ValidationResult where T == Input

/// Validates an input value.
///
/// - Parameters:
/// - rules: The validation rules array.
///
/// - Returns: A validation result.
func validate<T>(rules: [any IValidationRule<T>]) -> 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<T>(rule: some IValidationRule<T>) -> ValidationResult where T == Input {
let result = validator.validate(input: inputValue, rule: rule)
validationHandler?(result)
return result
}

@discardableResult
func validate<T>(rules: [any IValidationRule<T>]) -> ValidationResult where T == Input {
let result = validator.validate(input: inputValue, rules: rules)
validationHandler?(result)
return result
}

func add<T>(rule: some IValidationRule<T>) where T == Input {
validationRules.append(rule)
}

var validationRules: [any IValidationRule<Input>] {
get {
(objc_getAssociatedObject(self, &kValidationRules) as? AnyObject) as? [any IValidationRule<Input>] ?? []
}
set {
objc_setAssociatedObject(
self,
&kValidationRules,
newValue as [any IValidationRule<Input>],
.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)
}
}
}
}
12 changes: 0 additions & 12 deletions Sources/ValidatorUI/ValidatorUI.swift

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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])
Expand All @@ -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])
Expand All @@ -70,27 +70,6 @@ final class ValidatorTests: XCTestCase {
}
}

func test_thatValidatorValidatesInput_whenThereAreCoupleOfRules() {
// given
let rules: [AnyValidationRule<String>] = [
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<String>] = [
Expand Down
Loading

0 comments on commit bb9a3ba

Please sign in to comment.