diff --git a/Example/Example.xcodeproj/project.pbxproj b/Example/Example.xcodeproj/project.pbxproj index 4504b24..e9e5f6e 100644 --- a/Example/Example.xcodeproj/project.pbxproj +++ b/Example/Example.xcodeproj/project.pbxproj @@ -10,7 +10,7 @@ 3D5D898A281D511A00DD9301 /* Assets.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 3D5D8989281D511A00DD9301 /* Assets.xcassets */; }; 3D7BB5D827A46B8C009D4145 /* App.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D727A46B8C009D4145 /* App.swift */; }; 3D7BB5DA27A46B8C009D4145 /* ContentView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3D7BB5D927A46B8C009D4145 /* ContentView.swift */; }; - 3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */ = {isa = PBXBuildFile; productRef = 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */; }; + EDE7521229D7099D00FDB406 /* ValidatedPropertyKit in Frameworks */ = {isa = PBXBuildFile; productRef = EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */; }; /* End PBXBuildFile section */ /* Begin PBXFileReference section */ @@ -18,7 +18,7 @@ 3D7BB5D427A46B8C009D4145 /* Example.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = Example.app; sourceTree = BUILT_PRODUCTS_DIR; }; 3D7BB5D727A46B8C009D4145 /* App.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = App.swift; sourceTree = ""; }; 3D7BB5D927A46B8C009D4145 /* ContentView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentView.swift; sourceTree = ""; }; - 3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ValidatedPropertyKit; path = ..; sourceTree = ""; }; + EDE7521029D7097B00FDB406 /* ValidatedPropertyKit */ = {isa = PBXFileReference; lastKnownFileType = wrapper; name = ValidatedPropertyKit; path = ..; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFrameworksBuildPhase section */ @@ -26,7 +26,7 @@ isa = PBXFrameworksBuildPhase; buildActionMask = 2147483647; files = ( - 3D7BB5E827A46BA4009D4145 /* ValidatedPropertyKit in Frameworks */, + EDE7521229D7099D00FDB406 /* ValidatedPropertyKit in Frameworks */, ); runOnlyForDeploymentPostprocessing = 0; }; @@ -36,10 +36,10 @@ 3D7BB5CB27A46B8C009D4145 = { isa = PBXGroup; children = ( + EDE7521029D7097B00FDB406 /* ValidatedPropertyKit */, 3D7BB5D627A46B8C009D4145 /* Example */, 3D7BB5D527A46B8C009D4145 /* Products */, 3D7BB5E627A46BA4009D4145 /* Frameworks */, - 3D7BB5E527A46B9F009D4145 /* ValidatedPropertyKit */, ); sourceTree = ""; }; @@ -85,7 +85,7 @@ ); name = Example; packageProductDependencies = ( - 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */, + EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */, ); productName = Example; productReference = 3D7BB5D427A46B8C009D4145 /* Example.app */; @@ -279,6 +279,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -307,6 +308,7 @@ INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + IPHONEOS_DEPLOYMENT_TARGET = 16.0; LD_RUNPATH_SEARCH_PATHS = ( "$(inherited)", "@executable_path/Frameworks", @@ -344,7 +346,7 @@ /* End XCConfigurationList section */ /* Begin XCSwiftPackageProductDependency section */ - 3D7BB5E727A46BA4009D4145 /* ValidatedPropertyKit */ = { + EDE7521129D7099D00FDB406 /* ValidatedPropertyKit */ = { isa = XCSwiftPackageProductDependency; productName = ValidatedPropertyKit; }; diff --git a/Example/Example/ContentView.swift b/Example/Example/ContentView.swift index 18a1929..faeeb9e 100644 --- a/Example/Example/ContentView.swift +++ b/Example/Example/ContentView.swift @@ -1,27 +1,48 @@ +import Combine +import RegexBuilder import SwiftUI import ValidatedPropertyKit +final class ContentModel: ObservableObject { + @PublishedValidated( + .range(3..., error: "Username is too short") + && .regex(Regex { OneOrMore(.digit) }, error: "Requires 1+ digits") + ) + var username = "" { + didSet { + usernameInvalid = !_username.isValid + usernameError = _username.errorAfterChanges + } + } + + @Published + var usernameInvalid = true + + @Published + var usernameError: String? = nil +} + struct ContentView { - - @Validated(!.isEmpty) + @Validated(!.isEmpty(error: "Username is not valid")) var username = String() - + + @ObservedObject + var model = ContentModel() + + @FocusState var focus } extension ContentView: View { - var body: some View { NavigationView { List { Section( header: Text(verbatim: "Username"), footer: Group { - if self._username.isInvalidAfterChanges { - Text( - verbatim: "Username is not valid" - ) - .foregroundColor(.red) - } + Text( + verbatim: _username.isInvalidAfterChanges ? "Username is not valid" : "" + ) + .foregroundColor(.red) } ) { TextField( @@ -29,6 +50,7 @@ extension ContentView: View { text: self.$username ) } + Section( footer: Button( action: { @@ -39,12 +61,44 @@ extension ContentView: View { } .buttonStyle(.borderedProminent) .validated(self._username) + ) {} + + Section( + header: Text(verbatim: "View Model Username"), + footer: Group { + Text( + verbatim: model.usernameError ?? "" + ) + .foregroundColor(.red) + } ) { - + TextField( + "John Doe", + text: $model.username + ) } + + Section( + footer: Button( + action: { + print("Login") + } + ) { + Text(verbatim: "Login") + } + .buttonStyle(.borderedProminent) + .disabled(model.usernameInvalid) + ) {} } .navigationTitle("ValidatedPropertyKit") } } - } + +#if DEBUG +struct ContentView_Previews: PreviewProvider { + static var previews: some View { + ContentView() + } +} +#endif diff --git a/Sources/PublishedValidated.swift b/Sources/PublishedValidated.swift new file mode 100644 index 0000000..888e60e --- /dev/null +++ b/Sources/PublishedValidated.swift @@ -0,0 +1,95 @@ +// +// PublishedValidated.swift +// +// +// Created by Jan Švec on 31.03.2023. +// + +import Combine +import SwiftUI + +@available(iOS 14.0, *) +@propertyWrapper +public final class PublishedValidated: ObservableObject, Validatable { + public var validation: Validation { + didSet { + validate() + } + } + + @Published + public private(set) var value: Value + + @Published + public private(set) var hasChanges = false + + @Published + public private(set) var isValid: Bool + + @Published + public private(set) var error: String? + + public var wrappedValue: Value { + get { + return value + } + set { + if newValue == value { return } + + value = newValue + validate() + + if !hasChanges { + hasChanges.toggle() + } + } + } + + public var projectedValue: Published.Publisher { + get { + return $value + } + set { + $value = newValue + validate() + + if !hasChanges { + hasChanges.toggle() + } + } + } + + public var isInvalidAfterChanges: Bool { + hasChanges && !isValid + } + + public var errorAfterChanges: String? { + isInvalidAfterChanges ? error : nil + } + + public func validate() { + do { + try validation.validate(value) + isValid = true + error = nil + } catch { + self.error = error as? String + isValid = false + } + } + + public init( + wrappedValue: Value, + _ validation: Validation + ) { + self.validation = validation + + _value = .init( + initialValue: wrappedValue + ) + + _isValid = .init( + initialValue: validation.validateCatched(wrappedValue) + ) + } +} diff --git a/Sources/Validated.swift b/Sources/Validated.swift index 46830a8..f99cb75 100644 --- a/Sources/Validated.swift +++ b/Sources/Validated.swift @@ -5,14 +5,12 @@ import SwiftUI /// A Validated PropertyWrapper @propertyWrapper public struct Validated: Validatable, DynamicProperty { - // MARK: Properties /// The Validation public var validation: Validation { didSet { - // Re-Validate - self.isValid = self.validation.validate(self.value) + self.revalidate() } } @@ -51,7 +49,8 @@ public struct Validated: Validatable, DynamicProperty { if !self.hasChanges { self.hasChanges.toggle() } - self.isValid = self.validation.validate(newValue) + + revalidate() } ) } @@ -67,13 +66,15 @@ public struct Validated: Validatable, DynamicProperty { _ validation: Validation ) { self.validation = validation - self._value = .init( - initialValue: wrappedValue - ) - self._isValid = .init( - initialValue: validation - .validate(wrappedValue) - ) + self._value = .init(initialValue: wrappedValue) + + var isValid = false + do { + try validation.validate(wrappedValue) + isValid = true + } catch {} + + self._isValid = .init(initialValue: isValid) } /// Creates a new instance of `Validated` @@ -95,37 +96,43 @@ public struct Validated: Validatable, DynamicProperty { ) } + func revalidate() { + self.isValid = self.validate() + } + + func validate() -> Bool { + do { + try self.validation.validate(self.value) + return true + } catch { + return false + } + } } // MARK: - Validated+validatedValue public extension Validated { - /// The value if is valid otherwise returns `nil` var validatedValue: Value? { self.isValid ? self.value : nil } - } // MARK: - Validated+isInvalid public extension Validated { - /// A Boolean value if the value is invalid var isInvalid: Bool { !self.isValid } - } // MARK: - Validated+isInvalidAfterChanges public extension Validated { - /// A Boolean value if the value is invalid and has been previously modified var isInvalidAfterChanges: Bool { self.hasChanges && !self.isValid } - } diff --git a/Sources/Validation/Validation+BinaryInteger.swift b/Sources/Validation/Validation+BinaryInteger.swift index 6b10233..eca9a22 100644 --- a/Sources/Validation/Validation+BinaryInteger.swift +++ b/Sources/Validation/Validation+BinaryInteger.swift @@ -3,15 +3,17 @@ import Foundation // MARK: - Validation+BinaryInteger public extension Validation where Value: BinaryInteger { - /// Validation that validates if thie value is a multiple of the given value /// - Parameter other: The other Value static func isMultiple( - of other: @autoclosure @escaping () -> Value + of other: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value.isMultiple(of: other()) - } + .init( + predicate: { value in + value.isMultiple(of: other()) + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+Collection.swift b/Sources/Validation/Validation+Collection.swift index ea702c5..5cce4eb 100644 --- a/Sources/Validation/Validation+Collection.swift +++ b/Sources/Validation/Validation+Collection.swift @@ -3,22 +3,26 @@ import Foundation // MARK: - Validation+Collection public extension Validation where Value: Collection { - - /// The isEmpty Validation - static var isEmpty: Self { - .init { value in - value.isEmpty - } + static func isEmpty(error: String? = nil) -> Self { + .init( + predicate: { value in + value.isEmpty + }, + error: error + ) } - + /// Validation with RangeExpression /// - Parameter range: The RangeExpression static func range( - _ range: @autoclosure @escaping () -> R + _ range: @autoclosure @escaping () -> R, + error: String? = nil ) -> Self where R.Bound == Int { - .init { value in - range().contains(value.count) - } + .init( + predicate: { value in + range().contains(value.count) + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+Comparable.swift b/Sources/Validation/Validation+Comparable.swift index 23edd8c..2acd2f9 100644 --- a/Sources/Validation/Validation+Comparable.swift +++ b/Sources/Validation/Validation+Comparable.swift @@ -3,45 +3,59 @@ import Foundation // MARK: - Validation+Comparable public extension Validation where Value: Comparable { - /// Validation with less `<` than comparable value /// - Parameter comparableValue: The Comparable value static func less( - _ comparableValue: @autoclosure @escaping () -> Value + _ comparableValue: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value < comparableValue() - } + .init( + predicate: { value in + value < comparableValue() + }, + error: error + ) } - + /// Validation with less or equal `<=` than comparable value /// - Parameter comparableValue: The Comparable value static func lessOrEqual( - _ comparableValue: @autoclosure @escaping () -> Value + _ comparableValue: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value <= comparableValue() - } + .init( + predicate: { value in + value <= comparableValue() + }, + error: error + ) } - + /// Validation with greater `>` than comparable value /// - Parameter comparableValue: The Comparable value static func greater( - _ comparableValue: @autoclosure @escaping () -> Value + _ comparableValue: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value > comparableValue() - } + .init( + predicate: { value in + value > comparableValue() + }, + error: error + ) } - + /// Validation with greater or equal `>=` than comparable value /// - Parameter comparableValue: The Comparable value static func greaterOrEqual( - _ comparableValue: @autoclosure @escaping () -> Value + _ comparableValue: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value >= comparableValue() - } + .init( + predicate: { value in + value >= comparableValue() + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+ComparisonOperators.swift b/Sources/Validation/Validation+ComparisonOperators.swift index caaa9b6..e339534 100644 --- a/Sources/Validation/Validation+ComparisonOperators.swift +++ b/Sources/Validation/Validation+ComparisonOperators.swift @@ -3,7 +3,6 @@ import Foundation // MARK: - Validation+Equal public extension Validation { - /// Returns a Validation where two given Validation results will be compared to equality /// - Parameters: /// - lhs: The left-hand side of the operation @@ -13,16 +12,14 @@ public extension Validation { rhs: Self ) -> Self { .init { value in - lhs.validate(value) == rhs.validate(value) + lhs.validateCatched(value) == rhs.validateCatched(value) } } - } // MARK: - Validation+Unequal public extension Validation { - /// Returns a Validation where two given Validation results will be compared to unequality /// - Parameters: /// - lhs: The left-hand side of the operation @@ -32,8 +29,7 @@ public extension Validation { rhs: Self ) -> Self { .init { value in - lhs.validate(value) != rhs.validate(value) + lhs.validateCatched(value) != rhs.validateCatched(value) } } - } diff --git a/Sources/Validation/Validation+Equatable.swift b/Sources/Validation/Validation+Equatable.swift index 61d95d1..28b25c3 100644 --- a/Sources/Validation/Validation+Equatable.swift +++ b/Sources/Validation/Validation+Equatable.swift @@ -3,15 +3,17 @@ import Foundation // MARK: - Validation+Equatable public extension Validation where Value: Equatable { - /// Returns a Validation indicating whether two values are equal. /// - Parameter equatableValue: The Equatable value static func equals( - _ equatableValue: @autoclosure @escaping () -> Value + _ equatableValue: @autoclosure @escaping () -> Value, + error: String? = nil ) -> Self { - .init { value in - value == equatableValue() - } + .init( + predicate: { value in + value == equatableValue() + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+KeyPath.swift b/Sources/Validation/Validation+KeyPath.swift index 08440ac..fa8104b 100644 --- a/Sources/Validation/Validation+KeyPath.swift +++ b/Sources/Validation/Validation+KeyPath.swift @@ -3,28 +3,34 @@ import Foundation // MARK: - Validation+KeyPath public extension Validation { - /// Validation via KeyPath /// - Parameters: /// - keyPath: A key path from a specific root type to a specific resulting value type /// - validation: The Validation for the specific resulting value type static func keyPath( _ keyPath: @autoclosure @escaping () -> KeyPath, - _ validation: @autoclosure @escaping () -> Validation + _ validation: @autoclosure @escaping () -> Validation, + error: String? = nil ) -> Self { - .init { value in - validation().validate(value[keyPath: keyPath()]) - } + .init( + predicate: { value in + validation().validateCatched(value[keyPath: keyPath()]) + }, + error: error + ) } - + /// Validation that checks if a given Bool value KeyPath evaluates to `true` /// - Parameter keyPath: The Bool value KeyPath static func keyPath( - _ keyPath: @autoclosure @escaping () -> KeyPath + _ keyPath: @autoclosure @escaping () -> KeyPath, + error: String? = nil ) -> Self { - .init { value in - value[keyPath: keyPath()] - } + .init( + predicate: { value in + value[keyPath: keyPath()] + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+LogicalOperators.swift b/Sources/Validation/Validation+LogicalOperators.swift index 8be98e4..ddf2881 100644 --- a/Sources/Validation/Validation+LogicalOperators.swift +++ b/Sources/Validation/Validation+LogicalOperators.swift @@ -3,23 +3,28 @@ import Foundation // MARK: - Validation+Not public extension Validation { - /// Performs a logical `NOT` (`!`) operation on a Validation /// - Parameter validation: The Validation value to negate static prefix func ! ( validation: Self ) -> Self { - .init { value in - !validation.validate(value) - } + .init( + predicate: { value in + do { + try validation.validate(value) + return false + } catch { + return true + } + }, + error: validation.error + ) } - } // MARK: - Validation+And public extension Validation { - /// Performs a logical `AND` (`&&`) operation on two Validations /// - Parameters: /// - lhs: The left-hand side of the operation @@ -29,16 +34,16 @@ public extension Validation { rhs: @autoclosure @escaping () -> Self ) -> Self { .init { value in - lhs.validate(value) && rhs().validate(value) + try lhs.validate(value) + try rhs().validate(value) + return true } } - } // MARK: - Validation+Or public extension Validation { - /// Performs a logical `OR` (`||`) operation on two Validations /// - Parameters: /// - lhs: The left-hand side of the operation @@ -48,8 +53,13 @@ public extension Validation { rhs: @autoclosure @escaping () -> Self ) -> Self { .init { value in - lhs.validate(value) || rhs().validate(value) + do { + try lhs.validate(value) + } catch { + try rhs().validate(value) + } + + return true } } - } diff --git a/Sources/Validation/Validation+Sequence.swift b/Sources/Validation/Validation+Sequence.swift index 8485aa8..1ec04d1 100644 --- a/Sources/Validation/Validation+Sequence.swift +++ b/Sources/Validation/Validation+Sequence.swift @@ -3,26 +3,32 @@ import Foundation // MARK: - Validation+Sequence public extension Validation where Value: Sequence, Value.Element: Equatable { - /// Validation with contains elements /// - Parameter elements: The Elements that should be contained static func contains( - _ elements: Value.Element... + _ elements: Value.Element..., + error: String? = nil ) -> Self { - .init { value in - elements.map(value.contains).contains(true) - } + .init( + predicate: { value in + elements.map(value.contains).contains(true) + }, + error: error + ) } - + /// Returns a Validation indicating whether the initial elements /// of the sequence are the same as the elements in another sequence /// - Parameter elements: The Elements to compare to static func startsWith( - _ elements: Value.Element... + _ elements: Value.Element..., + error: String? = nil ) -> Self { - .init { value in - value.starts(with: elements) - } + .init( + predicate: { value in + value.starts(with: elements) + }, + error: error + ) } - } diff --git a/Sources/Validation/Validation+String.swift b/Sources/Validation/Validation+String.swift index 6a43bfe..c6aabee 100644 --- a/Sources/Validation/Validation+String.swift +++ b/Sources/Validation/Validation+String.swift @@ -3,71 +3,88 @@ import Foundation // MARK: - Validation+StringProtocol public extension Validation where Value: StringProtocol { - /// Validation with contains String /// - Parameters: /// - string: The String that should be contained /// - options: The String ComparisonOptions. Default value `.init` static func contains( _ string: @autoclosure @escaping () -> S, - options: @autoclosure @escaping () -> NSString.CompareOptions = .init() + options: @autoclosure @escaping () -> NSString.CompareOptions = .init(), + error: String? = nil ) -> Self { - .init { value in - value.range(of: string(), options: options()) != nil - } + .init( + predicate: { value in + value.range(of: string(), options: options()) != nil + }, + error: error + ) } /// Validation with has prefix /// - Parameter prefix: The prefix static func hasPrefix( - _ prefix: @autoclosure @escaping () -> S + _ prefix: @autoclosure @escaping () -> S, + error: String? = nil ) -> Self { - .init { value in - value.hasPrefix(prefix()) - } + .init( + predicate: { value in + value.hasPrefix(prefix()) + }, + error: error + ) } /// Validation with has suffix /// - Parameter suffix: The suffix static func hasSuffix( - _ suffix: @autoclosure @escaping () -> S + _ suffix: @autoclosure @escaping () -> S, + error: String? = nil ) -> Self { - .init { value in - value.hasSuffix(suffix()) - } + .init( + predicate: { value in + value.hasSuffix(suffix()) + }, + error: error + ) } - } // MARK: - Validation+String public extension Validation where Value == String { - /// Validation if the String is a subset of a given CharacterSet /// - Parameter characterSet: The CharacterSet static func isSubset( - of characterSet: @autoclosure @escaping () -> CharacterSet + of characterSet: @autoclosure @escaping () -> CharacterSet, + error: String? = nil ) -> Self { - .init { value in - CharacterSet( - charactersIn: value - ) - .isSubset( - of: characterSet() - ) - } + .init( + predicate: { value in + CharacterSet( + charactersIn: value + ) + .isSubset( + of: characterSet() + ) + }, + error: error + ) } /// Validation if the String is numeric /// - Parameter locale: The Locale. Default value `.current` static func isNumeric( - locale: @autoclosure @escaping () -> Locale = .current + locale: @autoclosure @escaping () -> Locale = .current, + error: String? = nil ) -> Self { - .init { value in - let scanner = Scanner(string: value) - scanner.locale = locale() - return scanner.scanDecimal() != nil && scanner.isAtEnd - } + .init( + predicate: { value in + let scanner = Scanner(string: value) + scanner.locale = locale() + return scanner.scanDecimal() != nil && scanner.isAtEnd + }, + error: error + ) } /// Validation if the String matches with a given NSTextCheckingResult CheckingType @@ -76,44 +93,50 @@ public extension Validation where Value == String { /// - validate: An optional closure to validate the NSTextCheckingResult. Default value `nil` static func matches( _ textCheckingResult: @autoclosure @escaping () -> NSTextCheckingResult.CheckingType, - validate: ((NSTextCheckingResult) -> Bool)? = nil + validate: ((NSTextCheckingResult) -> Bool)? = nil, + error: String? = nil ) -> Self { - .init { value in - // Initialize NSDataDetector with link checking type - let detector = try? NSDataDetector( - types: textCheckingResult().rawValue - ) - // Initialize Range from value - let range = NSRange( - value.startIndex.. Self { + self.matches(.link, + validate: { match in + match.url?.scheme == "mailto" + }, + error: error) } /// Validation with RegularExpression @@ -122,15 +145,19 @@ public extension Validation where Value == String { /// - matchingOptions: The NSRegularExpression.MatchingOptions. Default value `.init` static func regularExpression( _ regularExpression: @autoclosure @escaping () -> NSRegularExpression, - matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init() + matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init(), + error: String? = nil ) -> Self { - .init { value in - regularExpression().firstMatch( - in: value, - options: matchingOptions(), - range: .init(value.startIndex..., in: value) - ) != nil - } + .init( + predicate: { value in + regularExpression().firstMatch( + in: value, + options: matchingOptions(), + range: .init(value.startIndex..., in: value) + ) != nil + }, + error: error + ) } /// Validation with RegularExpression Pattern @@ -141,20 +168,31 @@ public extension Validation where Value == String { static func regularExpression( _ pattern: @autoclosure @escaping () -> String, onInvalidPatternValidation: @autoclosure @escaping () -> Validation = .constant(false), - matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init() + matchingOptions: @autoclosure @escaping () -> NSRegularExpression.MatchingOptions = .init(), + error: String? = nil ) -> Self { let regularExpression: NSRegularExpression do { regularExpression = try .init(pattern: pattern()) } catch { return .init { _ in - onInvalidPatternValidation().validate(()) + onInvalidPatternValidation().validateCatched(()) } } return self.regularExpression( regularExpression, - matchingOptions: matchingOptions() + matchingOptions: matchingOptions(), + error: error ) } + @available(iOS 16.0, *) + static func regex(_ regex: Regex, error: String? = nil) -> Self { + .init( + predicate: { value in + value.contains(regex) + }, + error: error + ) + } } diff --git a/Sources/Validation/Validation.swift b/Sources/Validation/Validation.swift index 6525e22..7dfab96 100644 --- a/Sources/Validation/Validation.swift +++ b/Sources/Validation/Validation.swift @@ -4,25 +4,28 @@ import Foundation /// A Validation public struct Validation { - // MARK: Typealias /// The validation predicate typealias representing a `(Value) -> Bool` closure - public typealias Predicate = (Value) -> Bool + public typealias Predicate = (Value) throws -> Bool // MARK: Properties /// The Predicate private let predicate: Predicate + public let error: String? + // MARK: Initializer /// Creates a new instance of `Validation` /// - Parameter predicate: A closure that takes a value and returns a Boolean value if the passed value is valid public init( - predicate: @escaping Predicate + predicate: @escaping Predicate, + error: String? = nil ) { self.predicate = predicate + self.error = error } /// Creates a new instance of `Validation` @@ -33,24 +36,46 @@ public struct Validation { _ validation: Validation, isNilValid: @autoclosure @escaping () -> Bool = false ) where WrappedValue? == Value { - self.init { value in - value.flatMap(validation.validate) ?? isNilValid() - } + self.init( + predicate: { value in + value.flatMap { v in + do { + try validation.validate(v) + return true + } catch { + return false + } + + } ?? isNilValid() + }, + error: validation.error + ) } - } // MARK: - Validate public extension Validation { - - /// Validates a value and returns a Boolean value wether the value is valid or invalid - /// - Parameter value: The value that should be validated - /// - Returns: A Boolen value wether the value is valid or invalid func validate( _ value: Value - ) -> Bool { - self.predicate(value) + ) throws { + let isValid = try predicate(value) + + if !isValid { + throw error ?? "Not valid" + } } + func validateCatched( + _ value: Value + ) -> Bool { + do { + try validate(value) + return true + } catch { + return false + } + } } + +extension String: Error {}