diff --git a/Package.swift b/Package.swift index 4d94bf6..73091d0 100644 --- a/Package.swift +++ b/Package.swift @@ -1,8 +1,18 @@ +// swift-tools-version:4.0 import PackageDescription let package = Package( name: "Validation", + products: [ + .library(name: "Validation", targets: ["Validation"]), + ], dependencies: [ - .Package(url: "https://github.com/vapor/debugging.git", majorVersion: 1), + // Core extensions, type-aliases, and functions that facilitate common tasks. + .package(url: "https://github.com/vapor/core.git", .exact("3.0.0-beta.1")), + ], + targets: [ + // Validation + .target(name: "Validation", dependencies: ["CodableKit"]), + .testTarget(name: "ValidationTests", dependencies: ["Validation"]), ] ) diff --git a/Package@swift-4.swift b/Package@swift-4.swift deleted file mode 100644 index 5f2e889..0000000 --- a/Package@swift-4.swift +++ /dev/null @@ -1,16 +0,0 @@ -// swift-tools-version:4.0 -import PackageDescription - -let package = Package( - name: "Validation", - products: [ - .library(name: "Validation", targets: ["Validation"]), - ], - dependencies: [ - .package(url: "https://github.com/vapor/debugging.git", .upToNextMajor(from: "1.0.0")), - ], - targets: [ - .target(name: "Validation", dependencies: ["Debugging"]), - .testTarget(name: "ValidationTests", dependencies: ["Validation"]), - ] -) diff --git a/Sources/Validation/Convenience/ASCII.swift b/Sources/Validation/Convenience/ASCII.swift deleted file mode 100644 index 8cb2fce..0000000 --- a/Sources/Validation/Convenience/ASCII.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct ASCIIValidator: Validator { - - public init () {} - - private let pattern = "^[ -~]+$" - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not valid ascii") - } - } - -} diff --git a/Sources/Validation/Convenience/Alphanumeric.swift b/Sources/Validation/Convenience/Alphanumeric.swift deleted file mode 100644 index 91b7aba..0000000 --- a/Sources/Validation/Convenience/Alphanumeric.swift +++ /dev/null @@ -1,26 +0,0 @@ -private let alphanumeric = "abcdefghijklmnopqrstuvwxyz0123456789" -private let validCharacters = alphanumeric.characters - -/// A validator that can be used to check that a -/// given string contains only alphanumeric characters -public struct OnlyAlphanumeric: Validator { - public init() {} - /** - Validate whether or not an input string contains only - alphanumeric characters. a...z0...9 - - - parameter value: input value to validate - - - throws: an error if validation fails - */ - public func validate(_ input: String) throws { - let passed = !input - .lowercased() - .characters - .contains { !validCharacters.contains($0) } - - if !passed { - throw error("\(input) is not alphanumeric") - } - } -} diff --git a/Sources/Validation/Convenience/Base64.swift b/Sources/Validation/Convenience/Base64.swift deleted file mode 100644 index 4e3a395..0000000 --- a/Sources/Validation/Convenience/Base64.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct Base64Validator: Validator { - - public init () {} - - private let pattern = "^(?:[a-z0-9\\+\\/]{4})*(?:[a-z0-9\\+\\/]{2}==|[a-z0-9\\+\\/]{3}=|[a-z0-9\\+\\/]{4})$" - - public func validate(_ input: String) throws { - guard input.characters.count % 4 == 0 && input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid base64 string") - } - } - -} diff --git a/Sources/Validation/Convenience/Compare.swift b/Sources/Validation/Convenience/Compare.swift deleted file mode 100644 index e5997cf..0000000 --- a/Sources/Validation/Convenience/Compare.swift +++ /dev/null @@ -1,62 +0,0 @@ -/// Validate a comparable -/// -/// - greaterThan: validate input is > associated value -/// - greaterThanOrEqual: validate input is >= associated value -/// - lessThan: validate input is < associated value -/// - lessThanOrEqual: validate input is <= associated value -/// - equals: validate input == associated value -/// - containedIn: validate low <= input && input <= high -public enum Compare: Validator where Input: Comparable, Input: Validatable { - case greaterThan(Input) - case greaterThanOrEqual(Input) - case lessThan(Input) - case lessThanOrEqual(Input) - case equals(Input) - case containedIn(low: Input, high: Input) - - /// Validate that a string passes associated compare evaluation - /// - /// - parameter value: input string to validate - /// - /// - throws: an error if validation fails - public func validate(_ input: Input) throws { - switch self { - case .greaterThan(let c) where input > c: - break - case .greaterThanOrEqual(let c) where input >= c: - break - case .lessThan(let c) where input < c: - break - case .lessThanOrEqual(let c) where input <= c: - break - case .equals(let e) where input == e: - break - case .containedIn(low: let l, high: let h) where l <= input && input <= h: - break - default: - let reason = errorReason(with: input) - throw error(reason) - } - } - - private func errorReason(with input: Input) -> String { - var reason = "\(input) is not " - - switch self { - case .greaterThan(let c): - reason += "greater than \(c)" - case .greaterThanOrEqual(let c): - reason += "greater than or equal to \(c)" - case .lessThan(let c): - reason += "less than \(c)" - case .lessThanOrEqual(let c): - reason += "less than or equal to \(c)" - case .equals(let e): - reason += "equal to \(e)" - case .containedIn(low: let l, high: let h): - reason += "not contained in \(l...h)" - } - - return reason - } -} diff --git a/Sources/Validation/Convenience/Contains.swift b/Sources/Validation/Convenience/Contains.swift deleted file mode 100644 index aa8d472..0000000 --- a/Sources/Validation/Convenience/Contains.swift +++ /dev/null @@ -1,18 +0,0 @@ -/// Validate that a sequence contains a given value -public struct Contains: Validator where T: Sequence, T: Validatable, T.Iterator.Element: Equatable { - /// The value expected to be in sequence - public let expecting: T.Iterator.Element - - /// Create a validator to check that a sequence contains the given value - /// - /// - parameter expecting: the value expected to be in sequence - public init(_ expecting: T.Iterator.Element) { - self.expecting = expecting - } - - /// validate - public func validate(_ input: T) throws { - if input.contains(expecting) { return } - throw error("\(input) does not contain \(expecting)") - } -} diff --git a/Sources/Validation/Convenience/Count.swift b/Sources/Validation/Convenience/Count.swift deleted file mode 100644 index 6bf749d..0000000 --- a/Sources/Validation/Convenience/Count.swift +++ /dev/null @@ -1,111 +0,0 @@ -/// Indicates that a particular type can be validated by count or length -public protocol Countable: Validatable { - // The type that will be used to evaluate the count - associatedtype CountType: Comparable, Equatable - - // The count of the object - var count: CountType { get } -} - -/// Use this to validate the count of a given countable type -/// -/// "someString".validated(by: Count.min(3) + OnlyAlphanumeric.self) -/// -/// - min: validate count is >= associated value -/// - max: validate count <= associated value -/// - equals: validate count == associated value -/// - containedIn: validate low is <= count and count is <= max -public enum Count: Validator { - public typealias CountType = Input.CountType - case min(CountType) - case max(CountType) - case equals(CountType) - case containedIn(low: CountType, high: CountType) - - /// Validate that a string passes associated length evaluation - /// - /// - parameter value: input string to validate - /// - /// - throws: an error if validation fails - public func validate(_ input: Input) throws { - let count = input.count - switch self { - case .min(let m) where count >= m: - break - case .max(let m) where count <= m: - break - case .equals(let e) where count == e: - break - case .containedIn(low: let l, high: let h) where l <= count && count <= h: - break - default: - let reason = errorReason(with: input) - throw error(reason) - } - } - - private func errorReason(with input: Input) -> String { - var reason = "\(input) count \(input.count) is " - - switch self { - case .min(let m): - reason += "less than minimum \(m)" - case .max(let m): - reason += "greater than maximum \(m)" - case .equals(let e): - reason += "doesn't equal \(e)" - case .containedIn(low: let l, high: let h): - reason += "not contained in \(l...h)" - } - - return reason - } -} - -// MARK: Conformance - -extension Array: Countable {} -extension Dictionary: Countable {} - -extension Set: Countable {} - -extension Int: Countable {} -extension Int8: Countable {} -extension Int16: Countable {} -extension Int32: Countable {} -extension Int64: Countable {} - -extension UInt: Countable {} -extension UInt8: Countable {} -extension UInt16: Countable {} -extension UInt32: Countable {} -extension UInt64: Countable {} - -extension Float: Countable {} -extension Double: Countable {} - -extension String: Countable { - public var count: Int { - return characters.count - } -} - -#if swift(>=4) -extension BinaryInteger { - public var count: Self { - return self - } -} -#else -extension Integer { - public var count: Self { - return self - } -} -#endif - -extension FloatingPoint { - public var count: Self { - return self - } -} diff --git a/Sources/Validation/Convenience/Email.swift b/Sources/Validation/Convenience/Email.swift deleted file mode 100644 index 27f1f56..0000000 --- a/Sources/Validation/Convenience/Email.swift +++ /dev/null @@ -1,14 +0,0 @@ -import Foundation - -public struct EmailValidator: Validator { - - public init() {} - private let pattern = "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}" - - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid email address") - } - } -} diff --git a/Sources/Validation/Convenience/Equals.swift b/Sources/Validation/Convenience/Equals.swift deleted file mode 100644 index bb79f9a..0000000 --- a/Sources/Validation/Convenience/Equals.swift +++ /dev/null @@ -1,16 +0,0 @@ -/// Validates that matches a given input -public struct Equals: Validator where T: Validatable, T: Equatable { - /// The value expected to be in sequence - public let expectation: T - - /// Initialize a validator with the expected value - public init(_ expectation: T) { - self.expectation = expectation - } - - public func validate(_ input: T) throws { - guard input == expectation else { - throw error("\(input) does not equal expectation \(expectation)") - } - } -} diff --git a/Sources/Validation/Convenience/Hexadecimal.swift b/Sources/Validation/Convenience/Hexadecimal.swift deleted file mode 100644 index e345b0f..0000000 --- a/Sources/Validation/Convenience/Hexadecimal.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct HexadecimalValidator: Validator { - - public init () {} - - private let pattern = "^[a-f0-9]+$" - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid hexadecimal") - } - } - -} diff --git a/Sources/Validation/Convenience/In.swift b/Sources/Validation/Convenience/In.swift deleted file mode 100644 index 59b01a7..0000000 --- a/Sources/Validation/Convenience/In.swift +++ /dev/null @@ -1,18 +0,0 @@ -/// Validate that is in given collection -public struct In: Validator where T: Validatable, T: Equatable { - private let collection: [T] - - /// Create in validation against passed iterator - /// - /// - parameter sequence: the sequence to check if contains - public init(_ sequence: S) where S.Iterator.Element == T { - collection = Array(sequence) - } - - public func validate(_ input: T) throws { - for next in collection where next == input { - return - } - throw error("\(collection) does not contain \(input)") - } -} diff --git a/Sources/Validation/Convenience/MD5.swift b/Sources/Validation/Convenience/MD5.swift deleted file mode 100644 index a30575b..0000000 --- a/Sources/Validation/Convenience/MD5.swift +++ /dev/null @@ -1,12 +0,0 @@ -public struct MD5Validator: Validator { - - public init () {} - - private let pattern = "^[a-f0-9]{32}$" - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid MD5") - } - } -} diff --git a/Sources/Validation/Convenience/MacAddress.swift b/Sources/Validation/Convenience/MacAddress.swift deleted file mode 100644 index c91341e..0000000 --- a/Sources/Validation/Convenience/MacAddress.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct MacAddressValidator: Validator { - - public init () {} - - private let pattern = "^([a-f0-9]{2}([:-]?[a-f0-9]{2}){5}|[a-f0-9]{4}(\\.?[a-f0-9]{4}){2})$" - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid mac address") - } - } - -} diff --git a/Sources/Validation/Convenience/MongoID.swift b/Sources/Validation/Convenience/MongoID.swift deleted file mode 100644 index f0b5501..0000000 --- a/Sources/Validation/Convenience/MongoID.swift +++ /dev/null @@ -1,13 +0,0 @@ -public struct MongoIDValidator: Validator { - - public init() {} - - private let pattern = "^[a-f0-9]{24}$" - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid mongo Id") - } - } - -} diff --git a/Sources/Validation/Convenience/UUID.swift b/Sources/Validation/Convenience/UUID.swift deleted file mode 100644 index 3d63efb..0000000 --- a/Sources/Validation/Convenience/UUID.swift +++ /dev/null @@ -1,20 +0,0 @@ -public enum UUIDValidator: String, Validator { - - case uuid1 = "1" - case uuid2 = "2" - case uuid3 = "3" - case uuid4 = "4" - case uuid5 = "5" - case uuid = "[1-5]" - - private var pattern: String { - return "^[0-9a-f]{8}-[0-9a-f]{4}-\(self.rawValue)[0-9a-f]{3}-[89abAB][0-9a-f]{3}-[0-9a-f]{12}$" - } - - public func validate(_ input: String) throws { - guard input.range(of: pattern, options: [.regularExpression, .caseInsensitive]) != nil else { - throw error("\(input) is not a valid \(self)") - } - } - -} diff --git a/Sources/Validation/Convenience/Unique.swift b/Sources/Validation/Convenience/Unique.swift deleted file mode 100644 index def59e7..0000000 --- a/Sources/Validation/Convenience/Unique.swift +++ /dev/null @@ -1,14 +0,0 @@ -/// Validates a given sequence is unique -public struct Unique: Validator where T: Sequence, T: Validatable, T.Iterator.Element: Equatable { - public init() {} - public func validate(_ sequence: T) throws { - var uniqueValues: [T.Iterator.Element] = [] - for value in sequence { - if uniqueValues.contains(value) { - throw error("\(sequence) is not unique)") - } else { - uniqueValues.append(value) - } - } - } -} diff --git a/Sources/Validation/Either.swift b/Sources/Validation/Either.swift deleted file mode 100644 index 1c2101a..0000000 --- a/Sources/Validation/Either.swift +++ /dev/null @@ -1,36 +0,0 @@ -// -// The MIT License (MIT) Copyright (c) 2016 Benjamin Encz -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE -// AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -/// A combination of two validators in which either one of them -/// can pass to be considered a successful validation -public final class Either: Validator { - let left: (Input) throws -> () - let right: (Input) throws -> () - - internal init(_ l: L, _ r: R) where L.Input == Input, R.Input == Input { - left = l.validate - right = r.validate - } - - public func validate(_ input: Input) throws { - guard let leftError = validate(input, with: left) else { return } - guard let rightError = validate(input, with: right) else { return } - throw error("neither validator passed '\(leftError)' and '\(rightError)'") - } -} diff --git a/Sources/Validation/Error.swift b/Sources/Validation/Error.swift deleted file mode 100644 index 958703b..0000000 --- a/Sources/Validation/Error.swift +++ /dev/null @@ -1,52 +0,0 @@ -import Debugging - -public protocol ValidationError: Debuggable {} - -// MARK: Error List - -public struct ErrorList: ValidationError { - public let errors: [Error] - - public init(_ errors: [Error]) { - self.errors = errors - } -} - -extension ErrorList { - public var identifier: String { - return "errorList" - } - - public var reason: String { - let collected = errors.map { "'\($0)'" } .joined(separator: ",") - return "Validation failed with the following errors: \(collected)" - } - - public var possibleCauses: [String] { return [] } - public var suggestedFixes: [String] { return [] } -} - -// MARK: ValidatorError - -public enum ValidatorError: ValidationError { - case failure(type: String, reason: String) -} - -extension ValidatorError { - public var reason: String { - switch self { - case .failure(type: let type, reason: let reason): - return "\(type) failed validation: \(reason)" - } - } - - public var identifier: String { - switch self { - case .failure(_): - return "failure" - } - } - - public var possibleCauses: [String] { return [] } - public var suggestedFixes: [String] { return [] } -} diff --git a/Sources/Validation/Exports.swift b/Sources/Validation/Exports.swift new file mode 100644 index 0000000..0575571 --- /dev/null +++ b/Sources/Validation/Exports.swift @@ -0,0 +1 @@ +@_exported import CodableKit diff --git a/Sources/Validation/Not.swift b/Sources/Validation/Not.swift deleted file mode 100644 index 867a857..0000000 --- a/Sources/Validation/Not.swift +++ /dev/null @@ -1,31 +0,0 @@ -// -// The MIT License (MIT) Copyright (c) 2016 Benjamin Encz -// -// Permission is hereby granted, free of charge, to any person obtaining a copy of this -// software and associated documentation files (the "Software"), to deal in the Software -// without restriction, including without limitation the rights to use, copy, modify, merge, -// publish, distribute, sublicense, and/or sell copies of the Software, and to permit -// persons to whom the Software is furnished to do so, subject to the following conditions: -// -// The above copyright notice and this permission notice shall be included in all copies or -// substantial portions of the Software. -// -// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, -// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE -// AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, -// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -// - -/// Used to invert the logic of a wrapped validator -public final class Not: Validator { - let validate: (Input) throws -> () - - init(_ validator: V) where V.Input == Input { - self.validate = validator.validate - } - - public func validate(_ input: Input) throws { - guard let _ = validate(input, with: validate) else { throw error("expected failure for \(input)") } - } -} diff --git a/Sources/Validation/Operators.swift b/Sources/Validation/Operators.swift deleted file mode 100644 index 6f59358..0000000 --- a/Sources/Validation/Operators.swift +++ /dev/null @@ -1,41 +0,0 @@ -// MARK: && - Combine Validators - -public func && (lhs: L, rhs: R) -> ValidatorList where L.Input == R.Input { - let list = ValidatorList() - list.extend(lhs) - list.extend(rhs) - return list -} - -public func && (lhs: ValidatorList, rhs: R) -> ValidatorList { - let list = ValidatorList() - lhs.validators.forEach(list.extend) - list.extend(rhs) - return list -} - -public func && (lhs: L, rhs: ValidatorList) -> ValidatorList { - let list = ValidatorList() - rhs.validators.forEach(list.extend) - list.extend(lhs) - return list -} - -public func && (lhs: ValidatorList, rhs: ValidatorList) -> ValidatorList { - let list = ValidatorList() - lhs.validators.forEach(list.extend) - rhs.validators.forEach(list.extend) - return list -} - -// MARK: || - Combine OR validators - -public func || (lhs: L, rhs: R) -> Either where L.Input == R.Input { - return Either(lhs, rhs) -} - -// MARK: ! - Invert Validator - -public prefix func ! (validator: V) -> Not { - return Not(validator) -} diff --git a/Sources/Validation/Validatable.swift b/Sources/Validation/Validatable.swift index f2913a0..56c1011 100644 --- a/Sources/Validation/Validatable.swift +++ b/Sources/Validation/Validatable.swift @@ -1,61 +1,66 @@ -/// This is an API driven protocol -/// and any type that might need to be validated -/// can be conformed independently -public protocol Validatable {} +/// Capable of being validated. +public protocol Validatable: Codable, ValidationDataRepresentable { + /// The validations that will run when `.validate()` + /// is called on an instance of this class. + static var validations: Validations { get } +} extension Validatable { - /// Validate an individual validator - public func validated(by validator: V) throws where V.Input == Self { - let list = ValidatorList(validator) - try validated(by: list) - } - - /// Push validation to list level for more consistent error lists - public func validated(by list: ValidatorList) throws { - try list.validate(self) + /// See ValidationDataRepresentable.makeValidationData() + public func makeValidationData() -> ValidationData { + return .validatable(self) } } extension Validatable { - /// Tests a value with a given validator, upon passing, returns self - /// or throws - public func tested(by v: V) throws -> Self where V.Input == Self { - try v.validate(self) - return self - } + /// Validates the model, throwing an error + /// if any of the validations fail. + /// note: non-validation errors may also be thrown + /// should the validators encounter unexpected errors. + public func validate() throws { + var errors: [ValidationError] = [] + + for (key, validation) in Self.validations.storage { + /// fetch the value for the key path and + /// convert it to validation data + let data = (self[keyPath: key.keyPath] as ValidationDataRepresentable).makeValidationData() + + /// run the validation, catching validation errors + do { + try validation.validate(data) + } catch var error as ValidationError { + error.codingPath += key.codingPath + errors.append(error) + } + } - /// Converts validation to a boolean indicating success/failure - public func passes(_ v: V) -> Bool where V.Input == Self { - do { - try validated(by: v) - return true - } catch { - return false + if !errors.isEmpty { + throw ValidatableError(errors) } } } -// MARK: Conformance +/// a collection of errors thrown by validatable +/// models validations +struct ValidatableError: ValidationError { + /// the errors thrown + var errors: [ValidationError] -extension String: Validatable {} + /// See ValidationError.keyPath + var codingPath: [CodingKey] -extension Set: Validatable {} -extension Array: Validatable {} -extension Dictionary: Validatable {} - -extension Bool: Validatable {} - -extension Int: Validatable {} -extension Int8: Validatable {} -extension Int16: Validatable {} -extension Int32: Validatable {} -extension Int64: Validatable {} - -extension UInt: Validatable {} -extension UInt8: Validatable {} -extension UInt16: Validatable {} -extension UInt32: Validatable {} -extension UInt64: Validatable {} + /// See ValidationError.reason + var reason: String { + return errors.map { error in + var mutableError = error + mutableError.codingPath = codingPath + error.codingPath + return mutableError.reason + }.joined(separator: ", ") + } -extension Float: Validatable {} -extension Double: Validatable {} + /// creates a new validatable error + public init(_ errors: [ValidationError]) { + self.errors = errors + self.codingPath = [] + } +} diff --git a/Sources/Validation/ValidationData.swift b/Sources/Validation/ValidationData.swift new file mode 100644 index 0000000..bb5004c --- /dev/null +++ b/Sources/Validation/ValidationData.swift @@ -0,0 +1,162 @@ +import Foundation + +/// Supported validation data. +public enum ValidationData { + case string(String) + case int(Int) + case uint(UInt) + case bool(Bool) + case data(Data) + case date(Date) + case double(Double) + case array([ValidationData]) + case dictionary([String: ValidationData]) + case validatable(Validatable) + case null +} + +/// Capable of being represented by validation data. +/// Custom types you want to validate must conform to this protocol. +public protocol ValidationDataRepresentable { + /// Converts to validation data + func makeValidationData() -> ValidationData +} + +extension Bool: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .bool(self) + } +} + +extension String: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .string(self) + } +} + +extension Int: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .int(self) + } +} + +extension Int8: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .int(Int(self)) + } +} + +extension Int16: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .int(Int(self)) + } +} + +extension Int32: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .int(Int(self)) + } +} + +extension Int64: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .int(Int(self)) + } +} + +extension UInt: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .uint(self) + } +} + +extension UInt8: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .uint(UInt(self)) + } +} + +extension UInt16: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .uint(UInt(self)) + } +} + +extension UInt32: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .uint(UInt(self)) + } +} + +extension UInt64: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .uint(UInt(self)) + } +} + +extension Double: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .double(self) + } +} + +extension Data: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .data(self) + } +} + +extension Date: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + return .date(self) + } +} + +extension Array: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + var items: [ValidationData] = [] + for el in self { + // FIXME: conditional conformance + items.append((el as! ValidationDataRepresentable).makeValidationData()) + } + return .array(items) + } +} + +extension Dictionary: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + var items: [String: ValidationData] = [:] + for (key, el) in self { + // FIXME: conditional conformance + items[(key as! String)] = (el as! ValidationDataRepresentable).makeValidationData() + } + return .dictionary(items) + } +} + +extension Optional: ValidationDataRepresentable { + /// See ValidationDataRepresentable.makeValidationData + public func makeValidationData() -> ValidationData { + switch self { + case .none: return .null + case .some(let s): return (s as? ValidationDataRepresentable)?.makeValidationData() ?? .null + } + } +} diff --git a/Sources/Validation/ValidationError.swift b/Sources/Validation/ValidationError.swift new file mode 100644 index 0000000..bce0407 --- /dev/null +++ b/Sources/Validation/ValidationError.swift @@ -0,0 +1,44 @@ +import Debugging + +/// Errors that can be thrown while working with validation +public struct BasicValidationError: ValidationError { + /// See Debuggable.reason + public var reason: String { + let path: String + if codingPath.count > 0 { + path = "`" + codingPath.map { $0.stringValue }.joined(separator: ".") + "`" + } else { + path = "data" + } + return "\(path) \(message)" + } + + /// The validation failure + public var message: String + + /// Key path the validation error happened at + public var codingPath: [CodingKey] + + /// Create a new JWT error + public init(_ message: String) { + self.message = message + self.codingPath = [] + } +} + +/// A validation error that supports dynamic +/// key paths. +public protocol ValidationError: Debuggable, Error { + /// See Debuggable.reason + var reason: String { get } + + /// Key path the validation error happened at + var codingPath: [CodingKey] { get set } +} + +extension ValidationError { + /// See Debuggable.identifier + public var identifier: String { + return "validationFailed" + } +} diff --git a/Sources/Validation/Validations.swift b/Sources/Validation/Validations.swift new file mode 100644 index 0000000..dc1148b --- /dev/null +++ b/Sources/Validation/Validations.swift @@ -0,0 +1,71 @@ +public struct Validations: ExpressibleByDictionaryLiteral { + /// Store the key and query field. + internal var storage: [ValidationKey: Validator] + + /// See ExpressibleByDictionaryLiteral + public init(dictionaryLiteral elements: (ValidationKey, Validator)...) { + self.storage = [:] + for (key, validator) in elements { + storage[key] = validator + } + } +} + +/// A model property containing the +/// Swift key path for accessing it. +public struct ValidationKey: Hashable { + /// See `Hashable.hashValue` + public var hashValue: Int { + return keyPath.hashValue + } + + /// See `Equatable.==` + public static func ==(lhs: ValidationKey, rhs: ValidationKey) -> Bool { + return lhs.keyPath == rhs.keyPath + } + + /// The Swift keypath + public var keyPath: AnyKeyPath + + /// The respective CodingKey path. + public var codingPath: [CodingKey] + + /// The properties type. + /// Storing this as `Any` since we lost + /// the type info converting to AnyKeyPAth + public var type: Any.Type + + /// True if the property on the model is optional. + /// The `type` is the Wrapped type if this is true. + public var isOptional: Bool + + /// Create a new model key. + internal init(keyPath: AnyKeyPath, codingPath: [CodingKey], type: T.Type, isOptional: Bool) { + self.keyPath = keyPath + self.codingPath = codingPath + self.type = type + self.isOptional = isOptional + } +} + +extension Validatable { + /// Create a validation key for the supplied key path. + public static func key(_ path: KeyPath) -> ValidationKey where T: ValidationDataRepresentable, T: KeyStringDecodable { + return ValidationKey( + keyPath: path, + codingPath: Self.codingPath(forKey: path), + type: T.self, + isOptional: false + ) + } + + /// Create a validation key for the supplied key path. + public static func key(_ path: KeyPath) -> ValidationKey where T: ValidationDataRepresentable, T: KeyStringDecodable { + return ValidationKey( + keyPath: path, + codingPath: Self.codingPath(forKey: path), + type: T.self, + isOptional: true + ) + } +} diff --git a/Sources/Validation/Validator.swift b/Sources/Validation/Validator.swift new file mode 100644 index 0000000..c0c9a66 --- /dev/null +++ b/Sources/Validation/Validator.swift @@ -0,0 +1,8 @@ +/// Capable of validating validation data or throwing a validation error +public protocol Validator { + /// used by the `NotValidator` + var inverseMessage: String { get } + + /// validates the supplied data + func validate(_ data: ValidationData) throws +} diff --git a/Sources/Validation/ValidatorList.swift b/Sources/Validation/ValidatorList.swift deleted file mode 100644 index f31c56f..0000000 --- a/Sources/Validation/ValidatorList.swift +++ /dev/null @@ -1,33 +0,0 @@ -public final class ValidatorList: Validator { - public private(set) var validators: [(Input) throws -> Void] = [] - - internal init() {} - - internal init(_ validator: V) where V.Input == Input { - extend(validator) - } - - internal func extend(_ v: V) where V.Input == Input { - validators.append(v.validate) - } - - internal func extend(_ v: @escaping (Input) throws -> Void) { - validators.append(v) - } - - public func validate(_ input: Input) throws { - var failures = [Error]() - - validators.forEach { validator in - do { - try validator(input) - } catch let error as ErrorList { - failures += error.errors - } catch { - failures.append(error) - } - } - - if !failures.isEmpty { throw ErrorList(failures) } - } -} diff --git a/Sources/Validation/Validators.swift b/Sources/Validation/Validators.swift deleted file mode 100644 index 6d6d78c..0000000 --- a/Sources/Validation/Validators.swift +++ /dev/null @@ -1,29 +0,0 @@ -/// An object that can be used to validate other objects -public protocol Validator { - /// The supported input type for this validator - associatedtype Input: Validatable - - /// The validation function, throw on failed validation with `throw error("reason")` - func validate(_ input: Input) throws -} - -extension Validator { - /// On validation failure, use this to indicate - /// why validation failed - public func error(_ reason: String) -> Error { - let type = String(describing: Swift.type(of: self)) - return ValidatorError.failure(type: type, reason: reason) - } -} - -extension Validator { - /// Convert throwing function to optional while providing error -- internal only - internal func validate(_ input: Input, with validator: (Input) throws -> ()) -> Error? { - do { - try validator(input) - return nil - } catch { - return error - } - } -} diff --git a/Sources/Validation/Validators/AndValidator.swift b/Sources/Validation/Validators/AndValidator.swift new file mode 100644 index 0000000..0920249 --- /dev/null +++ b/Sources/Validation/Validators/AndValidator.swift @@ -0,0 +1,85 @@ +/// Combines two validators into an and validator +public func && (lhs: Validator, rhs: Validator) -> Validator { + return AndValidator(lhs, rhs) +} + +/// Combines two validators, if either both succeed +/// the validation will succeed. +internal struct AndValidator: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "\(lhs.inverseMessage) and \(rhs.inverseMessage)" + } + + /// left validator + let lhs: Validator + + /// right validator + let rhs: Validator + + /// create a new and validator + init(_ lhs: Validator, _ rhs: Validator) { + self.lhs = lhs + self.rhs = rhs + } + + /// See Validator.validate + func validate(_ data: ValidationData) throws { + var left: ValidationError? + do { + try lhs.validate(data) + } catch let l as ValidationError { + left = l + } + + var right: ValidationError? + do { + try rhs.validate(data) + } catch let r as ValidationError { + right = r + } + + if left != nil || right != nil { + throw AndValidatorError(left, right) + } + } +} + +/// Error thrown if and validation fails +internal struct AndValidatorError: ValidationError { + /// error thrown by left validator + let left: ValidationError? + + /// error thrown by right validator + let right: ValidationError? + + /// See ValidationError.reason + var reason: String { + if let left = left, let right = right { + var mutableLeft = left, mutableRight = right + mutableLeft.codingPath = codingPath + left.codingPath + mutableRight.codingPath = codingPath + right.codingPath + return "\(mutableLeft.reason) and \(mutableRight.reason)" + } else if let left = left { + var mutableLeft = left + mutableLeft.codingPath = codingPath + left.codingPath + return mutableLeft.reason + } else if let right = right { + var mutableRight = right + mutableRight.codingPath = codingPath + right.codingPath + return mutableRight.reason + } else { + return "" + } + } + + /// See ValidationError.keyPath + var codingPath: [CodingKey] + + /// Creates a new or validator error + init(_ left: ValidationError?, _ right: ValidationError?) { + self.left = left + self.right = right + self.codingPath = [] + } +} diff --git a/Sources/Validation/Validators/IsASCII.swift b/Sources/Validation/Validators/IsASCII.swift new file mode 100644 index 0000000..eb97785 --- /dev/null +++ b/Sources/Validation/Validators/IsASCII.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Validates whether a string contains only ASCII characters +public struct IsASCII: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "ASCII" + } + + /// creates a new ASCII validator + public init() {} + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .string(let s): + guard s.range(of: "^[ -~]+$", options: [.regularExpression, .caseInsensitive]) != nil else { + throw BasicValidationError("is not ASCII") + } + default: + throw BasicValidationError("is not a string") + } + } +} diff --git a/Sources/Validation/Validators/IsAlphanumeric.swift b/Sources/Validation/Validators/IsAlphanumeric.swift new file mode 100644 index 0000000..096f2ab --- /dev/null +++ b/Sources/Validation/Validators/IsAlphanumeric.swift @@ -0,0 +1,27 @@ +import Foundation + +private let alphanumeric = "abcdefghijklmnopqrstuvwxyz0123456789" + +/// Validates whether a string contains only alphanumeric characters +public struct IsAlphanumeric: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "alphanumeric" + } + + /// creates a new alphanumeric validator + public init() {} + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .string(let s): + for char in s.lowercased() { + guard alphanumeric.contains(char) else { + throw BasicValidationError("is not alphanumeric") + } + } + default: throw BasicValidationError("is not a string") + } + } +} diff --git a/Sources/Validation/Validators/IsCount.swift b/Sources/Validation/Validators/IsCount.swift new file mode 100644 index 0000000..d994eec --- /dev/null +++ b/Sources/Validation/Validators/IsCount.swift @@ -0,0 +1,151 @@ +import Foundation + +/// Validates whether the data is within a supplied int range. +/// note: strings have length checked, while integers, doubles, and dates have their values checked +public struct IsCount: Validator where T: Comparable { + /// See Validator.inverseMessage + public var inverseMessage: String { + if let min = self.min, let max = self.max { + return "larger than \(min) or smaller than \(max)" + } else if let min = self.min { + return "larger than \(min)" + } else if let max = self.max { + return "smaller than \(max)" + } else { + return "valid" + } + } + + /// the minimum possible value, if nil, not checked + /// note: inclusive + public let min: T? + + /// the maximum possible value, if nil, not checked + /// note: inclusive + public let max: T? + + /// creates an is count validator using a predefined int range + /// 1...5 + public init(_ range: ClosedRange) { + self.min = range.lowerBound + self.max = range.upperBound + } + + /// creates an is count validator using a partial range through + /// ...5 + public init(_ range: PartialRangeThrough) { + self.max = range.upperBound + self.min = nil + } + + /// creates an is count validator using a partial range from + /// 5... + public init(_ range: PartialRangeFrom) { + self.max = nil + self.min = range.lowerBound + } + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .string(let s): + if let min = self.min as? Int { + guard s.count >= min else { + throw BasicValidationError("is not at least \(min) characters") + } + } else if let min = self.min as? String { + guard s >= min else { + throw BasicValidationError("is not alphabetically equal to or later than \(min) characters") + } + } + + if let max = self.max as? Int { + guard s.count <= max else { + throw BasicValidationError("is more than \(max) characters") + } + } else if let max = self.max as? String { + guard s <= max else { + throw BasicValidationError("is not alphabetically equal to or before \(max) characters") + } + } + case .int(let int): + if let min = self.min as? Int { + guard int >= min else { + throw BasicValidationError("is not larger than \(min)") + } + } else if let min = self.min as? UInt { + guard int >= min else { + throw BasicValidationError("is not larger than \(min)") + } + } + if let max = self.max as? Int { + guard int <= max else { + throw BasicValidationError("is larger than \(max)") + } + } else if let max = self.max as? UInt { + guard int <= max else { + throw BasicValidationError("is larger than \(max)") + } + } + case .uint(let uint): + if let min = self.min as? Int { + guard uint >= min else { + throw BasicValidationError("is not larger than \(min)") + } + } else if let min = self.min as? UInt { + guard uint >= min else { + throw BasicValidationError("is not larger than \(min)") + } + } + if let max = self.max as? Int { + guard uint <= max else { + throw BasicValidationError("is larger than \(max)") + } + } else if let max = self.max as? UInt { + guard uint <= max else { + throw BasicValidationError("is larger than \(max)") + } + } + case .double(let double): + if let min = self.min as? Double { + guard double >= min else { + throw BasicValidationError("is not larger than \(min)") + } + } + if let max = self.max as? Double { + guard double <= max else { + throw BasicValidationError("is larger than \(max)") + } + } + case .date(let date): + if let earliest = self.min as? Date { + guard date >= earliest else { + throw BasicValidationError("is not equal to or later than \(earliest)") + } + } + if let latest = self.max as? Date { + guard date <= latest else { + throw BasicValidationError("is not equal to or earlier than \(latest)") + } + } + break + default: + throw BasicValidationError("is invalid") + } + } +} + +/// - TODO: The conditional conformance here would not be necessary if the +/// validator instead tracked whether the range it was created with was closed +/// or open, and chose whether to use `<` and `>` or `<=` and `>=` on that +/// basis. That would be the most correct and flexible solution. However, the +/// vast majority of ranges are `Strideable` so this lazy solution is used to +/// avoid making the validator's code _considerably_ more complicated, for now. +extension IsCount where T: Strideable { + /// creates an is count validator using a predefined int range + /// 1..<5 + public init(_ range: Range) { + self.min = range.lowerBound + self.max = range.upperBound.advanced(by: -1) + } +} diff --git a/Sources/Validation/Validators/IsEmail.swift b/Sources/Validation/Validators/IsEmail.swift new file mode 100644 index 0000000..4812649 --- /dev/null +++ b/Sources/Validation/Validators/IsEmail.swift @@ -0,0 +1,24 @@ +import Foundation + +/// Validates whether a string is a valid email address +public struct IsEmail: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "valid email address" + } + + /// creates a new ASCII validator + public init() {} + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .string(let s): + guard s.range(of: "[A-Z0-9a-z._%+-]+@[A-Za-z0-9.-]+\\.[A-Za-z]{2,64}", options: [.regularExpression, .caseInsensitive]) != nil else { + throw BasicValidationError("is not a valid email address") + } + default: + throw BasicValidationError("is not a string") + } + } +} diff --git a/Sources/Validation/Validators/IsNil.swift b/Sources/Validation/Validators/IsNil.swift new file mode 100644 index 0000000..3667da0 --- /dev/null +++ b/Sources/Validation/Validators/IsNil.swift @@ -0,0 +1,18 @@ +/// Validates that the data is nil +public struct IsNil: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "nil" + } + + /// Creates a new is nil validator + public init() {} + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .null: break + default: throw BasicValidationError("is not nil") + } + } +} diff --git a/Sources/Validation/Validators/IsValid.swift b/Sources/Validation/Validators/IsValid.swift new file mode 100644 index 0000000..b93619c --- /dev/null +++ b/Sources/Validation/Validators/IsValid.swift @@ -0,0 +1,21 @@ +/// Checks whether a child validatable object +/// passes its validations. +public struct IsValid: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "valid" + } + + /// Create a new is valid validator + public init() {} + + /// See Validator.validate + public func validate(_ data: ValidationData) throws { + switch data { + case .validatable(let v): + try v.validate() + default: + throw BasicValidationError("is invalid") + } + } +} diff --git a/Sources/Validation/Validators/NotValidator.swift b/Sources/Validation/Validators/NotValidator.swift new file mode 100644 index 0000000..0ae3feb --- /dev/null +++ b/Sources/Validation/Validators/NotValidator.swift @@ -0,0 +1,33 @@ +/// Inverts a validator into a not validator +public prefix func ! (rhs: Validator) -> Validator { + return NotValidator(rhs) +} + +/// Inverts a validator +internal struct NotValidator: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "not \(rhs.inverseMessage)" + } + + /// right validator + let rhs: Validator + + /// create a new not validator + init(_ rhs: Validator) { + self.rhs = rhs + } + + /// See Validator.validate + func validate(_ data: ValidationData) throws { + var error: ValidationError? + do { + try rhs.validate(data) + } catch let e as ValidationError { + error = e + } + guard error != nil else { + throw BasicValidationError("is \(rhs.inverseMessage)") + } + } +} diff --git a/Sources/Validation/Validators/OrValidator.swift b/Sources/Validation/Validators/OrValidator.swift new file mode 100644 index 0000000..b31d72a --- /dev/null +++ b/Sources/Validation/Validators/OrValidator.swift @@ -0,0 +1,66 @@ +/// Combines two validators into an or validator +public func || (lhs: Validator, rhs: Validator) -> Validator { + return OrValidator(lhs, rhs) +} + +/// Combines two validators, if either is true +/// the validation will succeed. +internal struct OrValidator: Validator { + /// See Validator.inverseMessage + public var inverseMessage: String { + return "\(lhs.inverseMessage) or \(rhs.inverseMessage)" + } + + /// left validator + let lhs: Validator + + /// right validator + let rhs: Validator + + /// create a new or validator + init(_ lhs: Validator, _ rhs: Validator) { + self.lhs = lhs + self.rhs = rhs + } + + /// See Validator.validate + func validate(_ data: ValidationData) throws { + do { + try lhs.validate(data) + } catch let left as ValidationError { + do { + try rhs.validate(data) + } catch let right as ValidationError { + throw OrValidatorError(left, right) + } + } + } +} + +/// Error thrown if or validation fails +internal struct OrValidatorError: ValidationError { + /// error thrown by left validator + let left: ValidationError + + /// error thrown by right validator + let right: ValidationError + + /// See ValidationError.reason + var reason: String { + var left = self.left + left.codingPath = codingPath + self.left.codingPath + var right = self.right + right.codingPath = codingPath + self.right.codingPath + return "\(left.reason) and \(right.reason)" + } + + /// See ValidationError.keyPath + var codingPath: [CodingKey] + + /// Creates a new or validator error + init(_ left: ValidationError, _ right: ValidationError) { + self.left = left + self.right = right + self.codingPath = [] + } +} diff --git a/Tests/ValidationTests/ValidationConvenienceTests.swift b/Tests/ValidationTests/ValidationConvenienceTests.swift deleted file mode 100644 index d2178fe..0000000 --- a/Tests/ValidationTests/ValidationConvenienceTests.swift +++ /dev/null @@ -1,83 +0,0 @@ -// -// EventTests.swift -// Vapor -// -// Created by Logan Wright on 4/6/16. -// -// - -import XCTest -class ValidationConvenienceTests: XCTestCase { - static let allTests = [ - ("testTrue", testTrue), - ("testFalse", testFalse) - ] - - func testTrue() throws { - } - - func testFalse() throws { - } -} - -class AlphanumericValidationTests: ValidationConvenienceTests { - override func testTrue() throws { - let alphanumeric = "Analphanumericstring" - let _ = try alphanumeric.tested(by: OnlyAlphanumeric()) - } - - override func testFalse() throws { - let not = "I've got all types of characters!" - XCTAssertFalse(not.passes(OnlyAlphanumeric())) - } -} - -class CompareValidationTests: ValidationConvenienceTests { - override func testTrue() throws { - let comparable = 2.3 - let _ = try comparable.tested(by: Compare.lessThan(5.0)) - let _ = try comparable.tested(by: Compare.lessThanOrEqual(5.0)) - let _ = try comparable.tested(by: Compare.greaterThan(1.0)) - let _ = try comparable.tested(by: Compare.greaterThanOrEqual(2.3)) - let _ = try comparable.tested(by: Compare.equals(2.3)) - let _ = try comparable.tested(by: Compare.containedIn(low: 0.0, high: 5.0)) - - let a = "a" - let _ = try a.tested(by: Compare.lessThan("z") && Count.equals(1) && OnlyAlphanumeric()) - } - - override func testFalse() throws { - let comparable = 42.0 - - XCTAssertFalse(comparable.passes(Compare.greaterThan(50))) - XCTAssertFalse(comparable.passes(Compare.equals(-1))) - XCTAssertFalse(comparable.passes(Compare.lessThan(1))) - - let a = "z" - XCTAssertFalse(a.passes(Compare.lessThan("d"))) - XCTAssertFalse(a.passes(Count.equals(10))) - XCTAssertFalse(a.passes(!OnlyAlphanumeric())) - } -} - -class MatchesValidationTests: ValidationConvenienceTests { - override func testTrue() throws { - let collection = 1 - let _ = try collection.tested(by: Equals(1)) - } - - override func testFalse() throws { - let collection = 1 - let result = collection.passes(Equals(999)) - XCTAssertFalse(result) - } -} - -class ContainsValidationTests: ValidationConvenienceTests { - override func testTrue() throws { - let collection = [1, 2, 3, 4, 5] - let _ = try collection.tested(by: Contains(1)) - let _ = try collection.tested(by: Contains(2)) - let _ = try collection.tested(by: Contains(3)) - } -} diff --git a/Tests/ValidationTests/ValidationCountTests.swift b/Tests/ValidationTests/ValidationCountTests.swift deleted file mode 100644 index f4e7db4..0000000 --- a/Tests/ValidationTests/ValidationCountTests.swift +++ /dev/null @@ -1,50 +0,0 @@ -// -// EventTests.swift -// Vapor -// -// Created by Logan Wright on 4/6/16. -// -// - -import XCTest - -class ValidationCountTests: XCTestCase { - static let allTests = [ - ("testCountString", testCountString), - ("testCountInteger", testCountInteger), - ("testCountArray", testCountArray) - ] - - func testCountString() { - let string = "123456789" - XCTAssertTrue(string.passes(Count.equals(9))) - XCTAssertTrue(string.passes(Count.containedIn(low: 8, high: 10))) - - XCTAssertTrue(string.passes(!Count.equals(1))) - - XCTAssertFalse(string.passes(Count.min(10))) - XCTAssertFalse(string.passes(Count.max(8))) - } - - func testCountInteger() { - let value = 231 - XCTAssertTrue(value.passes(Count.equals(231))) - XCTAssertTrue(value.passes(!Count.containedIn(low: 0, high: 100))) - - XCTAssertFalse(value.passes(!Count.equals(231))) - - XCTAssertFalse(value.passes(Count.min(300))) - XCTAssertFalse(value.passes(Count.max(200))) - } - - func testCountArray() { - let array = [1, 2, 3, 4, 5] - XCTAssertTrue(array.passes(Count.equals(5))) - XCTAssertTrue(array.passes(Count.containedIn(low: 0, high: 10))) - - XCTAssertTrue(array.passes(!Count.equals(0))) - - XCTAssertFalse(array.passes(Count.min(10))) - XCTAssertFalse(array.passes(Count.max(1))) - } -} diff --git a/Tests/ValidationTests/ValidationTests.swift b/Tests/ValidationTests/ValidationTests.swift index 66a3b73..19516df 100644 --- a/Tests/ValidationTests/ValidationTests.swift +++ b/Tests/ValidationTests/ValidationTests.swift @@ -1,146 +1,85 @@ -// -// EventTests.swift -// Vapor -// -// Created by Logan Wright on 4/6/16. -// -// - +import Validation import XCTest -class Name: Validator { - func validate(_ input: String) throws { - let evaluation = OnlyAlphanumeric() - && Count.min(5) - && Count.max(20) - - try evaluation.validate(input) - } -} - class ValidationTests: XCTestCase { - static let allTests = [ - ("testName", testName), - ("testPassword", testPassword), - ("testNot", testNot), - ("testComposition", testComposition), - ("testDetailedFailure", testDetailedFailure), - ("testValidEmail", testValidEmail), - ("testValidHexadecimal", testValidHexadecimal), - ("testValidMacAddress", testValidMacAddress), - ("testValidUUID", testValidUUID), - ("testValidMongoID", testValidMongoID), - ("testValidMD5", testValidMD5), - ("testValidBase64", testValidBase64), - ("testValidASCII", testValidASCII) - ] - - func testName() throws { - try "fancyUser".validated(by: Name()) - try Name().validate("fancyUser") - } - - func testPassword() throws { - do { - try "no".validated(by: !OnlyAlphanumeric() && Count.min(5)) - XCTFail("Should error") - } catch {} - - try "yes*/pass".validated(by: !OnlyAlphanumeric() && Count.min(5)) - } - - func testNot() { - XCTAssertFalse("a".passes(!OnlyAlphanumeric())) - } - - func testComposition() throws { - let contrived = Count.max(9) - || Count.min(11) - && Name() - && OnlyAlphanumeric() - - try "contrive".validated(by: contrived) + func testValidate() throws { + let user = User(name: "Tanner", age: 23, pet: Pet(name: "Zizek Pulaski")) + try user.validate() } - func testDetailedFailure() throws { - let fail = Count.min(10) - let pass = Count.max(30) - let combo = pass && fail - do { - let _ = try 2.tested(by: combo) - XCTFail("should throw error") - } catch let e as ErrorList { - XCTAssertEqual(e.errors.count, 1) + func testASCII() throws { + try IsASCII().validate(.string("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/")) + XCTAssertThrowsError(try IsASCII().validate(.string("ABCDEFGHIJKLMNOPQR🤠STUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))) { + XCTAssert($0 is ValidationError) } } - func testValidEmail() { - XCTAssertFalse("".passes(EmailValidator())) - XCTAssertFalse("@".passes(EmailValidator())) - XCTAssertFalse("@.".passes(EmailValidator())) - XCTAssertFalse("@.com".passes(EmailValidator())) - XCTAssertFalse("foo@.com".passes(EmailValidator())) - XCTAssertFalse("@foo.com".passes(EmailValidator())) - XCTAssertTrue("f@b.co".passes(EmailValidator())) - XCTAssertTrue("foo@bar.com".passes(EmailValidator())) - XCTAssertTrue("SOMETHING@SOMETHING.SOMETHING".passes(EmailValidator())) - XCTAssertTrue("foo!-bar!-baz@foo.bar".passes(EmailValidator())) - XCTAssertFalse("f@b.".passes(EmailValidator())) - XCTAssertFalse("æøå@gmail.com".passes(EmailValidator())) - } - - func testValidHexadecimal() { - XCTAssertTrue("abcdefABCDEF0123456789".passes(HexadecimalValidator())) - XCTAssertTrue("f3f3f3".passes(HexadecimalValidator())) - XCTAssertFalse("fefefo".passes(HexadecimalValidator())) - XCTAssertFalse("Not a valid hex".passes(HexadecimalValidator())) - } - - func testValidMacAddress() { - XCTAssertTrue("e6:5e:6c:10:77:d3".passes(MacAddressValidator())) - XCTAssertTrue("c6-59-50-94-3f-e0".passes(MacAddressValidator())) - XCTAssertTrue("7ab3.5f8d.f56e".passes(MacAddressValidator())) - } - - func testValidUUID() { - XCTAssertNoThrow(try UUIDValidator.uuid1.validate("6be2ff40-6a7d-11e7-907b-a6006ad3dba0")) - XCTAssertThrowsError(try UUIDValidator.uuid1.validate("8cfa13d0-6a7d-51e7-907b-a6006ad3dbb7")) - XCTAssertNoThrow(try UUIDValidator.uuid2.validate("ccc1bf4a-617d-21d7-8459-0023dffdb426")) - XCTAssertThrowsError(try UUIDValidator.uuid2.validate("e7938f74-6a7d-61b7-8e23-0063dffad455")) - XCTAssertNoThrow(try UUIDValidator.uuid3.validate("e106e283-4459-347c-b151-040c9b38c52a")) - XCTAssertThrowsError(try UUIDValidator.uuid3.validate("f17fb9d3-8c5e-1bd2-a0b7-45dd87dd4f7b")) - XCTAssertNoThrow(try UUIDValidator.uuid4.validate("e18fb0d3-8c5e-4bc2-aad7-45dd87cc447b")) - XCTAssertThrowsError(try UUIDValidator.uuid4.validate("6ad2f2d0-6a7e-11e7-97b2-0023dffdd425")) - XCTAssertNoThrow(try UUIDValidator.uuid5.validate("bac02c65-7aa9-5c49-abef-edfe6ad7100c")) - XCTAssertThrowsError(try UUIDValidator.uuid5.validate("f68fb0d3-8c5e-4bc2-aad7-45dd87cc447b")) - XCTAssertNoThrow(try UUIDValidator.uuid.validate("e18fb0d3-8c5e-4bc2-aad7-45dd87cc447b")) - XCTAssertThrowsError(try UUIDValidator.uuid.validate("will throw")) + func testAlphanumeric() throws { + try IsAlphanumeric().validate(.string("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789")) + XCTAssertThrowsError(try IsAlphanumeric().validate(.string("ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"))) { + XCTAssert($0 is ValidationError) + } } - func testValidMongoID() { - XCTAssertTrue("596b028b40ee490e12038866".passes(MongoIDValidator())) - XCTAssertTrue("507f1f77bcf86cd799439011".passes(MongoIDValidator())) - XCTAssertFalse("507z1f77zzf86ch799j39011".passes(MongoIDValidator())) - XCTAssertFalse("Not A Valid MongoId".passes(MongoIDValidator())) + func testEmail() throws { + try IsEmail().validate(.string("tanner@vapor.codes")) + XCTAssertThrowsError(try IsEmail().validate(.string("asdf"))) { XCTAssert($0 is ValidationError) } } - - func testValidMD5() { - XCTAssertTrue("d41d8cd98f00b204e9800998ecf8427e".passes(MD5Validator())) - XCTAssertTrue("e7211b1d5948d21eef524327740094b4".passes(MD5Validator())) - XCTAssertFalse("d6db347e8ee25f8dcffddad9d1207e1bf".passes(MD5Validator())) - XCTAssertFalse("19554ad801e6dd97903O9ed752b4a6ce".passes(MD5Validator())) + + func testCount() throws { + try IsCount(-5...5).validate(.int(4)) + try IsCount(-5...5).validate(.int(5)) + try IsCount(-5...5).validate(.int(-5)) + XCTAssertThrowsError(try IsCount(-5...5).validate(.int(6))) { XCTAssert($0 is ValidationError) } + XCTAssertThrowsError(try IsCount(-5...5).validate(.int(-6))) { XCTAssert($0 is ValidationError) } + + try IsCount(5...).validate(.uint(UInt.max)) + try IsCount(...(UInt.max - 100)).validate(.int(Int.min)) + + XCTAssertThrowsError(try IsCount(...Int.max).validate(.uint(UInt.max))) { XCTAssert($0 is ValidationError) } + + try IsCount(-5...5).validate(.uint(4)) + XCTAssertThrowsError(try IsCount(-5...5).validate(.uint(6))) { XCTAssert($0 is ValidationError) } + + try IsCount(-5..<6).validate(.int(-5)) + try IsCount(-5..<6).validate(.int(-4)) + try IsCount(-5..<6).validate(.int(5)) + XCTAssertThrowsError(try IsCount(-5..<6).validate(.int(-6))) { XCTAssert($0 is ValidationError) } + XCTAssertThrowsError(try IsCount(-5..<6).validate(.int(6))) { XCTAssert($0 is ValidationError) } } + + static var allTests = [ + ("testValidate", testValidate), + ("testASCII", testASCII), + ("testAlphanumeric", testAlphanumeric), + ("testEmail", testEmail), + ("testCount", testCount), + ] +} - func testValidBase64() { - XCTAssertTrue("SSBsb3ZlIFZhcG9y".passes(Base64Validator())) - XCTAssertTrue("VmFwb3IgPiBFeHByZXNz".passes(Base64Validator())) - XCTAssertFalse("bWFsZmalformedl9ybWVk".passes(Base64Validator())) - XCTAssertFalse("".passes(Base64Validator())) +final class User: Validatable { + var id: Int? + var name: String + var age: Int + var pet: Pet + + init(id: Int? = nil, name: String, age: Int, pet: Pet) { + self.id = id + self.name = name + self.age = age + self.pet = pet } + + static var validations: Validations = [ + key(\.name): IsCount(5...), + key(\.age): IsCount(3...), + key(\.pet.name): IsCount(5...) + ] +} - func testValidASCII() { - XCTAssertTrue(" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~".passes(ASCIIValidator())) - XCTAssertFalse("piędziesięciogroszówka".passes(ASCIIValidator())) +final class Pet: Codable { + var name: String + init(name: String) { + self.name = name } - } diff --git a/Tests/ValidationTests/ValidationUniqueTests.swift b/Tests/ValidationTests/ValidationUniqueTests.swift deleted file mode 100644 index 63eec1f..0000000 --- a/Tests/ValidationTests/ValidationUniqueTests.swift +++ /dev/null @@ -1,71 +0,0 @@ -// -// EventTests.swift -// Vapor -// -// Created by Logan Wright on 4/6/16. -// -// - -import XCTest -@_exported import Validation - -class ValidationUniqueTests: XCTestCase { - static let allTests = [ - ("testIntsArray", testIntsArray), - ("testStringArray", testStringArray) - ] - - func testIntsArray() { - let unique = [1, 2, 3, 4, 5, 6, 7, 8] - XCTAssertTrue(unique.passes(Unique())) - let notUnique = unique + unique - XCTAssertFalse(notUnique.passes(Unique())) - } - - func testStringArray() { - let unique = ["a", "b", "c", "d", "e"] - XCTAssertTrue(unique.passes(Unique())) - let notUnique = unique + unique - XCTAssertFalse(notUnique.passes(Unique())) - } -} - -class ValidationInTests: XCTestCase { - static let allTests = [ - ("testIntsArray", testIntsArray), - ("testStringArray", testStringArray), - ("testInArray", testInArray), - ("testInSet", testInSet) - ] - - func testIntsArray() { - let unique = [1, 2, 3, 4, 5, 6, 7, 8] - XCTAssertTrue(unique.passes(Unique())) - let notUnique = unique + unique - XCTAssertFalse(notUnique.passes(Unique())) - } - - func testStringArray() { - let unique = ["a", "b", "c", "d", "e"] - XCTAssertTrue(unique.passes(Unique())) - let notUnique = unique + unique - XCTAssertFalse(notUnique.passes(Unique())) - } - - func testInArray() { - let array = [0,1,2,3] - XCTAssertTrue(1.passes(In(array))) - XCTAssertFalse(13.passes(In(array))) - } - - func testInSet() throws { - let set = Set(["a", "b", "c"]) - // Run at least two tests w/ same validator instance that should be true to - // ensure that iteratorFactory is functioning properly - let validator = In(set) - XCTAssertTrue("a".passes(validator)) - XCTAssertTrue("b".passes(validator)) - XCTAssertFalse("b".passes(!In(set))) - XCTAssertFalse("nope".passes(validator)) - } -} diff --git a/circle.yml b/circle.yml index 47a39ca..035a42c 100644 --- a/circle.yml +++ b/circle.yml @@ -3,37 +3,23 @@ version: 2 jobs: macos: macos: - xcode: "9.0" + xcode: "9.2" steps: - - run: brew install vapor/tap/vapor - checkout - run: swift build - run: swift test - linux: docker: - - image: swift:4.0 + - image: norionomura/swift:swift-4.1-branch steps: - - run: apt-get install -y libssl-dev - checkout + - run: apt-get update + - run: apt-get install -yq libssl-dev - run: swift build - run: swift test - - # 3.1 backward compat checks - - linux-3: - docker: - - image: swift:3.1 - steps: - - run: apt-get install -y libssl-dev - - checkout - - run: swift build - - run: swift test - workflows: version: 2 tests: jobs: - linux - - linux-3 - - macos + # - macos