Skip to content

Commit

Permalink
Implement a form validation logic
Browse files Browse the repository at this point in the history
  • Loading branch information
ns-vasilev committed Sep 26, 2023
1 parent 43fbc45 commit 9b4d743
Show file tree
Hide file tree
Showing 8 changed files with 290 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@ import SwiftUI
import ValidatorCore

public extension View {
/// Creates a view validation modifier.
///
/// - Parameters:
/// - item: The binding item to validate.
/// - rules: The array of validation rules to apply to the item’s value.
/// - content: A custom parameter attribute that constructs an error view from closures.
///
/// - Returns: A modified view.
func validate<T, ErrorView: View>(
item: Binding<T>,
rules: [any IValidationRule<T>],
Expand All @@ -20,4 +28,23 @@ public extension View {
)
)
}

/// Creates a view validation modifier.
///
/// - Parameters:
/// - validationContainer: The container to validate.
/// - content: A custom parameter attribute that constructs an error view from closures.
///
/// - Returns: A modified view.
func validate<ErrorView: View>(
validationContainer: any IFormValidationContainer,
@ViewBuilder content: @escaping ([any IValidationError]) -> ErrorView
) -> some View {
modifier(
FormValidationViewModifier(
validationContainer: validationContainer,
content: content
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

public typealias ValidationPublisher = AnyPublisher<ValidationResult, Never>

// MARK: - FormField

@propertyWrapper
public final class FormField<Value>: IFormField {
// MARK: Properties

@Published
/// The value to validate.
private var value: Value

/// The validation.
private let validator: IValidator

/// The validation rules.
private let rules: [any IValidationRule<Value>]

public var wrappedValue: Value {
get { value }
set { value = newValue }
}

// MARK: Initialization

public init(
wrappedValue: Value,
validator: IValidator = Validator(),
rules: [any IValidationRule<Value>]
) {
value = wrappedValue
self.validator = validator
self.rules = rules
}

// MARK: IFormField

public func validate(manager: some IFormFieldManager) -> any IFormValidationContainer {
let subject = CurrentValueSubject<Value, Never>(value)

let publisher = $value
.receive(on: RunLoop.main)
.handleEvents(receiveOutput: { subject.send($0) })
.map { self.validator.validate(input: $0, rules: self.rules) }
.eraseToAnyPublisher()

let container = FormValidationContainter(
value: subject,
publisher: publisher,
validator: validator,
rules: rules
)

manager.append(validator: container)

return container
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

/// A type that represents a field on a form.
public protocol IFormField {
/// Performs field validation.
///
/// - Note: Create a form validation container that keeps track of the validation.
///
/// - Parameter manager: The form field manager.
///
/// - Returns: A validation container.
func validate(manager: some IFormFieldManager) -> any IFormValidationContainer
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

public final class FormFieldManager: IFormFieldManager {
// MARK: Properties

@Published public var isValid = true

private var validators: [any IFormValidationContainer] = []

// MARK: Initialization

public init() {}

// MARK: IFormFieldManager

public func append(validator: some IFormValidationContainer) {
validators.append(validator)
validate()
}

public func validate() {
// swiftlint:disable:next contains_over_filter_is_empty
isValid = validators
.filter { $0.validate() != .valid }
.isEmpty
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

/// A type that manages the validation state of a form.
public protocol IFormFieldManager: ObservableObject {
/// A Boolean value that indicates whether all fields on a form are valid or not.
var isValid: Bool { get }

/// Appends a new validator to the manager.
///
/// - Parameter validator: The validation container that encompasses required validation logic.
func append(validator: some IFormValidationContainer)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

public struct FormValidationContainter<T>: IFormValidationContainer {
// MARK: Properties

public var value: FormValidatorValueSubject<T>
public let publisher: ValidationPublisher
public let validator: IValidator
public let rules: [any IValidationRule<T>]

// MARK: Initialization

public init(
value: FormValidatorValueSubject<T>,
publisher: ValidationPublisher,
validator: IValidator,
rules: [any IValidationRule<T>]
) {
self.value = value
self.publisher = publisher
self.validator = validator
self.rules = rules
}

// MARK: IFormValidationContainer

public func validate() -> ValidationResult {
validator.validate(input: value.value, rules: rules)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import Combine
import Foundation
import ValidatorCore

public typealias FormValidatorValueSubject<Value> = CurrentValueSubject<Value, Never>

// MARK: - IFormValidationContainer

/// A container for form validation logic.
public protocol IFormValidationContainer<Value> {
associatedtype Value

/// The value subject used for form validation.
var value: FormValidatorValueSubject<Value> { get }

/// The publisher responsible for emitting validation events.
var publisher: ValidationPublisher { get }

/// The validator associated with this validation container.
var validator: IValidator { get }

/// An array of validation rules to apply to the form field.
var rules: [any IValidationRule<Value>] { get }

/// Performs form field validation.
///
/// - Returns: The result of the validation process.
func validate() -> ValidationResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
//
// Validator
// Copyright © 2023 Space Code. All rights reserved.
//

import SwiftUI
import ValidatorCore

public struct FormValidationViewModifier<ErrorView: View>: ViewModifier {
// MARK: Properties

/// The result of the validation.
@State private var validationResult: ValidationResult = .valid

/// A container for form validation logic.
private let validationContainer: any IFormValidationContainer

/// A custom parameter attribute that constructs views from closures.
@ViewBuilder private let content: ([any IValidationError]) -> ErrorView

// MARK: Initialization

public init(
validationContainer: any IFormValidationContainer,
@ViewBuilder content: @escaping ([any IValidationError]) -> ErrorView
) {
self.validationContainer = validationContainer
self.content = content
}

// MARK: ViewModifier

public func body(content: Content) -> some View {
VStack(alignment: .leading) {
content
validationMessageView
}.onReceive(validationContainer.publisher) { result in
self.validationResult = result
}
}

// MARK: Private

private var validationMessageView: some View {
switch validationResult {
case .valid:
return EmptyView().eraseToAnyView()
case let .invalid(errors):
return content(errors).eraseToAnyView()
}
}
}

0 comments on commit 9b4d743

Please sign in to comment.