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