From b8f872e2750c36d769ded788a0f60fd70ff4eabc Mon Sep 17 00:00:00 2001 From: Soumya Ranjan Mahunt Date: Sun, 6 Nov 2022 12:19:43 +0530 Subject: [PATCH] feat: add data validation DSL --- Sources/ValidatableKit/Results/Invalid.swift | 23 +++ Sources/ValidatableKit/Results/Nested.swift | 23 +++ Sources/ValidatableKit/Results/Skipped.swift | 19 ++ .../Results/ValidatorResult.swift | 37 ++++ Sources/ValidatableKit/Validatable.swift | 41 ++++ Sources/ValidatableKit/Validation.swift | 54 +++++ Sources/ValidatableKit/Validations.swift | 129 ++++++++++++ Sources/ValidatableKit/Validator.swift | 69 +++++++ Sources/ValidatableKit/Validators/Bool.swift | 38 ++++ Sources/ValidatableKit/Validators/Case.swift | 56 +++++ .../Validators/CharacterSet.swift | 193 ++++++++++++++++++ Sources/ValidatableKit/Validators/Email.swift | 77 +++++++ Sources/ValidatableKit/Validators/Empty.swift | 37 ++++ Sources/ValidatableKit/Validators/Eql.swift | 40 ++++ Sources/ValidatableKit/Validators/In.swift | 40 ++++ Sources/ValidatableKit/Validators/Nil.swift | 38 ++++ .../Validators/Operators/And.swift | 48 +++++ .../Validators/Operators/NilIgnoring.swift | 55 +++++ .../Validators/Operators/Not.swift | 28 +++ .../Validators/Operators/Or.swift | 51 +++++ Sources/ValidatableKit/Validators/Regex.swift | 99 +++++++++ Sources/ValidatableKit/Validators/URL.swift | 45 ++++ 22 files changed, 1240 insertions(+) create mode 100644 Sources/ValidatableKit/Results/Invalid.swift create mode 100644 Sources/ValidatableKit/Results/Nested.swift create mode 100644 Sources/ValidatableKit/Results/Skipped.swift create mode 100644 Sources/ValidatableKit/Results/ValidatorResult.swift create mode 100644 Sources/ValidatableKit/Validatable.swift create mode 100644 Sources/ValidatableKit/Validation.swift create mode 100644 Sources/ValidatableKit/Validations.swift create mode 100644 Sources/ValidatableKit/Validator.swift create mode 100644 Sources/ValidatableKit/Validators/Bool.swift create mode 100644 Sources/ValidatableKit/Validators/Case.swift create mode 100644 Sources/ValidatableKit/Validators/CharacterSet.swift create mode 100644 Sources/ValidatableKit/Validators/Email.swift create mode 100644 Sources/ValidatableKit/Validators/Empty.swift create mode 100644 Sources/ValidatableKit/Validators/Eql.swift create mode 100644 Sources/ValidatableKit/Validators/In.swift create mode 100644 Sources/ValidatableKit/Validators/Nil.swift create mode 100644 Sources/ValidatableKit/Validators/Operators/And.swift create mode 100644 Sources/ValidatableKit/Validators/Operators/NilIgnoring.swift create mode 100644 Sources/ValidatableKit/Validators/Operators/Not.swift create mode 100644 Sources/ValidatableKit/Validators/Operators/Or.swift create mode 100644 Sources/ValidatableKit/Validators/Regex.swift create mode 100644 Sources/ValidatableKit/Validators/URL.swift diff --git a/Sources/ValidatableKit/Results/Invalid.swift b/Sources/ValidatableKit/Results/Invalid.swift new file mode 100644 index 0000000..3289e5a --- /dev/null +++ b/Sources/ValidatableKit/Results/Invalid.swift @@ -0,0 +1,23 @@ +public extension ValidatorResults { + /// `ValidatorResult` representing + /// validation has failed. + struct Invalid { + public let reason: String + } +} + +extension ValidatorResults.Invalid: ValidatorResult { + public var isFailure: Bool { + true + } + + public var successDescriptions: [String] { + [] + } + + public var failureDescriptions: [String] { + [ + "is invalid: \(self.reason)" + ] + } +} diff --git a/Sources/ValidatableKit/Results/Nested.swift b/Sources/ValidatableKit/Results/Nested.swift new file mode 100644 index 0000000..80450f5 --- /dev/null +++ b/Sources/ValidatableKit/Results/Nested.swift @@ -0,0 +1,23 @@ +public extension ValidatorResults { + /// `ValidatorResult` representing + /// a group of `ValidatorResult`s. + struct Nested { + public let results: [ValidatorResult] + } +} + +extension ValidatorResults.Nested: ValidatorResult { + public var isFailure: Bool { + self.results.first { $0.isFailure } != nil + } + + public var successDescriptions: [String] { + self.results.lazy.filter { !$0.isFailure } + .flatMap { $0.successDescriptions } + } + + public var failureDescriptions: [String] { + self.results.lazy.filter { $0.isFailure } + .flatMap { $0.failureDescriptions } + } +} diff --git a/Sources/ValidatableKit/Results/Skipped.swift b/Sources/ValidatableKit/Results/Skipped.swift new file mode 100644 index 0000000..032d378 --- /dev/null +++ b/Sources/ValidatableKit/Results/Skipped.swift @@ -0,0 +1,19 @@ +public extension ValidatorResults { + /// `ValidatorResult` representing + /// validation is skipped by validator. + struct Skipped {} +} + +extension ValidatorResults.Skipped: ValidatorResult { + public var isFailure: Bool { + false + } + + public var successDescriptions: [String] { + [] + } + + public var failureDescriptions: [String] { + [] + } +} diff --git a/Sources/ValidatableKit/Results/ValidatorResult.swift b/Sources/ValidatableKit/Results/ValidatorResult.swift new file mode 100644 index 0000000..d891a13 --- /dev/null +++ b/Sources/ValidatableKit/Results/ValidatorResult.swift @@ -0,0 +1,37 @@ +/// A name space type containing all default +/// ``ValidatorResult`` provided by this library. +public struct ValidatorResults {} + +/// A type representing result of validations +/// performed by ``Validator``. +public protocol ValidatorResult { + /// Whether validation succedded or failed. + var isFailure: Bool { get } + /// Descriptions to use in the event of validation succeeds. + var successDescriptions: [String] { get } + /// Descriptions to use in the event of validation fails. + var failureDescriptions: [String] { get } +} + +/// An error type representing validation failure. +public struct ValidationError: Error, CustomStringConvertible { + /// The actual result of validation. + public let result: ValidatorResult + + /// A textual representation of this error. + public var description: String { + return result.failureDescriptions.joined(separator: "\n") + } +} + +internal extension ValidatorResult { + /// The result success and failure descriptions combined. + var resultDescription: String { + var desc: [String] = [] + desc.append("→ Successes") + desc += self.failureDescriptions.indented() + desc.append("→ Failures") + desc += self.failureDescriptions.indented() + return desc.joined(separator: "\n") + } +} diff --git a/Sources/ValidatableKit/Validatable.swift b/Sources/ValidatableKit/Validatable.swift new file mode 100644 index 0000000..a6db417 --- /dev/null +++ b/Sources/ValidatableKit/Validatable.swift @@ -0,0 +1,41 @@ +/// A type capable of being validated. +/// +/// While confirming a ``Validator`` needs to be provided for perorming +/// validation and conformance adds a throwing ``validate()`` method. +/// +/// ```swift +/// struct User: Validatable { +/// let name: String +/// let email: String +/// let age: Int +/// +/// var validator: Validator { +/// return Validator +/// .name(!.isEmpty, .alphanumeric) +/// .email(.isEmail) +/// } +/// } +/// ``` +public protocol Validatable { + /// The ``Validator`` used to validate data. + var validator: Validator { get } +} + +public extension Validatable { + /// Performs validation on current data + /// using provided ``validator``. + /// + /// - Throws: ``ValidationError`` if validation fails. + func validate() throws { + let result = self.validatorResult() + guard result.isFailure else { return } + throw ValidationError(result: result) + } + + /// Performs validation on current data and provides result. + /// + /// - Returns: The result of validation. + internal func validatorResult() -> ValidatorResult { + return self.validator.validate(self) + } +} diff --git a/Sources/ValidatableKit/Validation.swift b/Sources/ValidatableKit/Validation.swift new file mode 100644 index 0000000..578a616 --- /dev/null +++ b/Sources/ValidatableKit/Validation.swift @@ -0,0 +1,54 @@ +/// Adds ``Validator``s for property type `U` of parent type `T`. +/// +/// Use the `@dynamicCallable` feature to provide ``Validator``s +/// to add for the already provided property that the validation was created with. +@dynamicCallable +public struct Validation { + /// The `KeyPath` to property for which current validation added. + internal let keyPath: KeyPath + /// The validations store for all the property based + /// validations of parent type. + internal var parent: Validations + + /// Creates a new validation for the provided propety. + /// + /// - Parameters: + /// - keyPath: The `KeyPath` for the property. + /// - parent: The store for all the property based validations of parent type. + /// + /// - Returns: The newly created validation. + internal init(keyPath: KeyPath, parent: Validations) { + self.keyPath = keyPath + self.parent = parent + } + + /// Adds validators for a property of parent type `T`. + /// + /// When validation is performed on parent type data, + /// properties will be validated with the provided validators. + /// + /// - Parameter args: The validators to add. + /// - Returns: The ``Validations`` object that stores + /// all property validation of parent type `T`. + public func dynamicallyCall( + withArguments args: [Validator] + ) -> Validations { + for arg in args { parent.addValidator(at: keyPath, arg) } + return parent + } + + /// Adds validators for a property of parent type `T`. + /// + /// When validation is performed on parent type data, + /// properties will be validated with the provided validators. + /// + /// - Parameter args: The validators to add. + /// - Returns: The ``Validator`` of parent type `T` + /// containing all the provided validations. + public func dynamicallyCall( + withArguments args: [Validator] + ) -> Validator { + for arg in args { parent.addValidator(at: keyPath, arg) } + return parent.validator + } +} diff --git a/Sources/ValidatableKit/Validations.swift b/Sources/ValidatableKit/Validations.swift new file mode 100644 index 0000000..2b4cae0 --- /dev/null +++ b/Sources/ValidatableKit/Validations.swift @@ -0,0 +1,129 @@ +/// Stores property validations of data type `T` specialized with. +/// +/// Use the `@dynamicMemberLookup` +/// feature to add validations based on property. +@dynamicMemberLookup +public class Validations { + /// `ValidatorResult` of a validator that validates + /// all the properties and groups them with their `KeyPath`. + public struct Property: ValidatorResult { + /// The result of property validations associated with property `KeyPath`. + public internal(set) var results: [PartialKeyPath: ValidatorResult] = + [:] + + public var isFailure: Bool { + return self.results.first(where: \.value.isFailure) != nil + } + + public var successDescriptions: [String] { + var desc: [String] = [] + for (keyPath, result) in self.results where !result.isFailure { + desc.append("→ \(keyPath)") + desc += result.successDescriptions.indented() + } + return desc + } + + public var failureDescriptions: [String] { + var desc: [String] = [] + for (keyPath, result) in self.results where result.isFailure { + desc.append("→ \(keyPath)") + desc += result.failureDescriptions.indented() + } + return desc + } + } + + /// Stores all property based validations associated with the property `KeyPath`. + private var storage: [PartialKeyPath: [(T) -> ValidatorResult]] = [:] + + /// The parent type `T` data validator + /// with all the property based validations. + internal var validator: Validator { + return .init { self.validate($0) } + } + + /// Stores provided property validator and `KeyPath`. + /// + /// - Parameters: + /// - keyPath: The `KeyPath` for the property. + /// - validator: The validator to add. + @usableFromInline + internal func addValidator( + at keyPath: KeyPath, + _ validator: Validator + ) { + let validation: (T) -> ValidatorResult = { + let data = $0[keyPath: keyPath] + return validator.validate(data) + } + + if storage[keyPath] == nil { + storage[keyPath] = [validation] + } else { + storage[keyPath]!.append(validation) + } + } + + /// Stores the validator associated with provided property `KeyPath`. + /// + /// - Parameter keyPath: The `KeyPath` for the property. + @inlinable + internal func addValidator(at keyPath: KeyPath) { + addValidator(at: keyPath, .init { $0.validator.validate($0) }) + } + + /// Validates properties of data of type `T` with stored validators + /// and returns the result. + /// + /// - Parameter data: The data to validate. + /// - Returns: The result of validation. + internal func validate(_ data: T) -> ValidatorResult { + guard !storage.isEmpty else { return ValidatorResults.Skipped() } + var result = Property() + for (keyPath, validations) in storage where !validations.isEmpty { + let results = validations.map { $0(data) } + result.results[keyPath] = ValidatorResults.Nested(results: results) + } + return result + } + + /// Exposes property of the specialized data type `T` to add validation on. + /// + /// Provide validator(s) to the returned ``Validation`` + /// to validate that property with the validators. + /// + /// - Parameter keyPath: The `KeyPath` for the property. + /// - Returns: ``Validation`` for the property. + public subscript( + dynamicMember keyPath: KeyPath + ) -> Validation { + let validation = Validation(keyPath: keyPath, parent: self) + return validation + } + + /// Exposes property of the specialized data type `T` to add validation on. + /// + /// Adds the provided property ``Validatable/validator`` + /// along with any provided validator(s) to the returned + /// ``Validation`` to validate that property. + /// + /// - Parameter keyPath: The `KeyPath` for the property. + /// - Returns: ``Validation`` for the property. + public subscript( + dynamicMember keyPath: KeyPath + ) -> Validation { + addValidator(at: keyPath) + let validation = Validation(keyPath: keyPath, parent: self) + return validation + } +} + +internal extension Array where Element == String { + /// Indents provided array of `String`. + /// + /// - Returns: The indented `String`s. + func indented() -> [String] { + return self.map { " " + $0 } + } +} diff --git a/Sources/ValidatableKit/Validator.swift b/Sources/ValidatableKit/Validator.swift new file mode 100644 index 0000000..6d5e182 --- /dev/null +++ b/Sources/ValidatableKit/Validator.swift @@ -0,0 +1,69 @@ +import Foundation + +/// Validates data of type `T` specialized with. +/// +/// Use the initializer ``init(validate:)`` +/// to provide validation action or use `@dynamicMemberLookup` +/// feature to add validations based on property. +@dynamicMemberLookup +public struct Validator { + /// Validates data of type `T` specialized with and returns the result. + /// + /// Use the ``Validatable/validate()`` method instead + /// to perform validation if actual validation result is not needed. + /// + /// - Parameter data: The data to validate. + /// - Returns: The result of validation. + public let validate: (_ data: T) -> ValidatorResult + + /// Creates a new validator with an action that performs validation. + /// + /// Use this initializer to create different validators for basic data types, + /// i.e. `Int`, `String`, and combine them to perform complex validations. + /// + /// - Parameter validate: The action that validates data and returns result. + /// - Returns: The newly created validator. + public init(validate: @escaping (_ data: T) -> ValidatorResult) { + self.validate = validate + } + + /// A specialvalidator that skips performing any validation. + /// + /// The validation result for this validator always returns + /// ``ValidatorResults/Skipped``. + public static var skip: Self { + return .init { _ in return ValidatorResults.Skipped() } + } + + /// Exposes property of the specialized data type `T` to add validation on. + /// + /// Provide validator(s) to the returned ``Validation`` + /// to validate that property with the validators. + /// + /// - Parameter keyPath: The `KeyPath` for the property. + /// - Returns: ``Validation`` for the property. + public static subscript( + dynamicMember keyPath: KeyPath + ) -> Validation { + let validations = Validations() + let validation = Validation(keyPath: keyPath, parent: validations) + return validation + } + + /// Exposes property of the specialized data type `T` to add validation on. + /// + /// Adds the provided property ``Validatable/validator`` + /// along with any provided validator(s) to the returned + /// ``Validation`` to validate that property. + /// + /// - Parameter keyPath: The `KeyPath` for the property. + /// - Returns: ``Validation`` for the property. + public static subscript( + dynamicMember keyPath: KeyPath + ) -> Validation { + let validations = Validations() + validations.addValidator(at: keyPath) + let validation = Validation(keyPath: keyPath, parent: validations) + return validation + } +} diff --git a/Sources/ValidatableKit/Validators/Bool.swift b/Sources/ValidatableKit/Validators/Bool.swift new file mode 100644 index 0000000..cbc1598 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Bool.swift @@ -0,0 +1,38 @@ +public extension Validator where T == Bool { + /// Validates whether a `Bool` is `true`. + static var isTrue: Self { + .init { + guard $0 else { return ValidatorResults.Boolean(isTrue: false) } + return ValidatorResults.Boolean(isTrue: true) + } + } + + /// Validates whether a `Bool` is `false`. + static var isFalse: Self { !isTrue } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates a boolean value. + public struct Boolean { + /// The input is `true`. + public let isTrue: Bool + } +} + +extension ValidatorResults.Boolean: ValidatorResult { + public var isFailure: Bool { + !self.isTrue + } + + public var successDescriptions: [String] { + [ + "is True" + ] + } + + public var failureDescriptions: [String] { + [ + "is False" + ] + } +} diff --git a/Sources/ValidatableKit/Validators/Case.swift b/Sources/ValidatableKit/Validators/Case.swift new file mode 100644 index 0000000..6ddb6ad --- /dev/null +++ b/Sources/ValidatableKit/Validators/Case.swift @@ -0,0 +1,56 @@ +public extension Validator where T: CustomStringConvertible { + /// Validates that the data can be converted to a value of an enum type with iterable cases. + static func `case`( + of enum: E.Type + ) -> Self where E.RawValue == T { + .init { + ValidatorResults.Case(enumType: E.self, rawValue: $0) + } + } + +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates + /// whether the data can be represented as a specific Enum case. + public struct Case< + T: CustomStringConvertible, + E: RawRepresentable & CaseIterable + > where E.RawValue == T { + /// The type of the enum to check. + public let enumType: E.Type + /// The raw value that would be tested against the enum type. + public let rawValue: T + } +} + +extension ValidatorResults.Case: ValidatorResult { + public var isFailure: Bool { + return enumType.init(rawValue: rawValue) == nil + } + + public var successDescriptions: [String] { + makeDescription(not: false) + } + + public var failureDescriptions: [String] { + makeDescription(not: true) + } + + func makeDescription(not: Bool) -> [String] { + let items = E.allCases.map { "\($0.rawValue)" } + let description: String + switch items.count { + case 1: + description = items[0].description + case 2: + description = "\(items[0].description) or \(items[1].description)" + default: + let first = items[0..<(items.count - 1)] + .map { $0.description }.joined(separator: ", ") + let last = items[items.count - 1].description + description = "\(first), or \(last)" + } + return ["is\(not ? " not" : "") \(description)"] + } +} diff --git a/Sources/ValidatableKit/Validators/CharacterSet.swift b/Sources/ValidatableKit/Validators/CharacterSet.swift new file mode 100644 index 0000000..1be9eac --- /dev/null +++ b/Sources/ValidatableKit/Validators/CharacterSet.swift @@ -0,0 +1,193 @@ +import struct Foundation.CharacterSet + +public extension Validator where T: StringProtocol { + /// Validates that all characters in a `String` are ASCII (bytes 0..<128). + static var ascii: Self { + .containsOnly(.ascii) + } + + /// Validates that all characters in a `String` are alphanumeric (a-z,A-Z,0-9). + static var alphanumeric: Self { + .containsOnly(.alphanumerics) + } + + /// Validates that all characters in a `String` are in the supplied `CharacterSet`. + static func containsOnly(_ characterSet: Foundation.CharacterSet) -> Self { + .init { + ValidatorResults.CharacterSet( + string: $0, + characterSet: characterSet + ) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates that a `String` + /// contains characters in a given `CharacterSet`. + public struct CharacterSet { + /// The validated string. + public let string: S + + /// The set of characters the input is allowed to contain. + public let characterSet: Foundation.CharacterSet + + /// On validation failure, the first substring of the input with + /// characters not contained in `characterSet`. + var invalidRange: Swift.Range? { + self.string.rangeOfCharacter(from: self.characterSet.inverted) + } + + public var invalidSlice: String? { + self.invalidRange.flatMap { self.string[$0] } + .map { .init($0) } + } + + var allowedCharacterString: String { + self.characterSet.traits.joined(separator: ", ") + } + } +} + +extension ValidatorResults.CharacterSet: ValidatorResult { + public var isFailure: Bool { + self.invalidRange != nil + } + + public var successDescriptions: [String] { + [ + "contains only \(self.allowedCharacterString)" + ] + } + + public var failureDescriptions: [String] { + guard + let slice = invalidSlice + else { + return [ + "(allowed: \(self.allowedCharacterString))" + ] + } + return [ + "contains '\(slice)' (allowed: \(self.allowedCharacterString))" + ] + } +} + +extension Validator where T == [String] { + /// Validates that all characters in elements of a `[String]` are ASCII (bytes 0..<128). + public static var ascii: Validator { + .containsOnly(.ascii) + } + + /// Validates that all characters in elements of a `[String]` are alphanumeric (a-z,A-Z,0-9). + public static var alphanumeric: Validator { + .containsOnly(.alphanumerics) + } + + /// Validates that all characters in elements of a + /// `[String]` are in the supplied `CharacterSet`. + public static func containsOnly( + _ characterSet: Foundation.CharacterSet + ) -> Validator { + .init { + ValidatorResults.CollectionCharacterSet( + strings: $0, + characterSet: characterSet + ) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates + /// that all elements of a `[String]` contain characters + /// in a given `CharacterSet`. + public struct CollectionCharacterSet { + /// The validated string. + public let strings: [String] + + /// The set of characters the input is allowed to contain. + public let characterSet: Foundation.CharacterSet + + /// On validation failure, the first substring of the input with characters + /// not contained in `characterSet`. + var invalidRanges: [(Int, Swift.Range)?] { + return self.strings.enumerated().compactMap { + if let range = $1.rangeOfCharacter( + from: self.characterSet.inverted + ) { + return ($0, range) + } + return nil + } + } + + var allowedCharacterString: String { + self.characterSet.traits.joined(separator: ", ") + } + } +} + +extension ValidatorResults.CollectionCharacterSet: ValidatorResult { + public var isFailure: Bool { + !self.invalidRanges.isEmpty + } + + public var successDescriptions: [String] { + [ + "contains only \(self.allowedCharacterString)" + ] + } + + public var failureDescriptions: [String] { + let disallowedCharacters = self.invalidRanges.compactMap { $0 } + .map { (invalidSlice) in + "string at index \(invalidSlice.0) contains " + + "'\(String(self.strings[invalidSlice.0][invalidSlice.1]))'" + } + return [ + disallowedCharacters.joined(separator: ", ") + + "(allowed: \(self.allowedCharacterString))" + ] + } +} + +/// Unions two character sets. +/// +/// .containsOnly(.alphanumerics + .whitespaces) +/// +public func + (lhs: CharacterSet, rhs: CharacterSet) -> CharacterSet { + lhs.union(rhs) +} + +private extension CharacterSet { + /// ASCII (byte 0..<128) character set. + static var ascii: CharacterSet { + .init((0..<128).map(Unicode.Scalar.init)) + } + + /// Returns an array of strings describing the contents of this `CharacterSet`. + var traits: [String] { + var desc: [String] = [] + if isSuperset(of: .newlines) { + desc.append("newlines") + } + if isSuperset(of: .whitespaces) { + desc.append("whitespace") + } + if isSuperset(of: .ascii) { + desc.append("ASCII") + } + if isSuperset(of: .capitalizedLetters) { + desc.append("A-Z") + } + if isSuperset(of: .lowercaseLetters) { + desc.append("a-z") + } + if isSuperset(of: .decimalDigits) { + desc.append("0-9") + } + return desc + } +} diff --git a/Sources/ValidatableKit/Validators/Email.swift b/Sources/ValidatableKit/Validators/Email.swift new file mode 100644 index 0000000..14590db --- /dev/null +++ b/Sources/ValidatableKit/Validators/Email.swift @@ -0,0 +1,77 @@ +public extension Validator where T: StringProtocol { + /// Validates whether a `String` is a valid email address. + static var isEmail: Self { + .init { + guard + let range = $0.range(of: regex, options: [.regularExpression]), + range.lowerBound == $0.startIndex + && range.upperBound == $0.endIndex, + // The numbers beneath here are as defined by https://emailregex.com/email-validation-summary/ + $0.count <= 320, // total length + $0.split(separator: "@")[0].count <= 64 // length before `@` + else { + return ValidatorResults.Email(isValidEmail: false) + } + return ValidatorResults.Email(isValidEmail: true) + } + } + + /// Validates whether a `String` is a valid international email address. + static var isInternationalEmail: Validator { + .init { + guard + let range = $0.range( + of: regexInternationalEmail, options: [.regularExpression]), + range.lowerBound == $0.startIndex + && range.upperBound == $0.endIndex, + // The numbers beneath here are as defined by https://emailregex.com/email-validation-summary/ + $0.count <= 320, // total length + $0.split(separator: "@")[0].count <= 64 // length before `@` + else { + return ValidatorResults.Email(isValidEmail: false) + } + return ValidatorResults.Email(isValidEmail: true) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates whether a `String` is a valid email address. + public struct Email { + /// The input is a valid email address + public let isValidEmail: Bool + } +} + +extension ValidatorResults.Email: ValidatorResult { + public var isFailure: Bool { + !self.isValidEmail + } + + public var successDescriptions: [String] { + [ + "is a valid email address" + ] + } + + public var failureDescriptions: [String] { + [ + "is not a valid email address" + ] + } +} + +// FIXME: this regex is too strict with capitalization of the domain part +private let regex: String = """ + (?:[a-zA-Z0-9!#$%\\&‘*+/=?\\^_`{|}~-]+(?:\\.[a-zA-Z0-9!#$%\\&'*+/=?\\^_`{|}\ + ~-]+)*|\"(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\\x23-\\x5b\\x5d-\\\ + x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])*\")@(?:(?:[a-z0-9](?:[a-\ + z0-9-]*[a-z0-9])?\\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?|\\[(?:(?:25[0-5\ + ]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-\ + 9][0-9]?|[a-z0-9-]*[a-z0-9]:(?:[\\x01-\\x08\\x0b\\x0c\\x0e-\\x1f\\x21\ + -\\x5a\\x53-\\x7f]|\\\\[\\x01-\\x09\\x0b\\x0c\\x0e-\\x7f])+)\\]) + """ + +private let regexInternationalEmail: String = """ + ^(?!\\.)((?!.*\\.{2})[a-zA-Z0-9\\u0080-\\u00FF\\u0100-\\u017F\\u0180-\\u024F\\u0250-\\u02AF\\u0300-\\u036F\\u0370-\\u03FF\\u0400-\\u04FF\\u0500-\\u052F\\u0530-\\u058F\\u0590-\\u05FF\\u0600-\\u06FF\\u0700-\\u074F\\u0750-\\u077F\\u0780-\\u07BF\\u07C0-\\u07FF\\u0900-\\u097F\\u0980-\\u09FF\\u0A00-\\u0A7F\\u0A80-\\u0AFF\\u0B00-\\u0B7F\\u0B80-\\u0BFF\\u0C00-\\u0C7F\\u0C80-\\u0CFF\\u0D00-\\u0D7F\\u0D80-\\u0DFF\\u0E00-\\u0E7F\\u0E80-\\u0EFF\\u0F00-\\u0FFF\\u1000-\\u109F\\u10A0-\\u10FF\\u1100-\\u11FF\\u1200-\\u137F\\u1380-\\u139F\\u13A0-\\u13FF\\u1400-\\u167F\\u1680-\\u169F\\u16A0-\\u16FF\\u1700-\\u171F\\u1720-\\u173F\\u1740-\\u175F\\u1760-\\u177F\\u1780-\\u17FF\\u1800-\\u18AF\\u1900-\\u194F\\u1950-\\u197F\\u1980-\\u19DF\\u19E0-\\u19FF\\u1A00-\\u1A1F\\u1B00-\\u1B7F\\u1D00-\\u1D7F\\u1D80-\\u1DBF\\u1DC0-\\u1DFF\\u1E00-\\u1EFF\\u1F00-\\u1FFFu20D0-\\u20FF\\u2100-\\u214F\\u2C00-\\u2C5F\\u2C60-\\u2C7F\\u2C80-\\u2CFF\\u2D00-\\u2D2F\\u2D30-\\u2D7F\\u2D80-\\u2DDF\\u2F00-\\u2FDF\\u2FF0-\\u2FFF\\u3040-\\u309F\\u30A0-\\u30FF\\u3100-\\u312F\\u3130-\\u318F\\u3190-\\u319F\\u31C0-\\u31EF\\u31F0-\\u31FF\\u3200-\\u32FF\\u3300-\\u33FF\\u3400-\\u4DBF\\u4DC0-\\u4DFF\\u4E00-\\u9FFF\\uA000-\\uA48F\\uA490-\\uA4CF\\uA700-\\uA71F\\uA800-\\uA82F\\uA840-\\uA87F\\uAC00-\\uD7AF\\uF900-\\uFAFF\\.!#$%&'*+-/=?^_`{|}~\\-\\d]+)@(?!\\.)([a-zA-Z0-9\\u0080-\\u00FF\\u0100-\\u017F\\u0180-\\u024F\\u0250-\\u02AF\\u0300-\\u036F\\u0370-\\u03FF\\u0400-\\u04FF\\u0500-\\u052F\\u0530-\\u058F\\u0590-\\u05FF\\u0600-\\u06FF\\u0700-\\u074F\\u0750-\\u077F\\u0780-\\u07BF\\u07C0-\\u07FF\\u0900-\\u097F\\u0980-\\u09FF\\u0A00-\\u0A7F\\u0A80-\\u0AFF\\u0B00-\\u0B7F\\u0B80-\\u0BFF\\u0C00-\\u0C7F\\u0C80-\\u0CFF\\u0D00-\\u0D7F\\u0D80-\\u0DFF\\u0E00-\\u0E7F\\u0E80-\\u0EFF\\u0F00-\\u0FFF\\u1000-\\u109F\\u10A0-\\u10FF\\u1100-\\u11FF\\u1200-\\u137F\\u1380-\\u139F\\u13A0-\\u13FF\\u1400-\\u167F\\u1680-\\u169F\\u16A0-\\u16FF\\u1700-\\u171F\\u1720-\\u173F\\u1740-\\u175F\\u1760-\\u177F\\u1780-\\u17FF\\u1800-\\u18AF\\u1900-\\u194F\\u1950-\\u197F\\u1980-\\u19DF\\u19E0-\\u19FF\\u1A00-\\u1A1F\\u1B00-\\u1B7F\\u1D00-\\u1D7F\\u1D80-\\u1DBF\\u1DC0-\\u1DFF\\u1E00-\\u1EFF\\u1F00-\\u1FFF\\u20D0-\\u20FF\\u2100-\\u214F\\u2C00-\\u2C5F\\u2C60-\\u2C7F\\u2C80-\\u2CFF\\u2D00-\\u2D2F\\u2D30-\\u2D7F\\u2D80-\\u2DDF\\u2F00-\\u2FDF\\u2FF0-\\u2FFF\\u3040-\\u309F\\u30A0-\\u30FF\\u3100-\\u312F\\u3130-\\u318F\\u3190-\\u319F\\u31C0-\\u31EF\\u31F0-\\u31FF\\u3200-\\u32FF\\u3300-\\u33FF\\u3400-\\u4DBF\\u4DC0-\\u4DFF\\u4E00-\\u9FFF\\uA000-\\uA48F\\uA490-\\uA4CF\\uA700-\\uA71F\\uA800-\\uA82F\\uA840-\\uA87F\\uAC00-\\uD7AF\\uF900-\\uFAFF\\-\\.\\d]+)((\\.([a-zA-Z\\u0080-\\u00FF\\u0100-\\u017F\\u0180-\\u024F\\u0250-\\u02AF\\u0300-\\u036F\\u0370-\\u03FF\\u0400-\\u04FF\\u0500-\\u052F\\u0530-\\u058F\\u0590-\\u05FF\\u0600-\\u06FF\\u0700-\\u074F\\u0750-\\u077F\\u0780-\\u07BF\\u07C0-\\u07FF\\u0900-\\u097F\\u0980-\\u09FF\\u0A00-\\u0A7F\\u0A80-\\u0AFF\\u0B00-\\u0B7F\\u0B80-\\u0BFF\\u0C00-\\u0C7F\\u0C80-\\u0CFF\\u0D00-\\u0D7F\\u0D80-\\u0DFF\\u0E00-\\u0E7F\\u0E80-\\u0EFF\\u0F00-\\u0FFF\\u1000-\\u109F\\u10A0-\\u10FF\\u1100-\\u11FF\\u1200-\\u137F\\u1380-\\u139F\\u13A0-\\u13FF\\u1400-\\u167F\\u1680-\\u169F\\u16A0-\\u16FF\\u1700-\\u171F\\u1720-\\u173F\\u1740-\\u175F\\u1760-\\u177F\\u1780-\\u17FF\\u1800-\\u18AF\\u1900-\\u194F\\u1950-\\u197F\\u1980-\\u19DF\\u19E0-\\u19FF\\u1A00-\\u1A1F\\u1B00-\\u1B7F\\u1D00-\\u1D7F\\u1D80-\\u1DBF\\u1DC0-\\u1DFF\\u1E00-\\u1EFF\\u1F00-\\u1FFF\\u20D0-\\u20FF\\u2100-\\u214F\\u2C00-\\u2C5F\\u2C60-\\u2C7F\\u2C80-\\u2CFF\\u2D00-\\u2D2F\\u2D30-\\u2D7F\\u2D80-\\u2DDF\\u2F00-\\u2FDF\\u2FF0-\\u2FFF\\u3040-\\u309F\\u30A0-\\u30FF\\u3100-\\u312F\\u3130-\\u318F\\u3190-\\u319F\\u31C0-\\u31EF\\u31F0-\\u31FF\\u3200-\\u32FF\\u3300-\\u33FF\\u3400-\\u4DBF\\u4DC0-\\u4DFF\\u4E00-\\u9FFF\\uA000-\\uA48F\\uA490-\\uA4CF\\uA700-\\uA71F\\uA800-\\uA82F\\uA840-\\uA87F\\uAC00-\\uD7AF\\uF900-\\uFAFF]){2,63})+)$ + """ diff --git a/Sources/ValidatableKit/Validators/Empty.swift b/Sources/ValidatableKit/Validators/Empty.swift new file mode 100644 index 0000000..4139fe3 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Empty.swift @@ -0,0 +1,37 @@ +public extension Validator where T: Collection { + /// Validates that the data is empty. + /// You can also check a non empty + /// state by negating this validator: `!.empty`. + static var isEmpty: Self { + .init { + ValidatorResults.Empty(isEmpty: $0.isEmpty) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator + /// that validates whether the data is empty. + public struct Empty { + /// The input is empty. + public let isEmpty: Bool + } +} + +extension ValidatorResults.Empty: ValidatorResult { + public var isFailure: Bool { + !self.isEmpty + } + + public var successDescriptions: [String] { + [ + "is empty" + ] + } + + public var failureDescriptions: [String] { + [ + "is not empty" + ] + } +} diff --git a/Sources/ValidatableKit/Validators/Eql.swift b/Sources/ValidatableKit/Validators/Eql.swift new file mode 100644 index 0000000..764204f --- /dev/null +++ b/Sources/ValidatableKit/Validators/Eql.swift @@ -0,0 +1,40 @@ +public extension Validator where T: Equatable { + /// Validates whether current value is equal to provided value. + /// + /// - Parameter value: The value to compare to. + /// - Returns: The validator validating equality. + static func eql(to value: T) -> Self { + .init { + guard $0 == value else { + return ValidatorResults.Equal(isEql: false) + } + return ValidatorResults.Equal(isEql: true) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates whether values are equal. + public struct Equal { + /// The values are equal. + public let isEql: Bool + } +} + +extension ValidatorResults.Equal: ValidatorResult { + public var isFailure: Bool { + !self.isEql + } + + public var successDescriptions: [String] { + [ + "is Equal" + ] + } + + public var failureDescriptions: [String] { + [ + "is not Equal" + ] + } +} diff --git a/Sources/ValidatableKit/Validators/In.swift b/Sources/ValidatableKit/Validators/In.swift new file mode 100644 index 0000000..ab26f5c --- /dev/null +++ b/Sources/ValidatableKit/Validators/In.swift @@ -0,0 +1,40 @@ +public extension Validator where T: Equatable { + /// Validates whether an item is contained in the supplied sequence. + /// + /// - Parameter sequence: The sequence to check membership for. + /// - Returns: The validator validating whether an item is contained in the supplied sequence. + static func `in`(_ sequence: S) -> Self where S.Element == T { + .init { + return ValidatorResults.In(items: sequence, item: $0) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates whether + /// an item is contained in the supplied sequence. + public struct In where S: Sequence, S.Element: Equatable { + /// The supplied sequence. + public let items: S + /// The item checked. + public let item: S.Element + } +} + +extension ValidatorResults.In: ValidatorResult { + public var isFailure: Bool { + !items.contains(item) + } + + public var successDescriptions: [String] { + [ + "present in \(self.items)" + ] + } + + public var failureDescriptions: [String] { + [ + "not present in \(self.items)" + ] + } +} diff --git a/Sources/ValidatableKit/Validators/Nil.swift b/Sources/ValidatableKit/Validators/Nil.swift new file mode 100644 index 0000000..443f2fe --- /dev/null +++ b/Sources/ValidatableKit/Validators/Nil.swift @@ -0,0 +1,38 @@ +public extension Validator { + /// Validates that the data is `nil`. Combine with the not-operator `!` + /// to validate that the data is not `nil`. + static func isNil() -> Self where T == Optional { + .init { + ValidatorResults.Nil(isNil: $0 == nil) + } + } + + /// Validates that the data is not `nil` and validates wrapped data with provided `Validator`s. + static func wrapped( + _ validators: Validator... + ) -> Self where T == Optional { + return validators.reduce(!Validator.isNil(), &&) + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates that the data is `nil`. + public struct Nil { + /// Input is `nil`. + public let isNil: Bool + } +} + +extension ValidatorResults.Nil: ValidatorResult { + public var isFailure: Bool { + !self.isNil + } + + public var successDescriptions: [String] { + return ["is null"] + } + + public var failureDescriptions: [String] { + return ["is not null"] + } +} diff --git a/Sources/ValidatableKit/Validators/Operators/And.swift b/Sources/ValidatableKit/Validators/Operators/And.swift new file mode 100644 index 0000000..d2b0aa2 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Operators/And.swift @@ -0,0 +1,48 @@ +/// Combines two `Validator`s using AND logic, succeeding if both `Validator`s succeed without error. +public func && (lhs: Validator, rhs: Validator) -> Validator { + .init { + ValidatorResults.And(left: lhs.validate($0), right: rhs.validate($0)) + } +} + +extension ValidatorResults { + /// `ValidatorResult` of "And" `Validator` that combines two `ValidatorResults`. + /// If both results are successful the combined result is as well. + public struct And { + /// `ValidatorResult` of left hand side of the "And" validation. + public let left: ValidatorResult + + /// `ValidatorResult` of right hand side of the "And" validation. + public let right: ValidatorResult + } +} + +extension ValidatorResults.And: ValidatorResult { + public var isFailure: Bool { + self.left.isFailure || self.right.isFailure + } + + public var successDescriptions: [String] { + switch (self.left.isFailure, self.right.isFailure) { + case (false, false): + return self.left.successDescriptions + + self.right.successDescriptions + default: + return [] + } + } + + public var failureDescriptions: [String] { + switch (self.left.isFailure, self.right.isFailure) { + case (true, true): + return self.left.failureDescriptions + + self.right.failureDescriptions + case (true, false): + return self.left.failureDescriptions + case (false, true): + return self.right.failureDescriptions + default: + return [] + } + } +} diff --git a/Sources/ValidatableKit/Validators/Operators/NilIgnoring.swift b/Sources/ValidatableKit/Validators/Operators/NilIgnoring.swift new file mode 100644 index 0000000..36c3d8f --- /dev/null +++ b/Sources/ValidatableKit/Validators/Operators/NilIgnoring.swift @@ -0,0 +1,55 @@ +/// Combines an optional and non-optional `Validator` using OR logic. The non-optional +/// validator will simply ignore `nil` values, assuming the other `Validator` handles them. +public func || (lhs: Validator, rhs: Validator) -> Validator { + lhs + || .init { + ValidatorResults.NilIgnoring(result: $0.flatMap(rhs.validate)) + } +} + +/// Combines an optional and non-optional `Validator` using OR logic. The non-optional +/// validator will simply ignore `nil` values, assuming the other `Validator` handles them. +public func || (lhs: Validator, rhs: Validator) -> Validator { + .init { + ValidatorResults.NilIgnoring(result: $0.flatMap(lhs.validate)) + } || rhs +} + +/// Combines an optional and non-optional `Validator` using AND logic. The non-optional +/// validator will simply ignore `nil` values, assuming the other `Validator` handles them. +public func && (lhs: Validator, rhs: Validator) -> Validator { + lhs + && .init { + ValidatorResults.NilIgnoring(result: $0.flatMap(rhs.validate)) + } +} + +/// Combines an optional and non-optional `Validator` using AND logic. The non-optional +/// validator will simply ignore `nil` values, assuming the other `Validator` handles them. +public func && (lhs: Validator, rhs: Validator) -> Validator { + .init { + ValidatorResults.NilIgnoring(result: $0.flatMap(lhs.validate)) + } && rhs +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that ignores nil values. + public struct NilIgnoring { + /// Result of a validation or nil if the input is nil. + public let result: ValidatorResult? + } +} + +extension ValidatorResults.NilIgnoring: ValidatorResult { + public var isFailure: Bool { + result?.isFailure == true + } + + public var successDescriptions: [String] { + self.result?.successDescriptions ?? [] + } + + public var failureDescriptions: [String] { + self.result?.failureDescriptions ?? [] + } +} diff --git a/Sources/ValidatableKit/Validators/Operators/Not.swift b/Sources/ValidatableKit/Validators/Operators/Not.swift new file mode 100644 index 0000000..be0c397 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Operators/Not.swift @@ -0,0 +1,28 @@ +/// Inverts a validation. +public prefix func ! (validator: Validator) -> Validator { + .init { + ValidatorResults.Not(result: validator.validate($0)) + } +} + +extension ValidatorResults { + /// `ValidatorResult` of "Not" `Validator` + /// that negates the provided validator. + public struct Not { + public let result: ValidatorResult + } +} + +extension ValidatorResults.Not: ValidatorResult { + public var isFailure: Bool { + !self.result.isFailure + } + + public var successDescriptions: [String] { + self.result.failureDescriptions + } + + public var failureDescriptions: [String] { + return self.result.successDescriptions + } +} diff --git a/Sources/ValidatableKit/Validators/Operators/Or.swift b/Sources/ValidatableKit/Validators/Operators/Or.swift new file mode 100644 index 0000000..5e776e3 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Operators/Or.swift @@ -0,0 +1,51 @@ +/// Combines two `Validator`s, succeeding if either of the `Validator`s does not fail. +public func || (lhs: Validator, rhs: Validator) -> Validator { + .init { + return ValidatorResults.Or( + left: lhs.validate($0), + right: rhs.validate($0) + ) + } +} + +extension ValidatorResults { + /// `ValidatorResult` of "Or" `Validator` that combines two `ValidatorResults`. + /// If either result is successful the combined result is as well. + public struct Or { + /// `ValidatorResult` of left hand side. + public let left: ValidatorResult + + /// `ValidatorResult` of right hand side. + public let right: ValidatorResult + } +} + +extension ValidatorResults.Or: ValidatorResult { + public var isFailure: Bool { + self.left.isFailure && self.right.isFailure + } + + public var successDescriptions: [String] { + switch (self.left.isFailure, self.right.isFailure) { + case (false, false): + return self.left.successDescriptions + + self.right.successDescriptions + case (true, false): + return self.right.successDescriptions + case (false, true): + return self.left.successDescriptions + default: + return [] + } + } + + public var failureDescriptions: [String] { + switch (left.isFailure, right.isFailure) { + case (true, true): + return self.left.failureDescriptions + + self.right.failureDescriptions + default: + return [] + } + } +} diff --git a/Sources/ValidatableKit/Validators/Regex.swift b/Sources/ValidatableKit/Validators/Regex.swift new file mode 100644 index 0000000..a48ecc6 --- /dev/null +++ b/Sources/ValidatableKit/Validators/Regex.swift @@ -0,0 +1,99 @@ +#if canImport(RegexBuilder) +public extension Validator +where T: BidirectionalCollection, T.SubSequence == Substring { + /// Validates whether current string matches provided regular expression + /// and validates regex output with provided additional validators. + /// + /// - Parameters: + /// - regex: The regular expression to match with. + /// - option: The option to use for regex matching. + /// - validators: Additional validators to apply to regex output. + /// + /// - Returns: The resulting validator validating regex matching. + @available(swift 5.7) + @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) + static func matching( + _ regex: Regex, + withOption option: ValidatorResults.Regex.MatchOption = .whole, + validators: Validator... + ) -> Self { + .init { + let match: Regex.Match? + switch option { + case .first: + match = $0.firstMatch(of: regex) + case .whole: + match = $0.wholeMatch(of: regex) + case .prefix: + match = $0.prefixMatch(of: regex) + case .custom(let matcher): + match = matcher($0, regex) + } + guard let match = match else { + return ValidatorResults.Regex(matched: false) + } + + let validator = validators.reduce( + Validator.init { _ in + return ValidatorResults.Regex(matched: true) + }, + && + ) + return validator.validate(match.output) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator + /// that validates whether a `String` + /// is a match for provided regex pattern. + @available(swift 5.7) + @available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) + public struct Regex { + /// The options for regex matching. + public enum MatchOption< + Input: BidirectionalCollection, + Output + > where Input.SubSequence == Substring { + /// Regex returns the first match. + case first + /// Regex matches the entirety of string. + case whole + /// Regex checks for matches against the string, + /// starting at its beginning. + case prefix + /// Custom match action the provides the match result. + case custom( + ( + Input, + _StringProcessing.Regex + ) -> _StringProcessing.Regex.Match? + ) + } + + /// The input string is a match. + public let matched: Bool + } +} + +@available(swift 5.7) +@available(macOS 13, iOS 16, macCatalyst 16, tvOS 16, watchOS 9, *) +extension ValidatorResults.Regex: ValidatorResult { + public var isFailure: Bool { + !self.matched + } + + public var successDescriptions: [String] { + [ + "matched pattern" + ] + } + + public var failureDescriptions: [String] { + [ + "did not match pattern" + ] + } +} +#endif diff --git a/Sources/ValidatableKit/Validators/URL.swift b/Sources/ValidatableKit/Validators/URL.swift new file mode 100644 index 0000000..cf5d44b --- /dev/null +++ b/Sources/ValidatableKit/Validators/URL.swift @@ -0,0 +1,45 @@ +import struct Foundation.URL + +public extension Validator where T == String { + /// Validates whether a `String` is a valid URL. + /// + /// This validator will allow either file URLs, or URLs + /// containing at least a scheme and a host. + static var isURL: Self { + .init { + guard + let url = URL(string: $0), + url.isFileURL || (url.host != nil && url.scheme != nil) + else { + return ValidatorResults.URL(isValidURL: false) + } + return ValidatorResults.URL(isValidURL: true) + } + } +} + +extension ValidatorResults { + /// `ValidatorResult` of a validator that validates whether a string is a valid URL. + public struct URL { + /// The input is a valid URL. + public let isValidURL: Bool + } +} + +extension ValidatorResults.URL: ValidatorResult { + public var isFailure: Bool { + !self.isValidURL + } + + public var successDescriptions: [String] { + [ + "is a valid URL" + ] + } + + public var failureDescriptions: [String] { + [ + "is an invalid URL" + ] + } +}