Skip to content
This repository was archived by the owner on Nov 16, 2020. It is now read-only.

Commit b2e2ecc

Browse files
committed
validation 2.0.0 gm
1 parent 0bfc5f4 commit b2e2ecc

19 files changed

+239
-183
lines changed

Package.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ let package = Package(
88
],
99
dependencies: [
1010
// 🌎 Utility package containing tools for byte manipulation, Codable, OS APIs, and debugging.
11-
.package(url: "https://github.com/vapor/core.git", from: "3.0.0-rc.2"),
11+
.package(url: "https://github.com/vapor/core.git", from: "3.0.0"),
1212
],
1313
targets: [
1414
// Validation

Sources/Validation/Exports.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
1+
@_exported import Core

Sources/Validation/Validatable.swift

+29-22
Original file line numberDiff line numberDiff line change
@@ -1,31 +1,37 @@
1-
import Core
2-
31
/// Capable of being validated.
4-
public protocol Validatable: Codable, ValidationDataRepresentable {
5-
/// The validations that will run when `.validate()`
6-
/// is called on an instance of this class.
2+
///
3+
/// final class User: Validatable, Reflectable {
4+
/// var id: Int?
5+
/// var name: String
6+
/// var age: Int
7+
///
8+
/// init(id: Int? = nil, name: String, age: Int) {
9+
/// self.id = id
10+
/// self.name = name
11+
/// self.age = age
12+
/// }
13+
///
14+
/// static var validations: Validations = [
15+
/// key(\.name): IsCount(5...) && IsAlphanumeric(), // Is at least 5 letters and alphanumeric
16+
/// key(\.age): IsCount(18...) // 18 or older
17+
/// ]
18+
/// }
19+
///
20+
public protocol Validatable {
21+
/// The validations that will run when `.validate()` is called on an instance of this class.
722
static var validations: Validations { get }
823
}
924

1025
extension Validatable {
11-
/// See ValidationDataRepresentable.makeValidationData()
12-
public func makeValidationData() -> ValidationData {
13-
return .validatable(self)
14-
}
15-
}
16-
17-
extension Validatable {
18-
/// Validates the model, throwing an error
19-
/// if any of the validations fail.
20-
/// note: non-validation errors may also be thrown
21-
/// should the validators encounter unexpected errors.
26+
/// Validates the model, throwing an error if any of the validations fail.
27+
/// - note: non-validation errors may also be thrown should the validators encounter unexpected errors.
2228
public func validate() throws {
2329
var errors: [ValidationError] = []
2430

2531
for (key, validation) in Self.validations.storage {
2632
/// fetch the value for the key path and
2733
/// convert it to validation data
28-
let data = (self[keyPath: key.keyPath] as ValidationDataRepresentable).makeValidationData()
34+
let data = try getValidationData(at: key)
2935

3036
/// run the validation, catching validation errors
3137
do {
@@ -37,14 +43,15 @@ extension Validatable {
3743
}
3844

3945
if !errors.isEmpty {
40-
throw ValidatableError(errors)
46+
throw ValidateError(errors)
4147
}
4248
}
4349
}
4450

45-
/// a collection of errors thrown by validatable
46-
/// models validations
47-
struct ValidatableError: ValidationError {
51+
// MARK: Private
52+
53+
/// a collection of errors thrown by validatable models validations
54+
fileprivate struct ValidateError: ValidationError {
4855
/// the errors thrown
4956
var errors: [ValidationError]
5057

@@ -61,7 +68,7 @@ struct ValidatableError: ValidationError {
6168
}
6269

6370
/// creates a new validatable error
64-
public init(_ errors: [ValidationError]) {
71+
init(_ errors: [ValidationError]) {
6572
self.errors = errors
6673
self.path = []
6774
}

Sources/Validation/ValidationData.swift

+7-8
Original file line numberDiff line numberDiff line change
@@ -11,15 +11,14 @@ public enum ValidationData {
1111
case double(Double)
1212
case array([ValidationData])
1313
case dictionary([String: ValidationData])
14-
case validatable(Validatable)
1514
case null
1615
}
1716

1817
/// Capable of being represented by validation data.
1918
/// Custom types you want to validate must conform to this protocol.
2019
public protocol ValidationDataRepresentable {
2120
/// Converts to validation data
22-
func makeValidationData() -> ValidationData
21+
func makeValidationData() throws -> ValidationData
2322
}
2423

2524
extension Bool: ValidationDataRepresentable {
@@ -129,34 +128,34 @@ extension Date: ValidationDataRepresentable {
129128

130129
extension Array: ValidationDataRepresentable {
131130
/// See ValidationDataRepresentable.makeValidationData
132-
public func makeValidationData() -> ValidationData {
131+
public func makeValidationData() throws -> ValidationData {
133132
var items: [ValidationData] = []
134133
for el in self {
135134
// FIXME: conditional conformance
136-
items.append((el as! ValidationDataRepresentable).makeValidationData())
135+
try items.append((el as! ValidationDataRepresentable).makeValidationData())
137136
}
138137
return .array(items)
139138
}
140139
}
141140

142141
extension Dictionary: ValidationDataRepresentable {
143142
/// See ValidationDataRepresentable.makeValidationData
144-
public func makeValidationData() -> ValidationData {
143+
public func makeValidationData() throws -> ValidationData {
145144
var items: [String: ValidationData] = [:]
146145
for (key, el) in self {
147146
// FIXME: conditional conformance
148-
items[(key as! String)] = (el as! ValidationDataRepresentable).makeValidationData()
147+
items[(key as! String)] = try (el as! ValidationDataRepresentable).makeValidationData()
149148
}
150149
return .dictionary(items)
151150
}
152151
}
153152

154153
extension Optional: ValidationDataRepresentable {
155154
/// See ValidationDataRepresentable.makeValidationData
156-
public func makeValidationData() -> ValidationData {
155+
public func makeValidationData() throws -> ValidationData {
157156
switch self {
158157
case .none: return .null
159-
case .some(let s): return (s as? ValidationDataRepresentable)?.makeValidationData() ?? .null
158+
case .some(let s): return try (s as? ValidationDataRepresentable)?.makeValidationData() ?? .null
160159
}
161160
}
162161
}

Sources/Validation/ValidationError.swift

+15-17
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,20 @@
11
import Debugging
22

3+
/// A validation error that supports dynamic key paths.
4+
public protocol ValidationError: Debuggable {
5+
/// Key path to the invalid data.
6+
var path: [String] { get set }
7+
}
8+
9+
extension ValidationError {
10+
/// See `Debuggable`.
11+
public var identifier: String {
12+
return "validationFailed"
13+
}
14+
}
15+
16+
// MARK: Basic
17+
318
/// Errors that can be thrown while working with validation
419
public struct BasicValidationError: ValidationError {
520
/// See Debuggable.reason
@@ -25,20 +40,3 @@ public struct BasicValidationError: ValidationError {
2540
self.path = []
2641
}
2742
}
28-
29-
/// A validation error that supports dynamic
30-
/// key paths.
31-
public protocol ValidationError: Debuggable {
32-
/// See Debuggable.reason
33-
var reason: String { get }
34-
35-
/// Key path the validation error happened at
36-
var path: [String] { get set }
37-
}
38-
39-
extension ValidationError {
40-
/// See Debuggable.identifier
41-
public var identifier: String {
42-
return "validationFailed"
43-
}
44-
}
+49
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
/// A model property containing the Swift key path for accessing it.
2+
public struct ValidationKey: Hashable {
3+
/// See `Hashable.hashValue`
4+
public var hashValue: Int {
5+
return keyPath.hashValue
6+
}
7+
8+
/// See `Equatable`
9+
public static func ==(lhs: ValidationKey, rhs: ValidationKey) -> Bool {
10+
return lhs.keyPath == rhs.keyPath
11+
}
12+
13+
/// The Swift keypath
14+
fileprivate var keyPath: AnyKeyPath
15+
16+
/// Path segments to this key's value.
17+
public var path: [String]
18+
19+
/// The property's type. Storing this as `Any` since we lost the type info converting to AnyKeyPAth
20+
public var type: Any.Type
21+
22+
/// Create a new `ValidationKey`.
23+
fileprivate init<T>(keyPath: AnyKeyPath, path: [String], type: T.Type) {
24+
self.keyPath = keyPath
25+
self.path = path
26+
self.type = type
27+
}
28+
}
29+
30+
extension Validatable {
31+
/// Accesses the `ValidationDataRepresentable`
32+
public func getValidationData(at validationKey: ValidationKey) throws -> ValidationData {
33+
return try (self[keyPath: validationKey.keyPath] as ValidationDataRepresentable).makeValidationData()
34+
}
35+
}
36+
37+
extension Validatable {
38+
/// Create a validation key for the supplied key path.
39+
public static func key<T>(_ keyPath: KeyPath<Self, T>, at path: [String]) -> ValidationKey where T: ValidationDataRepresentable {
40+
return ValidationKey(keyPath: keyPath, path: path, type: T.self)
41+
}
42+
}
43+
44+
extension Validatable where Self: Reflectable {
45+
/// Create a validation key for the supplied key path.
46+
public static func key<T>(_ keyPath: KeyPath<Self, T>) -> ValidationKey where T: ValidationDataRepresentable {
47+
return try! self.key(keyPath, at: Self.reflectProperty(forKey: keyPath)?.path ?? [])
48+
}
49+
}

Sources/Validation/Validations.swift

+2-63
Original file line numberDiff line numberDiff line change
@@ -1,73 +1,12 @@
1-
import Core
2-
31
public struct Validations: ExpressibleByDictionaryLiteral {
4-
/// Store the key and query field.
2+
/// Internal storage.
53
internal var storage: [ValidationKey: Validator]
64

7-
/// See ExpressibleByDictionaryLiteral
5+
/// See `ExpressibleByDictionaryLiteral`.
86
public init(dictionaryLiteral elements: (ValidationKey, Validator)...) {
97
self.storage = [:]
108
for (key, validator) in elements {
119
storage[key] = validator
1210
}
1311
}
1412
}
15-
16-
/// A model property containing the
17-
/// Swift key path for accessing it.
18-
public struct ValidationKey: Hashable {
19-
/// See `Hashable.hashValue`
20-
public var hashValue: Int {
21-
return keyPath.hashValue
22-
}
23-
24-
/// See `Equatable.==`
25-
public static func ==(lhs: ValidationKey, rhs: ValidationKey) -> Bool {
26-
return lhs.keyPath == rhs.keyPath
27-
}
28-
29-
/// The Swift keypath
30-
public var keyPath: AnyKeyPath
31-
32-
/// The respective CodingKey path.
33-
public var path: [String]
34-
35-
/// The properties type.
36-
/// Storing this as `Any` since we lost
37-
/// the type info converting to AnyKeyPAth
38-
public var type: Any.Type
39-
40-
/// True if the property on the model is optional.
41-
/// The `type` is the Wrapped type if this is true.
42-
public var isOptional: Bool
43-
44-
/// Create a new model key.
45-
internal init<T>(keyPath: AnyKeyPath, path: [String], type: T.Type, isOptional: Bool) {
46-
self.keyPath = keyPath
47-
self.path = path
48-
self.type = type
49-
self.isOptional = isOptional
50-
}
51-
}
52-
53-
extension Validatable where Self: Reflectable {
54-
/// Create a validation key for the supplied key path.
55-
public static func key<T>(_ path: KeyPath<Self, T>) -> ValidationKey where T: ValidationDataRepresentable {
56-
return try! ValidationKey(
57-
keyPath: path,
58-
path: Self.reflectProperty(forKey: path)?.path ?? [],
59-
type: T.self,
60-
isOptional: false
61-
)
62-
}
63-
64-
/// Create a validation key for the supplied key path.
65-
public static func key<T>(_ path: KeyPath<Self, T?>) -> ValidationKey where T: ValidationDataRepresentable {
66-
return try! ValidationKey(
67-
keyPath: path,
68-
path: Self.reflectProperty(forKey: path)?.path ?? [],
69-
type: T.self,
70-
isOptional: true
71-
)
72-
}
73-
}

Sources/Validation/Validator.swift

+6-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
/// Capable of validating validation data or throwing a validation error
22
public protocol Validator {
3-
/// used by the `NotValidator`
4-
var inverseMessage: String { get }
3+
/// Suitable for placing after `is` _and_ `is not`.
4+
///
5+
/// is alphanumeric
6+
/// is not alphanumeric
7+
///
8+
var validatorReadable: String { get }
59

610
/// validates the supplied data
711
func validate(_ data: ValidationData) throws

Sources/Validation/Validators/AndValidator.swift

+2-2
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,8 @@ public func && (lhs: Validator, rhs: Validator) -> Validator {
77
/// the validation will succeed.
88
internal struct AndValidator: Validator {
99
/// See Validator.inverseMessage
10-
public var inverseMessage: String {
11-
return "\(lhs.inverseMessage) and \(rhs.inverseMessage)"
10+
public var validatorReadable: String {
11+
return "\(lhs.validatorReadable) and \(rhs.validatorReadable)"
1212
}
1313

1414
/// left validator
+14-15
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,23 @@
11
import Foundation
22

33
/// Validates whether a string contains only ASCII characters
4-
public struct IsASCII: Validator {
5-
/// See Validator.inverseMessage
6-
public var inverseMessage: String {
7-
return "ASCII"
4+
public struct IsASCII: CharacterSetValidator {
5+
/// See `CharacterSetValidator`.
6+
public var validatorCharacterSet: CharacterSet {
7+
return .ascii
88
}
99

10-
/// creates a new ASCII validator
11-
public init() {}
10+
/// Creates a new `IsASCII` validator.
11+
public init() { }
12+
}
1213

13-
/// See Validator.validate
14-
public func validate(_ data: ValidationData) throws {
15-
switch data {
16-
case .string(let s):
17-
guard s.range(of: "^[\\x09-\\x0d -~]+$", options: [.regularExpression, .caseInsensitive]) != nil else {
18-
throw BasicValidationError("is not ASCII")
19-
}
20-
default:
21-
throw BasicValidationError("is not a string")
14+
extension CharacterSet {
15+
/// ASCII (byte 0..<128) character set.
16+
fileprivate static var ascii: CharacterSet {
17+
var ascii: CharacterSet = .init()
18+
for i in 0..<128 {
19+
ascii.insert(Unicode.Scalar(i)!)
2220
}
21+
return ascii
2322
}
2423
}

0 commit comments

Comments
 (0)