Something went wrong. Try again.
A library for helping build high quality forms in SwiftUI and TCA
Something went wrong. Try again.
34 kB · 1033 lines
Swift
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034import ComposableArchitecture2import Dependenciesimport Foundation
public typealias ValidationErrors = [AnyHashable: [ErrorMessage]]public typealias FieldValidationStates = [AnyHashable: FieldValidationState]
private enum FormObservedChangeValidationDelayKey: DependencyKey { static var liveValue: Duration { .milliseconds(300) } static var testValue: Duration { .milliseconds(300) }}
extension DependencyValues { public var formObservedChangeValidationDelay: Duration { get { self[FormObservedChangeValidationDelayKey.self] } set { self[FormObservedChangeValidationDelayKey.self] = newValue } }}
public struct ValidationPolicy: Sendable, Hashable { public init(triggers: ValidationTriggers) { self.triggers = triggers }
public static let all = Self(triggers: .all) public static let defaultAsync = Self(triggers: .all) public static let defaultSync = Self(triggers: .all) public static let fieldSubmitted = Self(triggers: .fieldSubmitted) public static let focusLoss = Self(triggers: .focusLoss) public static let formSubmitted = Self(triggers: .formSubmitted) public static let observedChange = Self(triggers: .observedChange) public static let userActions = Self(triggers: .userActions)
public var triggers: ValidationTriggers
public func runs(on trigger: ValidationTrigger) -> Bool { self.triggers.contains(trigger) }}
public enum ValidationTrigger: Sendable, Hashable { case fieldSubmitted case focusLoss case formSubmitted case observedChange}
public struct ValidationTriggers: Sendable, OptionSet, Hashable { public init(rawValue: Int) { self.rawValue = rawValue }
public static let fieldSubmitted = Self(rawValue: 1 << 0) public static let focusLoss = Self(rawValue: 1 << 1) public static let formSubmitted = Self(rawValue: 1 << 2) public static let observedChange = Self(rawValue: 1 << 3)
public static let all: Self = [ .fieldSubmitted, .focusLoss, .formSubmitted, .observedChange, ] public static let userActions: Self = [ .fieldSubmitted, .focusLoss, .formSubmitted, ]
public let rawValue: Int
public func contains(_ trigger: ValidationTrigger) -> Bool { switch trigger { case .fieldSubmitted: self.contains(ValidationTriggers.fieldSubmitted) case .focusLoss: self.contains(ValidationTriggers.focusLoss) case .formSubmitted: self.contains(ValidationTriggers.formSubmitted) case .observedChange: self.contains(ValidationTriggers.observedChange) } }}
public struct ScopedFormFieldID: Hashable { public init(scope: AnyHashable, field: AnyHashable) { self.field = field self.scope = scope }
public let field: AnyHashable public let scope: AnyHashable}
public struct FieldValidationState: Sendable, Equatable { public static let valid = Self()
public static func invalid(_ error: ErrorMessage) -> Self { .invalid([error]) }
public static func invalid(_ errors: [ErrorMessage]) -> Self { Self(errors: errors) }
public static func validating(errors: [ErrorMessage] = []) -> Self { Self(errors: errors, isValidating: true) }
public init( errors: [ErrorMessage] = [], isValidating: Bool = false ) { self.errors = errors self.isValidating = isValidating }
public var errors: [ErrorMessage] public var isValidating: Bool}
public enum FormSubmissionStatus: Sendable, Equatable { case idle case submitted case submitting}
public struct ErrorMessage: Sendable, Equatable { public static func == (lhs: Self, rhs: Self) -> Bool { lhs.message == rhs.message && lhs.subtitle == rhs.subtitle }
public init( message: String, subtitle: String? = nil, originalError: (any Error)? = nil ) { self.message = message self.originalError = originalError self.subtitle = subtitle }
public let message: String public let originalError: (any Error)? public let subtitle: String?}
public enum ValidationResult: Sendable, Equatable { case invalid([ErrorMessage]) case valid
public static func invalid(_ message: ErrorMessage) -> Self { .invalid([message]) }
public static func invalid( _ message: String, subtitle: String? = nil, originalError: (any Error)? = nil ) -> Self { .invalid( ErrorMessage( message: message, subtitle: subtitle, originalError: originalError ) ) }}
public struct Validation<Value> { public typealias AsyncRun = nonisolated(nonsending) (Value) async throws -> ValidationResult
public enum Run { case async(AsyncRun) case sync((Value) throws -> ValidationResult) }
public init( policy: ValidationPolicy? = nil, _ validate: @escaping (Value) -> ValidationResult ) { self.policy = policy ?? .defaultSync self.run = .sync(validate) }
public init( policy: ValidationPolicy? = nil, _ validate: @escaping (Value) throws -> ValidationResult ) { self.policy = policy ?? .defaultSync self.run = .sync(validate) }
public init( policy: ValidationPolicy? = nil, _ validate: nonisolated(nonsending) @escaping (Value) async throws -> ValidationResult ) { self.policy = policy ?? .defaultAsync self.run = .async(validate) }
public init( _ validate: @escaping (Value) -> Bool, error: ErrorMessage, policy: ValidationPolicy? = nil ) { self.policy = policy ?? .defaultSync self.run = .sync { value in validate(value) ? .valid : .invalid(error) } }
public init( _ validate: @escaping (Value) throws -> Bool, error: ErrorMessage, policy: ValidationPolicy? = nil ) { self.policy = policy ?? .defaultSync self.run = .sync { value in try validate(value) ? .valid : .invalid(error) } }
public init( _ validate: @escaping (Value) -> Bool, error message: String, subtitle: String? = nil, policy: ValidationPolicy? = nil ) { self.init( validate, error: ErrorMessage(message: message, subtitle: subtitle), policy: policy ) }
public init( _ validate: @escaping (Value) throws -> Bool, error message: String, subtitle: String? = nil, policy: ValidationPolicy? = nil ) { self.init( validate, error: ErrorMessage(message: message, subtitle: subtitle), policy: policy ) }
public init( error: ErrorMessage, policy: ValidationPolicy? = nil, _ validate: @escaping (Value) -> Bool ) { self.init(validate, error: error, policy: policy) }
public init( error: ErrorMessage, policy: ValidationPolicy? = nil, _ validate: @escaping (Value) throws -> Bool ) { self.init(validate, error: error, policy: policy) }
public init( error message: String, subtitle: String? = nil, policy: ValidationPolicy? = nil, _ validate: @escaping (Value) -> Bool ) { self.init(validate, error: message, subtitle: subtitle, policy: policy) }
public init( error message: String, subtitle: String? = nil, policy: ValidationPolicy? = nil, _ validate: @escaping (Value) throws -> Bool ) { self.init(validate, error: message, subtitle: subtitle, policy: policy) }
public var policy: ValidationPolicy public var run: Run}
@resultBuilderpublic enum ValidationBuilder<Value> { public static func buildExpression(_ validation: Validation<Value>) -> Validation<Value> { validation }
public static func buildBlock(_ validations: Validation<Value>...) -> [Validation<Value>] { validations }}
@resultBuilderpublic enum FormFieldDefinitionBuilder<Value> { public static func buildExpression( _ field: FormFieldDefinition<Value> ) -> [FormFieldDefinition<Value>] { [field] }
public static func buildExpression( _ fields: [FormFieldDefinition<Value>] ) -> [FormFieldDefinition<Value>] { fields }
public static func buildBlock( _ fields: [FormFieldDefinition<Value>]... ) -> [FormFieldDefinition<Value>] { fields.flatMap { $0 } }}
public struct FormFieldDefinition<Value> { fileprivate init( id: AnyHashable, hasAsyncValidation: @escaping (ValidationTrigger) -> Bool, hasSkippedValidation: @escaping (ValidationTrigger) -> Bool, observedValue: @escaping (Value) -> AnyEquatable, validateAsync: nonisolated(nonsending) @escaping (Value, ValidationTrigger) async -> [ErrorMessage], validateSync: @escaping (Value, ValidationTrigger) -> ValidationEvaluation ) { self.hasAsyncValidation = hasAsyncValidation self.hasSkippedValidation = hasSkippedValidation self.id = id self.observedValue = observedValue self.validateAsync = validateAsync self.validateSync = validateSync }
public init<FieldValue: Equatable>( _ keyPath: KeyPath<Value, FieldValue>, id: AnyHashable? = nil, @ValidationBuilder<FieldValue> validations: () -> [Validation<FieldValue>] = { [] } ) { self.init( id: id ?? AnyHashable(keyPath), value: keyPath, validations: validations ) }
public init<FieldValue: Equatable>( id: AnyHashable, value keyPath: KeyPath<Value, FieldValue>, @ValidationBuilder<FieldValue> validations: () -> [Validation<FieldValue>] = { [] } ) { let validations = validations() self.hasAsyncValidation = { trigger in validations.hasAsyncValidation(on: trigger) } self.hasSkippedValidation = { trigger in validations.hasSkippedValidation(on: trigger) } self.id = id self.observedValue = { value in AnyEquatable(value[keyPath: keyPath]) } self.validateAsync = { value, trigger in await validations.validateAsync(value[keyPath: keyPath], on: trigger) } self.validateSync = { value, trigger in validations.validateSync(value[keyPath: keyPath], on: trigger) } }
fileprivate var hasAsyncValidation: (ValidationTrigger) -> Bool fileprivate var hasSkippedValidation: (ValidationTrigger) -> Bool public var id: AnyHashable fileprivate var observedValue: (Value) -> AnyEquatable fileprivate var validateAsync: nonisolated(nonsending) (Value, ValidationTrigger) async -> [ErrorMessage] fileprivate var validateSync: (Value, ValidationTrigger) -> ValidationEvaluation}
@Featurepublic struct Form<Value> { public init(fields: [FormFieldDefinition<Value>]) { self.init( fields: fields, submitDelegate: { _ in } ) }
public init( @FormFieldDefinitionBuilder<Value> fields: () -> [FormFieldDefinition<Value>] ) { self.init(fields: fields()) }
private init( fields: [FormFieldDefinition<Value>], submitDelegate: @escaping (Value) throws -> Void ) { self.fields = fields self.submitDelegate = submitDelegate var validationTaskIDs: [ValidationTaskKey: StoreTaskID] = [:] for field in fields { for trigger in validationTaskTriggers { validationTaskIDs[ ValidationTaskKey(field: field.id, trigger: trigger) ] = StoreTaskID() } } self.validationTaskIDs = validationTaskIDs }
public static func Field<Root, FieldValue: Equatable>( _ keyPath: KeyPath<Root, FieldValue>, id: AnyHashable? = nil, @ValidationBuilder<FieldValue> validations: () -> [Validation<FieldValue>] = { [] } ) -> FormFieldDefinition<Root> { FormFieldDefinition( keyPath, id: id, validations: validations ) }
public static func Field<Root, FieldValue: Equatable>( id: AnyHashable, value keyPath: KeyPath<Root, FieldValue>, @ValidationBuilder<FieldValue> validations: () -> [Validation<FieldValue>] = { [] } ) -> FormFieldDefinition<Root> { FormFieldDefinition( id: id, value: keyPath, validations: validations ) }
public static func Scope<Child>( _ keyPath: KeyPath<Value, Child>, id: AnyHashable? = nil, @FormFieldDefinitionBuilder<Child> fields: () -> [FormFieldDefinition<Child>] ) -> [FormFieldDefinition<Value>] { let scope = id ?? AnyHashable(keyPath) return fields().map { field in FormFieldDefinition( id: AnyHashable( ScopedFormFieldID( scope: scope, field: field.id ) ), hasAsyncValidation: field.hasAsyncValidation, hasSkippedValidation: field.hasSkippedValidation, observedValue: { value in field.observedValue(value[keyPath: keyPath]) }, validateAsync: { value, trigger in await field.validateAsync(value[keyPath: keyPath], trigger) }, validateSync: { value, trigger in field.validateSync(value[keyPath: keyPath], trigger) } ) } }
public struct State { public init( value: Value, focusedField: AnyHashable? = nil, submissionStatus: FormSubmissionStatus = .idle, validation: FieldValidationStates = [:] ) { self.asyncValidationRuns = [:] self.focusedField = focusedField self.nextValidationRunID = 0 self.pendingObservedChangeValidationFields = [] self.submissionStatus = submissionStatus self.validation = validation self.value = value }
public init( value: Value, focusedField: AnyHashable? = nil, validationErrors: ValidationErrors ) { self.init( value: value, focusedField: focusedField, submissionStatus: .idle, validation: validationErrors.mapValues(FieldValidationState.invalid) ) }
public var canSubmit: Bool { !self.hasErrors && !self.isValidating } public var focusedField: AnyHashable? public var hasErrors: Bool { self.validation.values.contains { !$0.errors.isEmpty } } public var isSubmitted: Bool { self.submissionStatus == .submitted } public var isSubmitting: Bool { self.submissionStatus == .submitting } public var isValidating: Bool { self.validation.values.contains { $0.isValidating } } public var submissionStatus: FormSubmissionStatus public var validation: FieldValidationStates public var validationErrors: ValidationErrors { get { self.validation.compactMapValues { state in state.errors.isEmpty ? nil : state.errors } } set { self.validation = newValue.mapValues(FieldValidationState.invalid) } } public var value: Value fileprivate var asyncValidationRuns: [ValidationRunID: ActiveAsyncValidationRun] fileprivate var nextValidationRunID: Int fileprivate var pendingObservedChangeValidationFields: Set<AnyHashable> }
public enum Action { case fieldSubmitted(AnyHashable) case submittedForm }
public func onSubmit(_ action: @escaping (Value) throws -> Void) -> Self { Self( fields: self.fields, submitDelegate: action ) }
@Dependency(\.continuousClock) private var clock @Dependency(\.formObservedChangeValidationDelay) private var observedChangeValidationDelay @StoreTaskID private var observedChangeValidationTask
private let fields: [FormFieldDefinition<Value>] private let submitDelegate: (Value) throws -> Void private let validationTaskIDs: [ValidationTaskKey: StoreTaskID]
public var body: some Feature { let _ = Self._logChanges() let submit: (Value) -> Void = { value in store.addTask { try self.submitDelegate(value) } } let runAsyncValidation: (PendingAsyncValidation<Value>) -> Void = { validation in store.addTask(id: self.validationTaskID(for: validation)) { let errors = await validation.validateAsync(validation.value, validation.trigger) try Task.checkCancellation() var submitValue: Value? try store.modify { state in let isSubmitRun = state.finishAsyncValidation( validation, errors: errors ) self.refreshValidatingState(for: validation.field, state: &state) if isSubmitRun, state.finishSubmissionIfValidationSettled() { submitValue = state.value } } if let submitValue { try self.submitDelegate(submitValue) } } } let cancelAsyncValidationTasks: (AnyHashable) -> Void = { field in let validationTaskIDs = self.validationTaskIDs(for: field) store.addTask { for validationTaskID in validationTaskIDs { validationTaskID.cancel() } } store.addTask(id: self.observedChangeValidationTask) {} } let scheduleDebouncedObservedValidation: () -> Void = { store.addTask(id: self.observedChangeValidationTask) { try await self.clock.sleep(for: self.observedChangeValidationDelay) var pendingAsyncValidations: [PendingAsyncValidation<Value>] = [] try store.modify { state in let fields = state.pendingObservedChangeValidationFields state.pendingObservedChangeValidationFields.removeAll() for field in fields { self.validate( field, state: &state, trigger: .observedChange, runAsyncValidation: { validation in pendingAsyncValidations.append(validation) }, cancelAsyncValidationTasks: cancelAsyncValidationTasks ) } } for validation in pendingAsyncValidations { let errors = await validation.validateAsync(validation.value, validation.trigger) try Task.checkCancellation() var submitValue: Value? try store.modify { state in let isSubmitRun = state.finishAsyncValidation( validation, errors: errors ) self.refreshValidatingState(for: validation.field, state: &state) if isSubmitRun, state.finishSubmissionIfValidationSettled() { submitValue = state.value } } if let submitValue { try self.submitDelegate(submitValue) } } } }
Update { state, action in switch action { case let .fieldSubmitted(field): if state.focusedField != field { self.validate( field, state: &state, trigger: .fieldSubmitted, runAsyncValidation: runAsyncValidation, cancelAsyncValidationTasks: cancelAsyncValidationTasks ) } state.focusedField = self.nextField(after: field)
case .submittedForm: for field in self.fields { self.validate( field.id, state: &state, trigger: .formSubmitted, runAsyncValidation: runAsyncValidation, cancelAsyncValidationTasks: cancelAsyncValidationTasks ) } if state.updateSubmissionStatusAfterSubmit() { submit(state.value) } } } .onChange(of: store.focusedField) { oldField, newField, state in guard let oldField, oldField != newField else { return } self.validate( oldField, state: &state, trigger: .focusLoss, runAsyncValidation: runAsyncValidation, cancelAsyncValidationTasks: cancelAsyncValidationTasks ) } .onChange(of: self.snapshot(store.value)) { oldSnapshot, newSnapshot, state in let changedFields = self.changedFields(from: oldSnapshot, to: newSnapshot) if !changedFields.isEmpty { state.submissionStatus = .idle } for field in changedFields { state.cancelAsyncValidationRuns(for: field) cancelAsyncValidationTasks(field) if state.hasErrors(for: field) { self.validate( field, state: &state, trigger: .observedChange, runAsyncValidation: runAsyncValidation, cancelAsyncValidationTasks: cancelAsyncValidationTasks ) } else { state.validation[field] = nil state.pendingObservedChangeValidationFields.insert(field) scheduleDebouncedObservedValidation() } } } }
private func changedFields( from oldSnapshot: FormFieldSnapshot, to newSnapshot: FormFieldSnapshot ) -> [AnyHashable] { self.fields.compactMap { field in oldSnapshot.values[field.id] != newSnapshot.values[field.id] ? field.id : nil } }
private func nextField(after id: AnyHashable) -> AnyHashable? { guard let index = self.fields.firstIndex(where: { $0.id == id }) else { return nil } let nextIndex = self.fields.index(after: index) guard nextIndex != self.fields.endIndex else { return nil } return self.fields[nextIndex].id }
private func snapshot(_ value: Value) -> FormFieldSnapshot { FormFieldSnapshot( values: Dictionary( uniqueKeysWithValues: self.fields.map { field in (field.id, field.observedValue(value)) } ) ) }
private func validate( _ field: AnyHashable, state: inout State, trigger: ValidationTrigger, runAsyncValidation: (PendingAsyncValidation<Value>) -> Void, cancelAsyncValidationTasks: (AnyHashable) -> Void ) { guard let definition = self.fields.first(where: { $0.id == field }) else { return } state.pendingObservedChangeValidationFields.remove(field) if trigger != .observedChange { state.cancelAsyncValidationRuns(for: field) cancelAsyncValidationTasks(field) } let syncResult = definition.validateSync(state.value, trigger) guard syncResult.errors.isEmpty else { state.validation[field] = .invalid(syncResult.errors) return } guard definition.hasAsyncValidation(trigger) else { if syncResult.didRun, !definition.hasSkippedValidation(trigger) { state.validation[field] = .valid } else { state.validation[field] = nil } return } let value = state.value let validatedValue = definition.observedValue(value) let runID = state.startAsyncValidation( field: field, isSubmitRun: trigger == .formSubmitted, validatedValue: validatedValue ) runAsyncValidation( PendingAsyncValidation( field: field, id: runID, marksValid: !definition.hasSkippedValidation(trigger), observedValue: definition.observedValue, validatedValue: validatedValue, validateAsync: definition.validateAsync, trigger: trigger, value: value ) ) }
private func validationTaskIDs(for field: AnyHashable) -> [StoreTaskID] { validationTaskTriggers.compactMap { trigger in self.validationTaskID(for: field, trigger: trigger) } }
private func refreshValidatingState(for field: AnyHashable, state: inout State) { guard let definition = self.fields.first(where: { $0.id == field }) else { return } let currentValue = definition.observedValue(state.value) let isValidatingCurrentValue = state.asyncValidationRuns.values.contains { run in run.field == field && run.validatedValue == currentValue } if isValidatingCurrentValue { state.validation[field] = .validating( errors: state.validation[field]?.errors ?? [] ) } else if var validation = state.validation[field], validation.isValidating { validation.isValidating = false state.validation[field] = validation.errors.isEmpty ? .valid : validation } }
private func validationTaskID( for field: AnyHashable, trigger: ValidationTrigger ) -> StoreTaskID? { self.validationTaskIDs[ ValidationTaskKey(field: field, trigger: trigger) ] }
private func validationTaskID( for validation: PendingAsyncValidation<Value> ) -> StoreTaskID? { return self.validationTaskID( for: validation.field, trigger: validation.trigger ) }}
extension Form.State: Equatable where Value: Equatable {}
private struct ActiveAsyncValidationRun: Equatable { var field: AnyHashable var isSubmitRun: Bool var validatedValue: AnyEquatable}
private struct PendingAsyncValidation<Value> { var field: AnyHashable var id: ValidationRunID var marksValid: Bool var observedValue: (Value) -> AnyEquatable var validatedValue: AnyEquatable var validateAsync: nonisolated(nonsending) (Value, ValidationTrigger) async -> [ErrorMessage] var trigger: ValidationTrigger var value: Value}
private struct ValidationRunID: Hashable { var rawValue: Int}
private struct ValidationTaskKey: Hashable { var field: AnyHashable var trigger: ValidationTrigger}
private let validationTaskTriggers: [ValidationTrigger] = [ .fieldSubmitted, .focusLoss, .formSubmitted, .observedChange,]
extension Form.State { fileprivate mutating func cancelAsyncValidationRuns(for field: AnyHashable) { self.asyncValidationRuns = self.asyncValidationRuns.filter { _, run in run.field != field } }
fileprivate mutating func finishAsyncValidation( _ validation: PendingAsyncValidation<Value>, errors: [ErrorMessage] ) -> Bool { guard let run = self.asyncValidationRuns.removeValue(forKey: validation.id) else { return false } guard validation.observedValue(self.value) == validation.validatedValue else { return run.isSubmitRun } if errors.isEmpty, !validation.marksValid { self.validation[validation.field] = nil } else { self.validation[validation.field] = FieldValidationState(errors: errors) } return run.isSubmitRun }
fileprivate mutating func finishSubmissionIfValidationSettled() -> Bool { guard self.submissionStatus == .submitting, !self.hasSubmitValidationRuns else { return false } guard !self.hasErrors else { self.submissionStatus = .idle return false } self.submissionStatus = .submitted return true }
fileprivate var hasSubmitValidationRuns: Bool { self.asyncValidationRuns.values.contains { $0.isSubmitRun } }
fileprivate func hasErrors(for field: AnyHashable) -> Bool { !(self.validation[field]?.errors ?? []).isEmpty }
fileprivate mutating func startAsyncValidation( field: AnyHashable, isSubmitRun: Bool, validatedValue: AnyEquatable ) -> ValidationRunID { let runID = ValidationRunID(rawValue: self.nextValidationRunID) self.nextValidationRunID += 1 self.asyncValidationRuns[runID] = ActiveAsyncValidationRun( field: field, isSubmitRun: isSubmitRun, validatedValue: validatedValue ) self.validation[field] = .validating() return runID }
fileprivate mutating func updateSubmissionStatusAfterSubmit() -> Bool { if self.hasSubmitValidationRuns { self.submissionStatus = .submitting return false } else { guard !self.hasErrors else { self.submissionStatus = .idle return false } self.submissionStatus = .submitted return true } }}
private struct AnyEquatable: Equatable { init<Value: Equatable>(_ value: Value) { self.value = value self.isEqual = { other in guard let other = other as? Value else { return false } return value == other } }
private let value: Any private let isEqual: (Any) -> Bool
static func == (lhs: Self, rhs: Self) -> Bool { lhs.isEqual(rhs.value) }}
private struct FormFieldSnapshot: Equatable { var values: [AnyHashable: AnyEquatable]}
private struct ValidationEvaluation { var didRun = false var errors: [ErrorMessage] = []}
private extension Array { func hasAsyncValidation<Value>(on trigger: ValidationTrigger) -> Bool where Element == Validation<Value> { self.contains { validation in guard case .async = validation.run else { return false } return validation.policy.runs(on: trigger) } }
func hasSkippedValidation<Value>(on trigger: ValidationTrigger) -> Bool where Element == Validation<Value> { self.contains { validation in !validation.policy.runs(on: trigger) } }
nonisolated(nonsending) func validateAsync<Value>( _ value: Value, on trigger: ValidationTrigger ) async -> [ErrorMessage] where Element == Validation<Value> { var errors: [ErrorMessage] = [] for validation in self { guard case let .async(validate) = validation.run else { continue } guard validation.policy.runs(on: trigger) else { continue } do { switch try await validate(value) { case let .invalid(messages): errors.append(contentsOf: messages) case .valid: break } } catch { errors.append(ErrorMessage(error)) } } return errors }
func validateSync<Value>( _ value: Value, on trigger: ValidationTrigger ) -> ValidationEvaluation where Element == Validation<Value> { var result = ValidationEvaluation() for validation in self { guard case let .sync(validate) = validation.run else { continue } guard validation.policy.runs(on: trigger) else { continue } result.didRun = true do { switch try validate(value) { case let .invalid(messages): result.errors.append(contentsOf: messages) case .valid: break } } catch { result.errors.append(ErrorMessage(error)) } } return result }}
private extension ErrorMessage { init(_ error: any Error) { self.init( message: error.localizedDescription, originalError: error ) }}