├── .gitignore ├── Tests ├── LinuxMain.swift └── FormsTests │ └── FormsTests.swift ├── .travis.yml ├── Package.swift ├── Sources └── Forms │ ├── Tags │ ├── LabelForField.swift │ ├── ValueForField.swift │ ├── ErrorsForField.swift │ ├── IfFieldHasErrors.swift │ └── LoopErrorsForField.swift │ ├── Provider.swift │ ├── Validator.swift │ ├── Validators │ ├── DatabaseValidators.swift │ ├── Int+Validators.swift │ ├── UInt+Validators.swift │ ├── Double+Validators.swift │ └── String+Validators.swift │ ├── Result.swift │ ├── Form.swift │ ├── Error.swift │ ├── Fieldset.swift │ └── Field.swift └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .build 2 | Packages 3 | *.xcodeproj 4 | Package.pins 5 | .DS_Store 6 | -------------------------------------------------------------------------------- /Tests/LinuxMain.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import FormsTests 3 | 4 | XCTMain([ 5 | testCase(FormsTests.allTests), 6 | ]) 7 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | os: 2 | - linux 3 | language: generic 4 | sudo: required 5 | dist: trusty 6 | script: 7 | - eval "$(curl -sL https://swift.vapor.sh/ci-3.1)" 8 | - eval "$(curl -sL https://swift.vapor.sh/codecov)" 9 | -------------------------------------------------------------------------------- /Package.swift: -------------------------------------------------------------------------------- 1 | import PackageDescription 2 | 3 | let package = Package( 4 | name: "Forms", 5 | dependencies: [ 6 | .Package(url: "https://github.com/vapor/validation-provider.git", majorVersion: 0), 7 | .Package(url: "https://github.com/vapor/leaf-provider.git", majorVersion: 0), 8 | .Package(url: "https://github.com/vapor/fluent-provider.git", majorVersion: 0), 9 | ] 10 | ) 11 | -------------------------------------------------------------------------------- /Sources/Forms/Tags/LabelForField.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | public final class LabelForField: BasicTag { 4 | public let name = "labelForField" 5 | 6 | // Arg1: Fieldset 7 | // Arg2: Field name 8 | public func run(arguments: [Argument]) throws -> Node? { 9 | guard 10 | arguments.count == 2, 11 | let fieldset = arguments[0].value?.object, 12 | let fieldName = arguments[1].value?.string, 13 | let label = fieldset[fieldName]?["label"] 14 | else { return nil } 15 | return label 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Forms/Tags/ValueForField.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | public final class ValueForField: BasicTag { 4 | public let name = "valueForField" 5 | 6 | // Arg1: Fieldset 7 | // Arg2: Field name 8 | public func run(arguments: [Argument]) throws -> Node? { 9 | guard 10 | arguments.count == 2, 11 | let fieldset = arguments[0].value?.object, 12 | let fieldName = arguments[1].value?.string, 13 | let value = fieldset[fieldName]?["value"] 14 | else { return nil } 15 | return value 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Forms/Tags/ErrorsForField.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | public final class ErrorsForField: BasicTag { 4 | public let name = "errorsForField" 5 | 6 | // Arg1: Fieldset 7 | // Arg2: Field name 8 | public func run(arguments: [Argument]) throws -> Node? { 9 | guard 10 | arguments.count == 2, 11 | let fieldset = arguments[0].value?.object, 12 | let fieldName = arguments[1].value?.string, 13 | let errors = fieldset[fieldName]?["errors"] 14 | else { return nil } 15 | return errors 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /Sources/Forms/Provider.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Leaf 3 | import VaporLeaf 4 | 5 | public final class Provider: Vapor.Provider { 6 | public init(config: Config) {} 7 | 8 | public func boot(_ drop: Droplet) { 9 | do { 10 | let stem = try drop.stem() 11 | let tags: [Tag] = [ 12 | ErrorsForField(), 13 | IfFieldHasErrors(), 14 | LabelForField(), 15 | LoopErrorsForField(), 16 | ValueForField() 17 | ] 18 | tags.forEach(stem.register) 19 | } catch {} 20 | } 21 | 22 | public func beforeRun(_ drop: Droplet) {} // Remove when able 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Forms/Tags/IfFieldHasErrors.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | public final class IfFieldHasErrors: Tag { 4 | public let name = "ifFieldHasErrors" 5 | 6 | public func run(stem: Stem, context: Context, tagTemplate: TagTemplate, arguments: [Argument]) throws -> Node? { 7 | return nil 8 | } 9 | 10 | // Arg1: Fieldset 11 | // Arg2: Field name 12 | // Render if there are errors in the field's errors array 13 | public func shouldRender(stem: Stem, context: Context, tagTemplate: TagTemplate, arguments: [Argument], value: Node?) -> Bool { 14 | guard 15 | arguments.count == 2, 16 | let fieldset = arguments[0].value?.object, 17 | let fieldName = arguments[1].value?.string, 18 | let errors = fieldset[fieldName]?["errors"]?.array 19 | else { return false } 20 | return !errors.isEmpty 21 | } 22 | 23 | } 24 | -------------------------------------------------------------------------------- /Sources/Forms/Validator.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | // This is a class because http://stackoverflow.com/a/40413937/284120 4 | /** 5 | Inherit from this class to implement a field validator. A field validator 6 | takes a single value of type `T` and returns a `FieldValidationResult`. 7 | */ 8 | open class FieldValidator { 9 | 10 | /** 11 | This initializer needs to be present until the following swift bug 12 | gets fixed, otherwise we can't subclass from this module 13 | https://bugs.swift.org/browse/SR-2295 14 | */ 15 | public init() {} 16 | 17 | /** 18 | Validate your value. If the validation was successful, return 19 | `.success(value)`. Otherwise, return 20 | `.failure([validationFailed(message: String)])` where the message is a 21 | string to be displayed to end-users of the form. 22 | */ 23 | open func validate(input value: T) -> FieldValidationResult { 24 | return .success(Node(nil)) 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /Sources/Forms/Tags/LoopErrorsForField.swift: -------------------------------------------------------------------------------- 1 | import Leaf 2 | 3 | public final class LoopErrorsForField: Tag { 4 | public let name = "loopErrorsForField" 5 | 6 | // Arg1: Fieldset 7 | // Arg2: Field name 8 | // Arg3: Constant name in loop 9 | // Render for each error message in field's error message array. 10 | public func run(stem: Stem, context: Context, tagTemplate: TagTemplate, arguments: [Argument]) throws -> Node? { 11 | guard 12 | arguments.count == 3, 13 | let fieldset = arguments[0].value?.object, 14 | let fieldName = arguments[1].value?.string, 15 | let constant = arguments[2].value?.string, 16 | let errors = fieldset[fieldName]?["errors"]?.array 17 | else { return nil } 18 | return .array(errors.map { [constant: $0] }) 19 | } 20 | 21 | public func render(stem: Stem, context: Context, value: Node?, leaf: Leaf) throws -> Bytes { 22 | guard let array = value?.array else { return "".makeBytes() } 23 | func renderItem(_ item: Node) throws -> Bytes { 24 | context.push(item) 25 | let rendered = try stem.render(leaf, with: context) 26 | context.pop() 27 | return rendered 28 | } 29 | return try array 30 | .map(renderItem) 31 | .flatMap { $0 + [.newLine] } 32 | } 33 | 34 | } 35 | -------------------------------------------------------------------------------- /Sources/Forms/Validators/DatabaseValidators.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Fluent 3 | 4 | /** 5 | Validates if the value exists on the database 6 | */ 7 | public class UniqueFieldValidator: FieldValidator { 8 | let column: String 9 | let additionalFilters: [(column:String, comparison:Filter.Comparison, value:String)]? 10 | let message: String? 11 | public init(column: String, additionalFilters: [(column:String, comparison:Filter.Comparison, value:String)]?=nil, message: String?=nil) { 12 | self.column = column 13 | self.additionalFilters = additionalFilters 14 | self.message = message 15 | } 16 | public override func validate(input value: String) -> FieldValidationResult { 17 | // Let's create the main filter 18 | do { 19 | let query = try ModelType.query() 20 | try query.filter(self.column, value) 21 | // If we have addition filters, add them 22 | if let filters = self.additionalFilters { 23 | for filter in filters { 24 | try query.filter(filter.column, filter.comparison, filter.value) 25 | } 26 | } 27 | // Check if any record exists 28 | if(try query.count() > 0){ 29 | return .failure([.validationFailed(message: message ?? "Value \(self.column) must be unique.")]) 30 | } 31 | // If not we have green light 32 | return .success(Node(value)) 33 | } catch { 34 | return .failure([.validationFailed(message: message ?? "Value \(self.column) must be unique.")]) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /Sources/Forms/Validators/Int+Validators.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | extension Int { 4 | 5 | /** 6 | Validates that the value is greater than or equal to a constraint. 7 | */ 8 | public class MinimumValidator: FieldValidator { 9 | let constraint: Int 10 | let message: String? 11 | public init(_ constraint: Int, message: String?=nil) { 12 | self.constraint = constraint 13 | self.message = message 14 | } 15 | public override func validate(input value: Int) -> FieldValidationResult { 16 | if value < constraint { 17 | return .failure([.validationFailed(message: message ?? "Value must be at least \(constraint).")]) 18 | } 19 | return .success(Node(value)) 20 | } 21 | } 22 | 23 | /** 24 | Validates that the value is less than or equal to a constraint. 25 | */ 26 | public class MaximumValidator: FieldValidator { 27 | let constraint: Int 28 | let message: String? 29 | public init(_ constraint: Int, message: String?=nil) { 30 | self.constraint = constraint 31 | self.message = message 32 | } 33 | public override func validate(input value: Int) -> FieldValidationResult { 34 | if value > constraint { 35 | return .failure([.validationFailed(message: message ?? "Value must be at most \(constraint).")]) 36 | } 37 | return .success(Node(value)) 38 | } 39 | } 40 | 41 | /** 42 | Validates that the value is equal to a constraint. 43 | */ 44 | public class ExactValidator: FieldValidator { 45 | let constraint: Int 46 | let message: String? 47 | public init(_ constraint: Int, message: String?=nil) { 48 | self.constraint = constraint 49 | self.message = message 50 | } 51 | public override func validate(input value: Int) -> FieldValidationResult { 52 | if value != constraint { 53 | return .failure([.validationFailed(message: message ?? "Value must be exactly \(constraint).")]) 54 | } 55 | return .success(Node(value)) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Forms/Validators/UInt+Validators.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | extension UInt { 4 | 5 | /** 6 | Validates that the value is greater than or equal to a constraint. 7 | */ 8 | public class MinimumValidator: FieldValidator { 9 | let constraint: UInt 10 | let message: String? 11 | public init(_ constraint: UInt, message: String?=nil) { 12 | self.constraint = constraint 13 | self.message = message 14 | } 15 | public override func validate(input value: UInt) -> FieldValidationResult { 16 | if value < constraint { 17 | return .failure([.validationFailed(message: message ?? "Value must be at least \(constraint).")]) 18 | } 19 | return .success(Node(value)) 20 | } 21 | } 22 | 23 | /** 24 | Validates that the value is less than or equal to a constraint. 25 | */ 26 | public class MaximumValidator: FieldValidator { 27 | let constraint: UInt 28 | let message: String? 29 | public init(_ constraint: UInt, message: String?=nil) { 30 | self.constraint = constraint 31 | self.message = message 32 | } 33 | public override func validate(input value: UInt) -> FieldValidationResult { 34 | if value > constraint { 35 | return .failure([.validationFailed(message: message ?? "Value must be at most \(constraint).")]) 36 | } 37 | return .success(Node(value)) 38 | } 39 | } 40 | 41 | /** 42 | Validates that the value is equal to a constraint. 43 | */ 44 | public class ExactValidator: FieldValidator { 45 | let constraint: UInt 46 | let message: String? 47 | public init(_ constraint: UInt, message: String?=nil) { 48 | self.constraint = constraint 49 | self.message = message 50 | } 51 | public override func validate(input value: UInt) -> FieldValidationResult { 52 | if value != constraint { 53 | return .failure([.validationFailed(message: message ?? "Value must be exactly \(constraint).")]) 54 | } 55 | return .success(Node(value)) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Forms/Validators/Double+Validators.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | extension Double { 4 | 5 | /** 6 | Validates that the value is greater than or equal to a constraint. 7 | */ 8 | public class MinimumValidator: FieldValidator { 9 | let constraint: Double 10 | let message: String? 11 | public init(_ constraint: Double, message: String?=nil) { 12 | self.constraint = constraint 13 | self.message = message 14 | } 15 | public override func validate(input value: Double) -> FieldValidationResult { 16 | if value < constraint { 17 | return .failure([.validationFailed(message: message ?? "Value must be at least \(constraint).")]) 18 | } 19 | return .success(Node(value)) 20 | } 21 | } 22 | 23 | /** 24 | Validates that the value is less than or equal to a constraint. 25 | */ 26 | public class MaximumValidator: FieldValidator { 27 | let constraint: Double 28 | let message: String? 29 | public init(_ constraint: Double, message: String?=nil) { 30 | self.constraint = constraint 31 | self.message = message 32 | } 33 | public override func validate(input value: Double) -> FieldValidationResult { 34 | if value > constraint { 35 | return .failure([.validationFailed(message: message ?? "Value must be at most \(constraint).")]) 36 | } 37 | return .success(Node(value)) 38 | } 39 | } 40 | 41 | /** 42 | Validates that the value is equal to a constraint. 43 | */ 44 | public class ExactValidator: FieldValidator { 45 | let constraint: Double 46 | let message: String? 47 | public init(_ constraint: Double, message: String?=nil) { 48 | self.constraint = constraint 49 | self.message = message 50 | } 51 | public override func validate(input value: Double) -> FieldValidationResult { 52 | if value != constraint { 53 | return .failure([.validationFailed(message: message ?? "Value must be exactly \(constraint).")]) 54 | } 55 | return .success(Node(value)) 56 | } 57 | } 58 | 59 | } 60 | -------------------------------------------------------------------------------- /Sources/Forms/Result.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | /** 4 | Returned after the validation of a single `ValidatableField`. 5 | 6 | If you are calling a `Validator` directly this result type is also used. 7 | */ 8 | public enum FieldValidationResult { 9 | /** 10 | The value was successfully validated. The value which was validated is 11 | returned in the associated value. 12 | 13 | Note that because of polymorphism, the return value is not guaranteed 14 | to be of the same type as the value passed in. For example, an 15 | `IntegerField` is able to accept a String value "42" which it will 16 | convert to an Int value 42 before validating. The value returned here 17 | will be the Int. 18 | */ 19 | case success(Node) 20 | /** 21 | The value did not pass at least one of the field's validators. The 22 | list of field validation errors, and their user-presentable error 23 | messgaes, are in the associated value. 24 | */ 25 | case failure([FieldError]) 26 | } 27 | 28 | /** 29 | Returned after the attempted validation of a `Fieldset`. 30 | */ 31 | public enum FieldsetValidationResult { 32 | /** 33 | The `Fieldset` validated correctly, and the valid results are in 34 | the associated value. The key is the name of the field, and the value 35 | is the validated value of the field. 36 | 37 | Note that because of polymorphism, the return value is not guaranteed 38 | to be of the same type as the value passed in. For example, an 39 | `IntegerField` is able to accept a String value "42" which it will 40 | convert to an Int value 42 before validating. The value returned here 41 | will be the Int. 42 | */ 43 | case success(validated: [String: Node]) 44 | /** 45 | The `Fieldset` did not pass at least one of the fieldset's validators, 46 | or required values were missing. `Fieldset.errors` is a 47 | keyed collection of fields and their errors, where the key is the field 48 | name and the value is an array of errors raised during that field's 49 | validation phase. 50 | 51 | `Fieldset.values` is the dictionary of values which were validated 52 | against. You can use this dictionary when re-rendering your HTML form to 53 | pre-fill the fields with the user's invalid input. 54 | */ 55 | case failure 56 | } 57 | -------------------------------------------------------------------------------- /Sources/Forms/Form.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | // TODO: Consider using Mirror to ensure that fields match properties on the Form. 4 | // Downside, what if you want to transform from the Field to the property? Or if there's 5 | // no matching property? Also Mirror can be a real performance hit. 6 | 7 | /** 8 | Conform to this protocol to create a re-usable, statically-typed form. 9 | 10 | When declaring your Form struct or class, you should also create your 11 | `Fieldset`. You will then be able to 12 | */ 13 | public protocol Form { 14 | /** 15 | Store your fieldset in this static var. 16 | */ 17 | static var fieldset: Fieldset { get } 18 | 19 | // TODO: Does this need to throw? We guarantee that `validated` is valid data, 20 | // if the fieldset is set up correctly, so it should be safe to extract 21 | // this data using implicitly-unwrapped optionals without needing to throw. 22 | /** 23 | Implement this initializer to finalise your `Form` object. It is called 24 | by the `Form` itself after your form is constructed with 25 | 26 | let formResult = FormObject.validating(request.context) 27 | guard case .success(let form) = formResult ... 28 | 29 | The values in `validated` are guaranteed to be valid for your `Fieldset`. 30 | In this initializer, you must map from each field to your struct or class's 31 | properties, like so: 32 | 33 | self.name = validated["name"]!.string 34 | 35 | If implicitly unwrapping your optionals is not something you would like to do, 36 | or if you need to perform 'whole-form' validation, this initializer throws so 37 | that you can throw an error: 38 | 39 | guard let name = validated["name"]?.string else { throw FormIncorrectlyConfigured } 40 | 41 | If you don't want a direct 1:1 mapping of fields to form values, you should do 42 | work on the validated values before storing them in your form instance. For example, 43 | uppercasing or lowercasing strings, combining two or more fields into a new field. 44 | */ 45 | init(validatedData: [String: Node]) throws 46 | } 47 | 48 | public extension Form { 49 | 50 | /** 51 | This is the standard entry point for creating a validated form. Pass in a `Context` 52 | object such as `request.data` to receive either a successful instantiation of a form 53 | with valid properties, or a validation failure containing an instance of the fieldset 54 | with helpful error messages split by field along with the data passed in so that you 55 | can render it as initial values in your HTML form. 56 | */ 57 | public init(validating content: Content) throws { 58 | var fieldset = Self.fieldset 59 | switch fieldset.validate(content) { 60 | case .failure: 61 | throw FormError.validationFailed(fieldset: fieldset) 62 | case .success(let validatedData): 63 | try self.init(validatedData: validatedData) 64 | } 65 | } 66 | 67 | public init(validating data: [String: Node]) throws { 68 | let content = Content() 69 | content.append(Node(data)) 70 | try self.init(validating: content) 71 | } 72 | 73 | } 74 | -------------------------------------------------------------------------------- /Sources/Forms/Error.swift: -------------------------------------------------------------------------------- 1 | import Node 2 | 3 | /** 4 | Represents an error in the field validation process. 5 | While they conform to `Error`, these are never thrown by Forms; 6 | instead, they are returned as part of a `...ValidationResult`. 7 | 8 | The `localizedDescription` of these errors is intended to be displayed 9 | to users as part of e.g. an HTML form. 10 | */ 11 | public enum FieldError: Error { 12 | /** 13 | The value did not pass validation. 14 | The `message` provides further information and can be displayed to users. 15 | */ 16 | case validationFailed(message: String) 17 | /** 18 | The field was marked as required, but a value was not provided. 19 | */ 20 | case requiredMissing 21 | 22 | public var localizedDescription: String { 23 | switch self { 24 | case .validationFailed(let message): 25 | return message 26 | case .requiredMissing: 27 | return "This field is required." 28 | } 29 | } 30 | 31 | } 32 | 33 | /** 34 | A custom key-value Collection containing arrays of `FieldError` values, each keyed 35 | by a `String`. 36 | 37 | This collection is used to represent all validation errors raised by a `Fieldset`. 38 | The key is the field name, and the value is the array of errors relating to that field. 39 | 40 | Subscripting is always safe: if the field does not exist in the collection, an empty 41 | array will be returned. 42 | */ 43 | public struct FieldErrorCollection: Error, ExpressibleByDictionaryLiteral { 44 | public typealias Key = String 45 | public typealias Value = [FieldError] 46 | 47 | private var contents: [Key: Value] 48 | 49 | /** 50 | When creating an instance by a dictionary literal, setting a key multiple times 51 | will append to the existing value, rather than replacing it. Therefore both of these 52 | are correct: 53 | 54 | ["fieldName": [error1, error2]] 55 | 56 | and: 57 | 58 | [ 59 | "fieldName": [error1], 60 | "fieldName": [error2], 61 | ] 62 | */ 63 | public init(dictionaryLiteral elements: (Key, Value)...) { 64 | contents = [:] 65 | for (key, value) in elements { 66 | self[key] += value 67 | } 68 | } 69 | 70 | /** 71 | Since a missing key always returns an empty array, it is safe to always append to 72 | this collection without needing to check for the existence of the key first. For 73 | example: 74 | 75 | errors["fieldName"].append(error1) 76 | errors["fieldName"].append(error2) 77 | */ 78 | public subscript (key: Key) -> Value { 79 | get { 80 | return contents[key] ?? [] 81 | } 82 | set { 83 | contents[key] = newValue 84 | } 85 | } 86 | 87 | /** 88 | `true` if there are no errors in the collection. 89 | */ 90 | public var isEmpty: Bool { 91 | return contents.isEmpty 92 | } 93 | 94 | } 95 | 96 | 97 | /** 98 | Represents an error in validating a Form. 99 | */ 100 | public enum FormError: Error { 101 | /** 102 | Validation of the Form failed. The Fieldset instance is returned with 103 | `errors` and `values` properties set to help with re-rendering the form. 104 | */ 105 | case validationFailed(fieldset: Fieldset) 106 | } 107 | -------------------------------------------------------------------------------- /Sources/Forms/Validators/String+Validators.swift: -------------------------------------------------------------------------------- 1 | import Foundation 2 | import Vapor 3 | import Validation 4 | 5 | extension String { 6 | 7 | /** 8 | Validates that the number of characters in the value is greater than or equal to a constraint. 9 | */ 10 | public class MinimumLengthValidator: FieldValidator { 11 | let characters: Int 12 | let message: String? 13 | public init(characters: Int, message: String?=nil) { 14 | self.characters = characters 15 | self.message = message 16 | } 17 | override public func validate(input value: String) -> FieldValidationResult { 18 | if value.characters.count < characters { 19 | return .failure([.validationFailed(message: message ?? "String must be at least \(characters) characters long.")]) 20 | } 21 | return .success(Node(value)) 22 | } 23 | } 24 | 25 | /** 26 | Validates that the number of characters in the value is less than or equal to a constraint. 27 | */ 28 | public class MaximumLengthValidator: FieldValidator { 29 | let characters: Int 30 | let message: String? 31 | public init(characters: Int, message: String?=nil) { 32 | self.characters = characters 33 | self.message = message 34 | } 35 | override public func validate(input value: String) -> FieldValidationResult { 36 | if value.characters.count > characters { 37 | return .failure([.validationFailed(message: message ?? "String must be at most \(characters) characters long.")]) 38 | } 39 | return .success(Node(value)) 40 | } 41 | } 42 | 43 | /** 44 | Validates that the number of characters in the value is equal to a constraint. 45 | */ 46 | public class ExactLengthValidator: FieldValidator { 47 | let characters: Int 48 | let message: String? 49 | public init(characters: Int, message: String?=nil) { 50 | self.characters = characters 51 | self.message = message 52 | } 53 | override public func validate(input value: String) -> FieldValidationResult { 54 | if value.characters.count != characters { 55 | return .failure([.validationFailed(message: message ?? "String must be exactly \(characters) characters long.")]) 56 | } 57 | return .success(Node(value)) 58 | } 59 | } 60 | 61 | /** 62 | Validates that the the value is a valid email address string. Does not 63 | validate that this email address actually exists, just that it is formatted 64 | correctly. 65 | */ 66 | public class EmailValidator: FieldValidator { 67 | let message: String? 68 | public init(message: String?=nil) { 69 | self.message = message 70 | } 71 | override public func validate(input value: String) -> FieldValidationResult { 72 | do { 73 | try Validation.EmailValidator().validate(value) 74 | } catch { 75 | return .failure([.validationFailed(message: message ?? "Enter a valid email address.")]) 76 | } 77 | return .success(Node(value)) 78 | } 79 | } 80 | 81 | /** 82 | Validates a string against the given regex 83 | */ 84 | public class RegexValidator: FieldValidator { 85 | let regex: String? 86 | let message: String? 87 | public init(regex: String?=nil, message: String?=nil) { 88 | self.regex = regex 89 | self.message = message 90 | } 91 | override public func validate(input value: String) -> FieldValidationResult { 92 | if let regex = self.regex { 93 | if let _ = value.range(of: regex, options: .regularExpression) { 94 | return .success(Node(value)) 95 | } 96 | } 97 | return .failure([.validationFailed(message: message ?? "Value did not match required format.")]) 98 | } 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /Sources/Forms/Fieldset.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | import Node 3 | import FormData 4 | 5 | /** 6 | A struct which contains a list of fields, each with their validators attached, 7 | and a list of field names which are required to receive a value for validation. 8 | 9 | Instantiate this struct with your choice of field names and `ValidatableField` 10 | instances, then call `validate()` with either a `Content` object (from 11 | `request.data`) or programmatically with a `[String: Node]` dictionary. 12 | 13 | If you have set a `finalValidationBlock`, then this block will be called after 14 | all fields have been validated. It won't be called if any fields have failed, 15 | so you can be sure that the data in `values` is clean. Use this closure to check 16 | any fields which depend on each other, and add to `errors` if the fieldset does 17 | not pass validation. 18 | 19 | A fieldset which does not pass validation will have the `values` and `errors` 20 | properties set. A fieldset can also be converted to a `Node` and passed directly 21 | to a view renderer. 22 | 23 | You can validate any `Content` object, so that means POSTed HTML form data, JSON, 24 | and GET query string data. 25 | 26 | A fieldset can be simply stored in a variable, but is most powerful when paired 27 | with a `Form`. 28 | 29 | To validate incoming data, the fieldset pulls the value out of the data structure 30 | using the field name, so if you are implementing an HTML form, the `name` of your 31 | inputs should match the field names in your fieldset. 32 | */ 33 | public struct Fieldset { 34 | // These are the field definitions. 35 | let fields: [String: ValidatableField] 36 | // These are the names of the fields that need answers. 37 | let requiredFieldNames: [String] 38 | // This block will be called after field validation for whole-fieldset validation. 39 | let finalValidationBlock: ((inout Fieldset) -> Void)? 40 | // This is passed-in data for the fields. Can be set manually, or is set at validation. 41 | // This data is never passed to validate() and is used only for rendering purposes. 42 | public var values: [String: Node] = [:] 43 | // These are field validation errors. Set at validation. 44 | public var errors: FieldErrorCollection = [:] 45 | 46 | public init(_ fields: [String: ValidatableField], requiring requiredFieldNames: [String]=[], finalValidationBlock: ((inout Fieldset) -> Void)?=nil) { 47 | self.fields = fields 48 | self.requiredFieldNames = requiredFieldNames 49 | self.finalValidationBlock = finalValidationBlock 50 | } 51 | 52 | public mutating func validate(_ content: Content) -> FieldsetValidationResult { 53 | var validatedData: [String: Node] = [:] 54 | values = [:] 55 | errors = [:] 56 | fields.forEach { fieldName, fieldDefinition in 57 | // For each field, see if there's a matching value in the Content 58 | // Fail if no matching value for a required field 59 | let value: Node 60 | if let nodeValue = content[fieldName] { 61 | value = nodeValue 62 | } 63 | // else if let field = content[fieldName] as? Field { 64 | // // Try to convert the Field body from bytes to a String 65 | // let fieldString: String 66 | // do { 67 | // fieldString = try String(bytes: field.part.body) 68 | // } catch { 69 | // performRequiredFieldCheck(for: fieldName) 70 | // return 71 | // } 72 | // value = Node(fieldString) 73 | // } 74 | else { 75 | performRequiredFieldCheck(for: fieldName) 76 | return 77 | } 78 | 79 | // Store the passed-in value to be returned later 80 | values[fieldName] = value 81 | // Now try to validate it against the field 82 | switch fieldDefinition.validate(value) { 83 | case .success(let validatedValue): 84 | validatedData[fieldName] = validatedValue 85 | case .failure(let fieldErrors): 86 | fieldErrors.forEach { errors[fieldName].append($0) } // TODO: allow append a list not individual items 87 | } 88 | } 89 | // Do any whole-form validation if the fields themselves validated fine 90 | if errors.isEmpty { 91 | finalValidationBlock?(&self) 92 | } 93 | // Now return 94 | if !errors.isEmpty { 95 | return .failure 96 | } 97 | return .success(validated: validatedData) 98 | } 99 | 100 | public mutating func validate(_ values: [String: Node]) -> FieldsetValidationResult { 101 | let content = Content() 102 | content.append(Node(values)) 103 | return validate(content) 104 | } 105 | 106 | private mutating func performRequiredFieldCheck(for fieldName: String) { 107 | if requiredFieldNames.contains(fieldName) { 108 | errors[fieldName].append(.requiredMissing) 109 | } 110 | } 111 | } 112 | 113 | extension Fieldset: NodeRepresentable { 114 | public func makeNode(in context: Context?) throws -> Node { 115 | /* 116 | [ 117 | "name": [ 118 | "label": "Your name", 119 | "value": "bob", 120 | "errors: [ 121 | "Name should be longer than 3 characters." 122 | ], 123 | ] 124 | */ 125 | var object: [String: Node] = [:] 126 | fields.forEach { fieldName, fieldDefinition in 127 | var fieldNode: [String: Node] = [ 128 | "label": Node(fieldDefinition.label), 129 | ] 130 | if let value = values[fieldName] { 131 | fieldNode["value"] = value 132 | } 133 | let fieldErrors = errors[fieldName] 134 | if !fieldErrors.isEmpty { 135 | fieldNode["errors"] = Node(fieldErrors.map { Node($0.localizedDescription) }) 136 | } 137 | object[fieldName] = Node(fieldNode) 138 | } 139 | return Node(object) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /Sources/Forms/Field.swift: -------------------------------------------------------------------------------- 1 | import Vapor 2 | 3 | /** 4 | A field which is able to validate a value. 5 | Adopt this protocol to be able to be included in a `Fieldset`. 6 | */ 7 | public protocol ValidatableField { 8 | /** 9 | The UI implementor should use this label when displaying the fieldset. 10 | */ 11 | var label: String { get } 12 | func validate(_: Node) -> FieldValidationResult 13 | } 14 | 15 | /** 16 | A field which can receive a `String` value and optionally validate it before 17 | returning a `FieldValidationResult`. 18 | */ 19 | public struct StringField: ValidatableField { 20 | public let label: String 21 | let validators: [FieldValidator] 22 | public init(label: String="", _ validators: FieldValidator...) { 23 | self.label = label 24 | self.validators = validators 25 | } 26 | public func validate(_ value: Node) -> FieldValidationResult { 27 | // In theory, this shouldn't ever really fail 28 | guard let string = value.string else { 29 | return .failure([.validationFailed(message: "Please enter valid text.")]) 30 | } 31 | let errors: [FieldError] = validators.reduce([]) { accumulated, validator in 32 | if case .failure(let errors) = validator.validate(input: string) { return accumulated + errors } 33 | return accumulated 34 | } 35 | return errors.isEmpty ? .success(Node(string)) : .failure(errors) 36 | } 37 | } 38 | 39 | /** 40 | A field which can receive an `Int` value and optionally validate it before 41 | returning a `FieldValidationResult`. 42 | */ 43 | public struct IntegerField: ValidatableField { 44 | public let label: String 45 | let validators: [FieldValidator] 46 | public init(label: String="", _ validators: FieldValidator...) { 47 | self.label = label 48 | self.validators = validators 49 | } 50 | public func validate(_ value: Node) -> FieldValidationResult { 51 | // Retrieving value.int, if value is a Double, will force-convert it to an Int which is 52 | // not what we want so we have to filter out all Doubles first. 53 | // This has the unfortunate side-effect of excluding any Doubles which are in fact whole numbers. 54 | if case .number(let number) = value.wrapped, case .double = number { 55 | return .failure([.validationFailed(message: "Please enter a whole number.")]) 56 | } 57 | guard let int = value.int else { 58 | return .failure([.validationFailed(message: "Please enter a whole number.")]) 59 | } 60 | let errors: [FieldError] = validators.reduce([]) { accumulated, validator in 61 | if case .failure(let errors) = validator.validate(input: int) { return accumulated + errors } 62 | return accumulated 63 | } 64 | return errors.isEmpty ? .success(Node(int)) : .failure(errors) 65 | } 66 | } 67 | 68 | /** 69 | A field which can receive a `UInt` value and optionally validate it before 70 | returning a `FieldValidationResult`. 71 | */ 72 | public struct UnsignedIntegerField: ValidatableField { 73 | public let label: String 74 | let validators: [FieldValidator] 75 | public init(label: String="", _ validators: FieldValidator...) { 76 | self.label = label 77 | self.validators = validators 78 | } 79 | public func validate(_ value: Node) -> FieldValidationResult { 80 | // Filter out Doubles (see comments in IntegerField) and negative Ints first. 81 | if case .number(let number) = value.wrapped, case .double = number { 82 | return .failure([.validationFailed(message: "Please enter a positive whole number.")]) 83 | } 84 | if case .number(let number) = value.wrapped, case .int(let int) = number, int < 0 { 85 | return .failure([.validationFailed(message: "Please enter a positive whole number.")]) 86 | } 87 | guard let uint = value.uint else { 88 | return .failure([.validationFailed(message: "Please enter a positive whole number.")]) 89 | } 90 | let errors: [FieldError] = validators.reduce([]) { accumulated, validator in 91 | if case .failure(let errors) = validator.validate(input: uint) { return accumulated + errors } 92 | return accumulated 93 | } 94 | return errors.isEmpty ? .success(Node(uint)) : .failure(errors) 95 | } 96 | } 97 | 98 | /** 99 | A field which can receive a `Double` value and optionally validate it before 100 | returning a `FieldValidationResult`. 101 | */ 102 | public struct DoubleField: ValidatableField { 103 | public let label: String 104 | let validators: [FieldValidator] 105 | public init(label: String="", _ validators: FieldValidator...) { 106 | self.label = label 107 | self.validators = validators 108 | } 109 | public func validate(_ value: Node) -> FieldValidationResult { 110 | guard let double = value.double else { 111 | return .failure([.validationFailed(message: "Please enter a number.")]) 112 | } 113 | let errors: [FieldError] = validators.reduce([]) { accumulated, validator in 114 | if case .failure(let errors) = validator.validate(input: double) { return accumulated + errors } 115 | return accumulated 116 | } 117 | return errors.isEmpty ? .success(Node(double)) : .failure(errors) 118 | } 119 | } 120 | 121 | /** 122 | A field which can receive a `Bool` value and optionally validate it before 123 | returning a `FieldValidationResult`. 124 | */ 125 | public struct BoolField: ValidatableField { 126 | public let label: String 127 | let validators: [FieldValidator] 128 | public init(label: String="", _ validators: FieldValidator...) { 129 | self.label = label 130 | self.validators = validators 131 | } 132 | public func validate(_ value: Node) -> FieldValidationResult { 133 | // If POSTing from a `checkbox` style input, the key's absence means `false`. 134 | let bool = value.bool ?? false 135 | let errors: [FieldError] = validators.reduce([]) { accumulated, validator in 136 | if case .failure(let errors) = validator.validate(input: bool) { return accumulated + errors } 137 | return accumulated 138 | } 139 | return errors.isEmpty ? .success(Node(bool)) : .failure(errors) 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Forms 2 | 3 | ![Swift](http://img.shields.io/badge/swift-3.1-brightgreen.svg) 4 | ![Vapor](http://img.shields.io/badge/vapor-1.5-brightgreen.svg) 5 | ![Travis](https://travis-ci.org/vapor-community/forms.svg?branch=master) 6 | 7 | --- 8 | 9 | **Vapor 2**: this package is *not* compatible with Vapor 2 beta. Once Vapor 2 10 | moves out of beta, this package will be updated. 11 | 12 | --- 13 | 14 | Brings simple, dynamic and re-usable web form handling to 15 | [Vapor](https://github.com/vapor/vapor). 16 | 17 | This library is being used in production and should be safe, but as an early 18 | release the API is subject to change. 19 | 20 | Don't forget to add to your `providers` if you want to use built-in Leaf tags: 21 | 22 | ```swift 23 | import Vapor 24 | import Forms 25 | 26 | let drop = Droplet() 27 | try drop.addProvider(Forms.Provider.self) 28 | ``` 29 | 30 | ## Features 31 | 32 | Create a `Fieldset` on the fly: 33 | 34 | ```swift 35 | let fieldset = Fieldset([ 36 | "firstName": StringField(label: "First Name"), 37 | "lastName": StringField(label: "Last Name"), 38 | ]) 39 | ``` 40 | 41 | and add validation: 42 | 43 | ```swift 44 | let fieldset = Fieldset([ 45 | "firstName": StringField(), 46 | "lastName": StringField(), 47 | "email": StringField(String.EmailValidator()), 48 | ], requiring: ["email"]) 49 | ``` 50 | 51 | You can add multiple validators, too: 52 | 53 | ```swift 54 | let fieldset = Fieldset([ 55 | "firstName": StringField(label: "First Name", 56 | String.MinimumLengthValidator(characters: 3), 57 | String.MaximumLengthValidator(characters: 255), 58 | ), 59 | "lastName": StringField(label: "Last Name", 60 | String.MinimumLengthValidator(characters: 3), 61 | String.MaximumLengthValidator(characters: 255), 62 | ), 63 | "email": StringField(String.EmailValidator()), 64 | ], requiring: ["email"]) 65 | ``` 66 | 67 | And even add whole-fieldset validation after individual field validators have run: 68 | 69 | ```swift 70 | static let loginFieldset = Fieldset([ 71 | "username": StringField(label: "Username"), 72 | "password": StringField(label: "Password"), 73 | ], requiring: ["username", "password"]) { fieldset in 74 | let loginResult = validateCredentials(username: fieldset.values["username"]!.string!, password: fieldset.values["password"]!.string!) 75 | if !loginResult { 76 | fieldset.errors["password"].append(FieldError.validationFailed(message: "Username and password not valid")) 77 | } 78 | } 79 | ``` 80 | 81 | Validate from a `request`: 82 | 83 | ```swift 84 | fieldset.validate(request.data) 85 | ``` 86 | 87 | or even from a simple object: 88 | 89 | ```swift 90 | fieldset.validate([ 91 | "firstName": "Peter", 92 | "lastName": "Pan", 93 | ]) 94 | ``` 95 | 96 | Validation results: 97 | 98 | ```swift 99 | switch fieldset.validate(request.data) { 100 | case .success(let validatedData): 101 | // validatedData is guaranteed to contain correct field names and values. 102 | let user = User( 103 | firstName: validatedData["firstName"]!.string!, 104 | lastName: validatedData["lastName"]!.string!, 105 | ) 106 | case .failure: 107 | // Use the field names and failed validation messages in `fieldset.errors`, 108 | // and the passed-in values in `fieldset.values` to re-render your form. 109 | // If a single field fails multiple validators, you'll receive 110 | // an error string for each rather than just failing at the first 111 | // validator. 112 | } 113 | ``` 114 | 115 | Gain strongly-typed results by wrapping the `Fieldset` in a re-usable `Form`. 116 | 117 | ```swift 118 | struct UserForm: Form { 119 | let firstName: String 120 | let lastName: String 121 | let email: String 122 | 123 | static let fieldset = Fieldset([ 124 | "firstName": StringField(), 125 | "lastName": StringField(), 126 | "email": StringField(String.EmailValidator()), 127 | ], requiring: ["firstName", "lastName", "email"]) 128 | 129 | init(validatedData: [String: Node]) throws { 130 | // validatedData is guaranteed to contain correct field names and values. 131 | firstName = validatedData["firstName"]!.string! 132 | lastName = validatedData["lastName"]!.string! 133 | email = validatedData["email"]!.string! 134 | } 135 | } 136 | ``` 137 | 138 | Now you can easily and type-safely access the results of your form validation. 139 | 140 | ```swift 141 | drop.get { request in 142 | do { 143 | let form = try UserForm(validating: request.data) 144 | // Return to your view, or use the properties to save a Model instance. 145 | return "Hello \(form.firstName) \(form.lastName)" 146 | } catch FormError.validationFailed(let fieldset) { 147 | // Use the leaf tags on the fieldset to re-render your form. 148 | return try drop.view.make("index", [ 149 | "fieldset": fieldset, 150 | ]) 151 | } 152 | } 153 | ``` 154 | 155 | Rendering a form with validation error messages: 156 | 157 | ```html 158 | 159 | 160 | #ifFieldHasErrors(fieldset, "name") {
    } 161 | #loopErrorsForField(fieldset, "name", "message") {
  • #(message)
  • } 162 | #ifFieldHasErrors(fieldset, "name") {
} 163 | ``` 164 | 165 | Rendering a `select`: 166 | 167 | ```html 168 | 174 | ``` 175 | 176 | ## Documentation 177 | 178 | See the extensive tests file for full usage while in early development. 179 | Built-in validators are in the `Validators` directory. 180 | Proper documentation to come. 181 | 182 | ## Known issues 183 | 184 | So far, everything works as it says on the tin. 185 | 186 | There are some unfortunate design aspects, though, which the author hopes to 187 | straighten out. 188 | 189 | One of Swift's greatest assets is strong typing, but this library largely 190 | bypasses all those benefits. This is due to limitations in both Swift's 191 | introspection mechanism, and the author's general intelligence. The `Form` 192 | protocol is an attempt to resolve this lack; *in theory*, when the end-user 193 | fills out their `fields` property and `init` method correctly there should 194 | be no problems, but it would be nice for the compiler to catch any typos 195 | before the app runs. Using an `enum` for field names would be a good idea. 196 | 197 | The majority of the library uses `...ValidationResult` enums to return useful 198 | information about the success or failure of validation. However, the `Form` 199 | protocol also `throws` because the mapping of validated data to instance 200 | property is implemented by the end-user and errors may arise. 201 | 202 | Vapor's `Node` is heavily used, as is `Content`. Unfortunately, the built-in 203 | [validation](https://vapor.github.io/documentation/guide/validation.html) 204 | is (despite the author's best efforts) almost completely unused. Future work 205 | may be able to converge the two validation mechanisms enough that this library 206 | doesn't need to supply its own. 207 | -------------------------------------------------------------------------------- /Tests/FormsTests/FormsTests.swift: -------------------------------------------------------------------------------- 1 | import XCTest 2 | @testable import Forms 3 | @testable import Vapor 4 | import Leaf 5 | import Fluent 6 | import FormData 7 | import Multipart 8 | import HTTP 9 | 10 | /** 11 | Layout of the vapor-forms library 12 | - Value: it's a Node for easy Vapor interoperability 13 | - Validator: a thing which operates on a Type (String, Int, etc) and checks a Value against its own validation rules. 14 | It returns FieldValidationResult .success or .failure(FieldErrorCollection). 15 | - Field: a thing which accepts a certain type of Value and holds a number of Validators. It checks a Value against 16 | its Validators and returns FieldValidationResult .success or .failure(FieldErrorCollection) 17 | - Fieldset: a collection of Fields which can take an input ValueSet and validate the whole lot against its Fields. 18 | It returns .success(ValueSet) or .failure(FieldErrorCollection, ValueSet) 19 | - Form: a protocol for a struct to make a reusable form out of Fieldsets. Can throw because the init needs to 20 | be implemented by the client (mapping fields to struct properties). 21 | 22 | Errors: 23 | - FieldError: is an enum of possible error types. 24 | - FieldErrorCollection: is a specialised collection mapping FieldErrorCollection to field names as String. 25 | 26 | Result sets: 27 | - FieldValidationResult is either empty .success or .failure(FieldErrorCollection) 28 | - FieldsetValidationResult is either .success([String: Value]) or .failure(FieldErrorCollection, [String: Value]) 29 | 30 | 31 | TDD: things a form should do 32 | ✅ it should be agnostic as to form-encoded, GET, JSON, etc 33 | ✅ have fields with field types and validation, special case for optionals 34 | ✅ validate data when inited with request data 35 | ✅ throw useful validation errors 36 | - provide useful information on each field to help generate HTML forms but not actually generate them 37 | */ 38 | 39 | class FormsTests: XCTestCase { 40 | static var allTests : [(String, (FormsTests) -> () throws -> Void)] { 41 | return [ 42 | // ValidationErrors struct 43 | ("testValidationErrorsDictionaryLiteral", testValidationErrorsDictionaryLiteral), 44 | ("testValidationErrorsCreateByAppending", testValidationErrorsCreateByAppending), 45 | // Field validation 46 | ("testFieldStringValidation", testFieldStringValidation), 47 | ("testFieldEmailValidation", testFieldEmailValidation), 48 | ("testFieldIntegerValidation", testFieldIntegerValidation), 49 | ("testFieldUnsignedIntegerValidation", testFieldUnsignedIntegerValidation), 50 | ("testFieldDoubleValidation", testFieldDoubleValidation), 51 | ("testFieldBoolValidation", testFieldBoolValidation), 52 | ("testUniqueFieldValidation", testUniqueFieldValidation), 53 | // Fieldset 54 | ("testSimpleFieldset", testSimpleFieldset), 55 | ("testSimpleFieldsetGetInvalidData", testSimpleFieldsetGetInvalidData), 56 | ("testSimpleFieldsetWithPostValidation", testSimpleFieldsetWithPostValidation), 57 | // Form 58 | ("testSimpleForm", testSimpleForm), 59 | ("testFormValidation", testFormValidation), 60 | // Binding 61 | ("testValidateFromContentObject", testValidateFromContentObject), 62 | ("testValidateFormFromContentObject", testValidateFormFromContentObject), 63 | // Leaf tags 64 | ("testTagErrorsForField", testTagErrorsForField), 65 | ("testTagIfFieldHasErrors", testTagIfFieldHasErrors), 66 | ("testTagLoopErrorsForField", testTagLoopErrorsForField), 67 | ("testTagValueForField", testTagValueForField), 68 | ("testTagLabelForField", testTagLabelForField), 69 | // Whole thing use case 70 | ("testWholeFieldsetUsage", testWholeFieldsetUsage), 71 | ("testWholeFormUsage", testWholeFormUsage), 72 | ("testSampleLoginForm", testSampleLoginForm), 73 | ("testSampleLoginFormWithMultipart", testSampleLoginFormWithMultipart), 74 | ] 75 | } 76 | 77 | override func setUp(){ 78 | Database.default = Database(TestDriver()) 79 | } 80 | 81 | func expectMatch(_ test: FieldValidationResult, _ match: Node, fail: () -> Void) { 82 | switch test { 83 | case .success(let value) where value == match: 84 | break 85 | default: 86 | fail() 87 | } 88 | } 89 | func expectSuccess(_ test: FieldValidationResult, fail: () -> Void) { 90 | switch test { 91 | case .success: break 92 | case .failure: fail() 93 | } 94 | } 95 | func expectFailure(_ test: FieldValidationResult, fail: () -> Void) { 96 | switch test { 97 | case .success: fail() 98 | case .failure: break 99 | } 100 | } 101 | 102 | func expectSuccess(_ test: FieldsetValidationResult, fail: () -> Void) { 103 | switch test { 104 | case .success: break 105 | case .failure: fail() 106 | } 107 | } 108 | func expectFailure(_ test: FieldsetValidationResult, fail: () -> Void) { 109 | switch test { 110 | case .success: fail() 111 | case .failure: break 112 | } 113 | } 114 | 115 | // MARK: ValidationErrors struct 116 | 117 | func testValidationErrorsDictionaryLiteral() { 118 | // Must be able to be instantiated by dictionary literal 119 | let error1 = FieldError.requiredMissing 120 | let error2 = FieldError.requiredMissing 121 | let errors: FieldErrorCollection = ["key": [error1, error2]] 122 | XCTAssertEqual(errors["key"].count, 2) 123 | // Another way of instantiating 124 | let errors2: FieldErrorCollection = [ 125 | "key": [error1], 126 | "key": [error2], 127 | ] 128 | XCTAssertEqual(errors2["key"].count, 2) 129 | } 130 | 131 | func testValidationErrorsCreateByAppending() { 132 | // Must be able to be instantiated mutably 133 | let error1 = FieldError.requiredMissing 134 | let error2 = FieldError.requiredMissing 135 | var errors: FieldErrorCollection = [:] 136 | XCTAssertEqual(errors["key"].count, 0) 137 | errors["key"].append(error1) 138 | XCTAssertEqual(errors["key"].count, 1) 139 | errors["key"].append(error2) 140 | XCTAssertEqual(errors["key"].count, 2) 141 | } 142 | 143 | // MARK: Field validation 144 | 145 | func testFieldStringValidation() { 146 | // Correct value should succeed 147 | expectMatch(StringField().validate("string"), Node("string")) { XCTFail() } 148 | // Incorrect value type should fail 149 | expectFailure(StringField().validate(nil)) { XCTFail() } 150 | // Value too short should fail 151 | expectFailure(StringField(String.MinimumLengthValidator(characters: 12)).validate("string")) { XCTFail() } 152 | // Value too long should fail 153 | expectFailure(StringField(String.MaximumLengthValidator(characters: 6)).validate("maxi string")) { XCTFail() } 154 | // Value not exact size should fail 155 | expectFailure(StringField(String.ExactLengthValidator(characters: 6)).validate("wrong size")) { XCTFail() } 156 | // Value in regex should succeed 157 | expectSuccess(StringField(String.RegexValidator(regex: "^[a-z]{6}$")).validate("string")) { XCTFail() } 158 | // Value in regex should failure 159 | expectFailure(StringField(String.RegexValidator(regex: "^[a-z]{6}$")).validate("string 1s wr0ng")) { XCTFail() } 160 | } 161 | 162 | func testFieldEmailValidation() { 163 | // Correct value should succeed 164 | expectMatch(StringField(String.EmailValidator()).validate("email@email.com"), "email@email.com") { XCTFail() } 165 | // Incorrect value type should fail 166 | expectFailure(StringField(String.EmailValidator()).validate(nil)) { XCTFail() } 167 | // Value too long should fail 168 | expectFailure(StringField(String.EmailValidator(), String.MaximumLengthValidator(characters: 6)).validate("email@email.com")) { XCTFail() } 169 | // Value not of email type should fail 170 | expectFailure(StringField(String.EmailValidator()).validate("not an email")) { XCTFail() } 171 | } 172 | 173 | func testFieldIntegerValidation() { 174 | // Correct value should succeed 175 | expectMatch(IntegerField().validate(42), Node(42)) { XCTFail() } 176 | expectMatch(IntegerField().validate("42"), Node(42)) { XCTFail() } 177 | expectMatch(IntegerField().validate(-42), Node(-42)) { XCTFail() } 178 | expectMatch(IntegerField().validate("-42"), Node(-42)) { XCTFail() } 179 | // Incorrect value type should fail 180 | expectFailure(IntegerField().validate(nil)) { XCTFail() } 181 | expectFailure(IntegerField().validate("I'm a string")) { XCTFail() } 182 | // Non-integer number should fail 183 | expectFailure(IntegerField().validate(3.4)) { XCTFail() } 184 | expectFailure(IntegerField().validate("3.4")) { XCTFail() } 185 | // Value too low should fail 186 | expectFailure(IntegerField(Int.MinimumValidator(42)).validate(4)) { XCTFail() } 187 | // Value too high should fail 188 | expectFailure(IntegerField(Int.MaximumValidator(42)).validate(420)) { XCTFail() } 189 | // Value not exact should fail 190 | expectFailure(IntegerField(Int.ExactValidator(42)).validate(420)) { XCTFail() } 191 | } 192 | 193 | func testFieldUnsignedIntegerValidation() { 194 | // Correct value should succeed 195 | expectMatch(UnsignedIntegerField().validate(42), Node(42)) { XCTFail() } 196 | expectMatch(UnsignedIntegerField().validate("42"), Node(42)) { XCTFail() } 197 | // Incorrect value type should fail 198 | expectFailure(UnsignedIntegerField().validate(nil)) { XCTFail() } 199 | expectFailure(UnsignedIntegerField().validate("I'm a string")) { XCTFail() } 200 | // Non-integer number should fail 201 | expectFailure(UnsignedIntegerField().validate(3.4)) { XCTFail() } 202 | expectFailure(UnsignedIntegerField().validate("3.4")) { XCTFail() } 203 | // Negative integer number should fail 204 | expectFailure(UnsignedIntegerField().validate(-42)) { XCTFail() } 205 | expectFailure(UnsignedIntegerField().validate("-42")) { XCTFail() } 206 | // Value too low should fail 207 | expectFailure(UnsignedIntegerField(UInt.MinimumValidator(42)).validate(4)) { XCTFail() } 208 | expectSuccess(UnsignedIntegerField(UInt.MinimumValidator(42)).validate(44)) { XCTFail() } 209 | // Value too high should fail 210 | expectFailure(UnsignedIntegerField(UInt.MaximumValidator(42)).validate(420)) { XCTFail() } 211 | // Value not exact should fail 212 | expectFailure(UnsignedIntegerField(UInt.ExactValidator(42)).validate(420)) { XCTFail() } 213 | } 214 | 215 | func testFieldDoubleValidation() { 216 | // Correct value should succeed 217 | expectMatch(DoubleField().validate(42.42), Node(42.42)) { XCTFail() } 218 | expectMatch(DoubleField().validate("42.42"), Node(42.42)) { XCTFail() } 219 | expectMatch(DoubleField().validate(-42.42), Node(-42.42)) { XCTFail() } 220 | expectMatch(DoubleField().validate("-42.42"), Node(-42.42)) { XCTFail() } 221 | // OK to enter an int here too 222 | expectMatch(DoubleField().validate(42), Node(42)) { XCTFail() } 223 | expectMatch(DoubleField().validate("42"), Node(42)) { XCTFail() } 224 | // Incorrect value type should fail 225 | expectFailure(DoubleField().validate(nil)) { XCTFail() } 226 | expectFailure(DoubleField().validate("I'm a string")) { XCTFail() } 227 | // Value too low should fail 228 | expectFailure(DoubleField(Double.MinimumValidator(4.2)).validate(4.0)) { XCTFail() } 229 | // Value too high should fail 230 | expectFailure(DoubleField(Double.MaximumValidator(4.2)).validate(5.6)) { XCTFail() } 231 | // Value not exact should fail 232 | expectFailure(DoubleField(Double.ExactValidator(4.2)).validate(42)) { XCTFail() } 233 | // Precision 234 | expectFailure(DoubleField(Double.MinimumValidator(4.0000002)).validate(4.0000001)) { XCTFail() } 235 | } 236 | 237 | func testFieldBoolValidation() { 238 | // Correct value should succeed 239 | expectMatch(BoolField().validate(true), Node(true)) { XCTFail() } 240 | expectMatch(BoolField().validate(false), Node(false)) { XCTFail() } 241 | // True-ish values should succeed 242 | expectMatch(BoolField().validate("true"), Node(true)) { XCTFail() } 243 | expectMatch(BoolField().validate("t"), Node(true)) { XCTFail() } 244 | expectMatch(BoolField().validate(1), Node(true)) { XCTFail() } 245 | // False-ish values should succeed 246 | expectMatch(BoolField().validate("false"), Node(false)) { XCTFail() } 247 | expectMatch(BoolField().validate("f"), Node(false)) { XCTFail() } 248 | expectMatch(BoolField().validate(0), Node(false)) { XCTFail() } 249 | } 250 | 251 | func testUniqueFieldValidation() { 252 | // Expect success because this count should return 0 253 | expectSuccess(StringField(UniqueFieldValidator(column: "name")).validate("filter_applied")) { XCTFail() } 254 | // Expect failure because this count should return 1 255 | expectFailure(StringField(UniqueFieldValidator(column: "name")).validate("not_unique")) { XCTFail() } 256 | } 257 | 258 | // MARK: Fieldset 259 | 260 | func testSimpleFieldset() { 261 | // It should be possible to create and validate a Fieldset on the fly. 262 | var fieldset = Fieldset([ 263 | "string": StringField(), 264 | "integer": IntegerField(), 265 | "double": DoubleField() 266 | ]) 267 | expectSuccess(fieldset.validate([:])) { XCTFail() } 268 | } 269 | 270 | func testSimpleFieldsetGetInvalidData() { 271 | // A fieldset passed invalid data should still hold a reference to that data 272 | var fieldset = Fieldset([ 273 | "string": StringField(), 274 | "integer": IntegerField(), 275 | "double": DoubleField() 276 | ], requiring: ["string", "integer", "double"]) 277 | // Pass some invalid data 278 | do { 279 | let result = fieldset.validate([ 280 | "string": "MyString", 281 | "integer": 42, 282 | ]) 283 | guard case .failure = result else { 284 | XCTFail() 285 | return 286 | } 287 | // For next rendering, I should be able to see that data which was passed 288 | XCTAssertEqual(fieldset.values["string"]?.string, "MyString") 289 | XCTAssertEqual(fieldset.values["integer"]?.int, 42) 290 | XCTAssertNil(fieldset.values["gobbledegook"]?.string) 291 | } 292 | // Try again with some really invalid data 293 | // Discussion: should the returned data be identical to what was sent, or should it be 294 | // "the data we tried to validate against"? For instance, our String validators check that 295 | // the Node value is actually a String, while Node.string is happy to convert e.g. an Int. 296 | do { 297 | let result = fieldset.validate([ 298 | "string": 42, 299 | "double": "walrus", 300 | "montypython": 7.7, 301 | ]) 302 | guard case .failure = result else { 303 | XCTFail() 304 | return 305 | } 306 | // XCTAssertNil(fieldset.values["string"]?.string) // see discussion above 307 | XCTAssertNil(fieldset.values["integer"]?.int) 308 | XCTAssertNil(fieldset.values["double"]?.double) 309 | } 310 | } 311 | 312 | func testSimpleFieldsetWithPostValidation() { 313 | // This fieldset validates the whole fieldset after validating individual inputs. 314 | do { 315 | var fieldset = Fieldset([ 316 | "string": StringField(), 317 | "integer": IntegerField(), 318 | "double": DoubleField() 319 | ]) { fieldset in 320 | fieldset.errors["string"].append(FieldError.validationFailed(message: "Always fail")) 321 | } 322 | expectFailure(fieldset.validate([:])) { XCTFail() } 323 | } 324 | // This fieldset validates a bit more intelligently 325 | do { 326 | var fieldset = Fieldset([ 327 | "string": StringField(), 328 | "integer": IntegerField(), 329 | "double": DoubleField() 330 | ], requiring: ["string"]) { fieldset in 331 | if fieldset.values["string"]?.string != "Charles" { 332 | fieldset.errors["string"].append(FieldError.validationFailed(message: "String must be Charles")) 333 | } 334 | } 335 | switch fieldset.validate(["string": "Richard"]) { 336 | case .success: 337 | XCTFail() 338 | case .failure: 339 | XCTAssertEqual(fieldset.errors["string"][0].localizedDescription, "String must be Charles") 340 | } 341 | } 342 | } 343 | 344 | // MARK: Form 345 | 346 | func testSimpleForm() { 347 | // It should be possible to create a type-safe struct around a Fieldset. 348 | struct SimpleForm: Form { 349 | let string: String 350 | let integer: Int 351 | let double: Double 352 | 353 | static let fieldset = Fieldset([ 354 | "string": StringField(), 355 | "integer": IntegerField(), 356 | "double": DoubleField() 357 | ]) 358 | 359 | internal init(validatedData: [String: Node]) throws { 360 | string = validatedData["string"]!.string! 361 | integer = validatedData["integer"]!.int! 362 | double = validatedData["double"]!.double! 363 | } 364 | } 365 | do { 366 | let _ = try SimpleForm(validating: [ 367 | "string": "String", 368 | "integer": 1, 369 | "double": 2, 370 | ]) 371 | } catch { XCTFail(String(describing: error)) } 372 | } 373 | 374 | func testFormValidation() { 375 | struct SimpleForm: Form { 376 | let string: String 377 | let integer: Int 378 | let double: Double? 379 | 380 | static let fieldset = Fieldset([ 381 | "string": StringField(), 382 | "integer": IntegerField(), 383 | "double": DoubleField() 384 | ], requiring: ["string", "integer"]) 385 | 386 | internal init(validatedData: [String: Node]) throws { 387 | string = validatedData["string"]!.string! 388 | integer = validatedData["integer"]!.int! 389 | double = validatedData["double"]?.double 390 | } 391 | } 392 | // Good validation should succeed 393 | do { 394 | let _ = try SimpleForm(validating: [ 395 | "string": "String", 396 | "integer": 1, 397 | "double": 2, 398 | ]) 399 | } catch { XCTFail(String(describing: error)) } 400 | // One invalid value should fail 401 | do { 402 | let _ = try SimpleForm(validating: [ 403 | "string": "String", 404 | "integer": "INVALID", 405 | "double": 2, 406 | ]) 407 | } catch FormError.validationFailed { 408 | } catch { XCTFail(String(describing: error)) } 409 | // Missing optional value should succeed 410 | do { 411 | let _ = try SimpleForm(validating: [ 412 | "string": "String", 413 | "integer": 1, 414 | ]) 415 | } catch { XCTFail(String(describing: error)) } 416 | // Missing required value should fail 417 | do { 418 | let _ = try SimpleForm(validating: [ 419 | "string": "String", 420 | ]) 421 | } catch FormError.validationFailed { 422 | } catch { XCTFail(String(describing: error)) } 423 | } 424 | 425 | // MARK: Binding 426 | 427 | func testValidateFromContentObject() { 428 | // I want to simulate receiving a Request in POST and binding to it. 429 | var fieldset = Fieldset([ 430 | "firstName": StringField(), 431 | "lastName": StringField(), 432 | "email": StringField(String.EmailValidator()), 433 | "age": IntegerField(), 434 | ], requiring: ["firstName", "lastName", "age"]) 435 | // request.data is a Content object. I need to create a Content object. 436 | let content = Content() 437 | content.append(Node([ 438 | "firstName": "Peter", 439 | "lastName": "Pan", 440 | "age": 13, 441 | ])) 442 | XCTAssertEqual(content["firstName"]?.string, "Peter") 443 | // Now validate 444 | expectSuccess(fieldset.validate(content)) { XCTFail() } 445 | } 446 | 447 | func testValidateFromJSON() { 448 | // I want to simulate receiving a Request in POST and binding to it. 449 | var fieldset = Fieldset([ 450 | "firstName": StringField(), 451 | "lastName": StringField(), 452 | "email": StringField(String.EmailValidator()), 453 | "age": IntegerField() 454 | ], requiring: ["firstName", "lastName", "age"]) 455 | // request.data is a Content object. I need to create a Content object. 456 | let content = Content() 457 | content.append(JSON([ 458 | "firstName": "Peter", 459 | "lastName": "Pan", 460 | "age": 13, 461 | ])) 462 | XCTAssertEqual(content["firstName"]?.string, "Peter") 463 | // Now validate 464 | expectSuccess(fieldset.validate(content)) { XCTFail() } 465 | } 466 | 467 | func testValidateFormFromContentObject() { 468 | // I want to simulate receiving a Request in POST and binding to it. 469 | struct SimpleForm: Form { 470 | let firstName: String? 471 | let lastName: String? 472 | let email: String? 473 | let age: Int? 474 | 475 | static let fieldset = Fieldset([ 476 | "firstName": StringField(), 477 | "lastName": StringField(), 478 | "email": StringField(String.EmailValidator()), 479 | "age": IntegerField(), 480 | ]) 481 | 482 | internal init(validatedData: [String: Node]) throws { 483 | firstName = validatedData["firstName"]?.string 484 | lastName = validatedData["lastName"]?.string 485 | email = validatedData["email"]?.string 486 | age = validatedData["age"]?.int 487 | } 488 | } 489 | // request.data is a Content object. I need to create a Content object. 490 | let content = Content() 491 | content.append(Node([ 492 | "firstName": "Peter", 493 | "lastName": "Pan", 494 | "age": 13, 495 | ])) 496 | XCTAssertEqual(content["firstName"]?.string, "Peter") 497 | // Now validate 498 | do { 499 | let _ = try SimpleForm(validating: content) 500 | } catch { XCTFail(String(describing: error)) } 501 | } 502 | 503 | // MARK: Leaf tags 504 | 505 | func testTagErrorsForField() { 506 | let stem = Stem(workingDirectory: "") 507 | stem.register(ErrorsForField()) 508 | let leaf = try! stem.spawnLeaf(raw: "#errorsForField(fieldset, \"fieldName\") { #loop(self, \"message\") { #(message) } }") 509 | var fieldset = Fieldset(["fieldName": StringField()]) 510 | fieldset.errors["fieldName"].append(FieldError.validationFailed(message: "Fail")) 511 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 512 | let rendered = try! stem.render(leaf, with: context).makeString() 513 | XCTAssertEqual(rendered, "Fail") 514 | } 515 | 516 | func testTagIfFieldHasErrors() { 517 | let stem = Stem(workingDirectory: "") 518 | stem.register(IfFieldHasErrors()) 519 | let leaf = try! stem.spawnLeaf(raw: "#ifFieldHasErrors(fieldset, \"fieldName\") { HasErrors }") 520 | do { 521 | var fieldset = Fieldset(["fieldName": StringField()]) 522 | fieldset.errors["fieldName"].append(FieldError.requiredMissing) 523 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 524 | let rendered = try! stem.render(leaf, with: context).makeString() 525 | XCTAssertEqual(rendered, "HasErrors") 526 | } 527 | do { 528 | let fieldset = Fieldset(["fieldName": StringField()]) 529 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 530 | let rendered = try! stem.render(leaf, with: context).makeString() 531 | XCTAssertEqual(rendered, "") 532 | } 533 | } 534 | 535 | func testTagLoopErrorsForField() { 536 | let stem = Stem(workingDirectory: "") 537 | stem.register(LoopErrorsForField()) 538 | let leaf = try! stem.spawnLeaf(raw: "#loopErrorsForField(fieldset, \"fieldName\", \"message\") { #(message) }") 539 | var fieldset = Fieldset(["fieldName": StringField()]) 540 | fieldset.errors["fieldName"].append(FieldError.validationFailed(message: "Fail1")) 541 | fieldset.errors["fieldName"].append(FieldError.validationFailed(message: "Fail2")) 542 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 543 | let rendered = try! stem.render(leaf, with: context).makeString() 544 | XCTAssertEqual(rendered, "Fail1\nFail2\n") 545 | } 546 | 547 | func testTagValueForField() { 548 | let stem = Stem(workingDirectory: "") 549 | stem.register(ValueForField()) 550 | let leaf = try! stem.spawnLeaf(raw: "#valueForField(fieldset, \"fieldName\")!") 551 | var fieldset = Fieldset(["fieldName": StringField()]) 552 | fieldset.values = ["fieldName": "FieldValue"] 553 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 554 | let rendered = try! stem.render(leaf, with: context).makeString() 555 | XCTAssertEqual(rendered, "FieldValue!") 556 | } 557 | 558 | func testTagLabelForField() { 559 | let stem = Stem(workingDirectory: "") 560 | stem.register(LabelForField()) 561 | let leaf = try! stem.spawnLeaf(raw: "#labelForField(fieldset, \"fieldName\")!") 562 | let fieldset = Fieldset(["fieldName": StringField(label: "NameLabel")]) 563 | let context = Context(["fieldset": try! fieldset.makeNode(in: nil)]) 564 | let rendered = try! stem.render(leaf, with: context).makeString() 565 | XCTAssertEqual(rendered, "NameLabel!") 566 | } 567 | 568 | // MARK: Whole thing 569 | 570 | func testWholeFieldsetUsage() { 571 | // Test the usability of a Fieldset. 572 | // I want to define a fieldset which can be used to render a view. 573 | // For that, the fields will need string labels. 574 | var fieldset = Fieldset([ 575 | "name": StringField(label: "Your name", 576 | String.MaximumLengthValidator(characters: 255) 577 | ), 578 | "age": UnsignedIntegerField(label: "Your age", 579 | UInt.MinimumValidator(18, message: "You must be 18+.") 580 | ), 581 | "email": StringField(label: "Email address", 582 | String.EmailValidator(), 583 | String.MaximumLengthValidator(characters: 255) 584 | ), 585 | ]) 586 | // Now, I want to be able to render this fieldset in a view. 587 | // That means I need to be able to convert it to a Node. 588 | // The node should be able to tell me the `label` for each field. 589 | do { 590 | let fieldsetNode = try! fieldset.makeNode(in: nil) 591 | XCTAssertEqual(fieldsetNode["name"]?["label"]?.string, "Your name") 592 | XCTAssertEqual(fieldsetNode["age"]?["label"]?.string, "Your age") 593 | XCTAssertEqual(fieldsetNode["email"]?["label"]?.string, "Email address") 594 | // .. Nice to have: other things for the field, such as 'type', 'maxlength'. 595 | // .. For now, that's up to the view implementer to take care of. 596 | } 597 | // I've received data from my rendered view. Validate it. 598 | do { 599 | let validationResult = fieldset.validate([ 600 | "name": "Peter Pan", 601 | "age": 11, 602 | "email": "peter@neverland.net", 603 | ]) 604 | // This should have failed 605 | expectFailure(validationResult) { XCTFail() } 606 | // Now I should be able to render the fieldset into a view 607 | // with the passed-in data and also any errors. 608 | let fieldsetNode = try! fieldset.makeNode(in: nil) 609 | XCTAssertEqual(fieldsetNode["name"]?["label"]?.string, "Your name") 610 | XCTAssertEqual(fieldsetNode["name"]?["value"]?.string, "Peter Pan") 611 | XCTAssertNil(fieldsetNode["name"]?["errors"]) 612 | XCTAssertEqual(fieldsetNode["age"]?["errors"]?[0]?.string, "You must be 18+.") 613 | } 614 | // Let's try and validate it correctly. 615 | do { 616 | let validationResult = fieldset.validate([ 617 | "name": "Peter Pan", 618 | "age": 33, 619 | "email": "peter@neverland.net", 620 | ]) 621 | guard case .success(let validatedData) = validationResult else { 622 | XCTFail() 623 | return 624 | } 625 | XCTAssertEqual(validatedData["name"]!.string!, "Peter Pan") 626 | XCTAssertEqual(validatedData["age"]!.int!, 33) 627 | XCTAssertEqual(validatedData["email"]!.string!, "peter@neverland.net") 628 | // I would now do something useful with this validated data. 629 | } 630 | } 631 | 632 | func testWholeFormUsage() { 633 | // Test the usability of a Form. 634 | struct SimpleForm: Form { 635 | let name: String 636 | let age: UInt 637 | let email: String? 638 | static let fieldset = Fieldset([ 639 | "name": StringField(label: "Your name", 640 | String.MaximumLengthValidator(characters: 255) 641 | ), 642 | "age": UnsignedIntegerField(label: "Your age", 643 | UInt.MinimumValidator(18, message: "You must be 18+.") 644 | ), 645 | "email": StringField(label: "Email address", 646 | String.EmailValidator(), 647 | String.MaximumLengthValidator(characters: 255) 648 | ), 649 | ], requiring: ["name", "age"]) 650 | internal init(validatedData: [String: Node]) throws { 651 | name = validatedData["name"]!.string! 652 | age = validatedData["age"]!.uint! 653 | email = validatedData["email"]?.string 654 | } 655 | } 656 | // I have defined a form with a fieldset with labels. Test 657 | // that I can properly render it. 658 | do { 659 | let fieldsetNode = try! SimpleForm.fieldset.makeNode(in: nil) 660 | XCTAssertEqual(fieldsetNode["name"]?["label"]?.string, "Your name") 661 | XCTAssertEqual(fieldsetNode["age"]?["label"]?.string, "Your age") 662 | XCTAssertEqual(fieldsetNode["email"]?["label"]?.string, "Email address") 663 | } 664 | // I've received data from my rendered view. Validate it. 665 | do { 666 | let _ = try SimpleForm(validating: [ 667 | "name": "Peter Pan", 668 | "age": 11, 669 | "email": "peter@neverland.net", 670 | ]) 671 | // This should not succeed 672 | XCTFail() 673 | } catch FormError.validationFailed(let fieldset) { 674 | // Now I should be able to render the fieldset into a view 675 | // with the passed-in data and also any errors. 676 | let fieldsetNode = try! fieldset.makeNode(in: nil) 677 | XCTAssertEqual(fieldsetNode["name"]?["label"]?.string, "Your name") 678 | XCTAssertEqual(fieldsetNode["name"]?["value"]?.string, "Peter Pan") 679 | XCTAssertNil(fieldsetNode["name"]?["errors"]) 680 | XCTAssertEqual(fieldsetNode["age"]?["errors"]?[0]?.string, "You must be 18+.") 681 | } catch { XCTFail() } 682 | // Let's try and validate it correctly. 683 | do { 684 | let form = try SimpleForm(validating: [ 685 | "name": "Peter Pan", 686 | "age": 33, 687 | "email": "peter@neverland.net", 688 | ]) 689 | XCTAssertEqual(form.name, "Peter Pan") 690 | XCTAssertEqual(form.age, 33) 691 | XCTAssertEqual(form.email, "peter@neverland.net") 692 | // I would now do something useful with this validated data. 693 | } catch { XCTFail() } 694 | } 695 | 696 | func testSampleLoginForm() { 697 | // Test a simple login form which validates against a credential store. 698 | struct LoginForm: Form { 699 | let username: String 700 | let password: String 701 | static let fieldset = Fieldset([ 702 | "username": StringField(label: "Username"), 703 | "password": StringField(label: "Password"), 704 | ], requiring: ["username", "password"]) { fieldset in 705 | let credentialStore = [ 706 | (username: "user1", password: "pass1"), 707 | (username: "user2", password: "pass2"), 708 | (username: "user3", password: "pass3"), 709 | ] 710 | if (credentialStore.filter { 711 | $0.username == fieldset.values["username"]!.string! && $0.password == fieldset.values["password"]!.string! 712 | }.isEmpty) { 713 | fieldset.errors["password"].append(FieldError.validationFailed(message: "Invalid password")) 714 | } 715 | } 716 | internal init(validatedData: [String: Node]) throws { 717 | username = validatedData["username"]!.string! 718 | password = validatedData["password"]!.string! 719 | } 720 | } 721 | // Try and log in incorrectly 722 | do { 723 | let postData = Content() 724 | postData.append(Node([ 725 | "username": "user1", 726 | "password": "notmypassword", 727 | ])) 728 | let _ = try LoginForm(validating: postData) 729 | XCTFail() 730 | } catch FormError.validationFailed(let fieldset) { 731 | XCTAssertEqual(fieldset.errors["password"][0].localizedDescription, "Invalid password") 732 | } catch { XCTFail() } 733 | // Try and log in correctly 734 | do { 735 | let postData = Content() 736 | postData.append(Node([ 737 | "username": "user1", 738 | "password": "pass1", 739 | ])) 740 | let form = try LoginForm(validating: postData) 741 | XCTAssertEqual(form.username, "user1") 742 | } catch { XCTFail() } 743 | } 744 | 745 | func testSampleLoginFormWithMultipart() { 746 | // Test a simple login form which validates against a credential store. 747 | struct LoginForm: Form { 748 | let username: String 749 | let password: String 750 | static let fieldset = Fieldset([ 751 | "username": StringField(label: "Username"), 752 | "password": StringField(label: "Password"), 753 | ], requiring: ["username", "password"]) { fieldset in 754 | let credentialStore = [ 755 | (username: "user1", password: "pass1"), 756 | (username: "user2", password: "pass2"), 757 | (username: "user3", password: "pass3"), 758 | ] 759 | if (credentialStore.filter { 760 | $0.username == fieldset.values["username"]!.string! && $0.password == fieldset.values["password"]!.string! 761 | }.isEmpty) { 762 | fieldset.errors["password"].append(FieldError.validationFailed(message: "Invalid password")) 763 | } 764 | } 765 | internal init(validatedData: [String: Node]) throws { 766 | username = validatedData["username"]!.string! 767 | password = validatedData["password"]!.string! 768 | } 769 | } 770 | // Try and log in incorrectly 771 | do { 772 | let user = "user1" 773 | let userPart = Part(headers: [:], body: user.makeBytes()) 774 | let userField = Field(name: "username", filename: nil, part: userPart) 775 | let password = "notmypassword" 776 | let passwordPart = Part(headers: [:], body: password.makeBytes()) 777 | let passwordField = Field(name: "password", filename: nil, part: passwordPart) 778 | let request = try Request(method: .get, uri: "form-data") 779 | request.formData = [ 780 | "username": userField, 781 | "password": passwordField 782 | ] 783 | let _ = try LoginForm(validating: request.data) 784 | XCTFail() 785 | } catch FormError.validationFailed(let fieldset) { 786 | XCTAssertEqual(fieldset.errors["password"][0].localizedDescription, "Invalid password") 787 | } catch { XCTFail() } 788 | // Try and log in correctly 789 | do { 790 | let user = "user1" 791 | let userPart = Part(headers: [:], body: user.makeBytes()) 792 | let userField = Field(name: "username", filename: nil, part: userPart) 793 | let password = "pass1" 794 | let passwordPart = Part(headers: [:], body: password.makeBytes()) 795 | let passwordField = Field(name: "password", filename: nil, part: passwordPart) 796 | let request = try Request(method: .get, uri: "form-data") 797 | request.formData = [ 798 | "username": userField, 799 | "password": passwordField 800 | ] 801 | let form = try LoginForm(validating: request.data) 802 | XCTAssertEqual(form.username, "user1") 803 | } catch { XCTFail() } 804 | } 805 | } 806 | 807 | // MARK: Mocks 808 | 809 | // Mock Driver to test DB validators 810 | class TestDriver: Driver, Connection { 811 | var idKey: String = "id" 812 | var idType: IdentifierType = .int 813 | var keyNamingConvention: KeyNamingConvention = .snake_case 814 | var closed: Bool { return false } 815 | 816 | func query(_ query: Query) throws -> Node { 817 | switch query.action { 818 | case .count: 819 | // If we have this specific filter consider it's not unique 820 | guard query.filters.contains(where: { 821 | guard case .compare(let key, let comparison, let value) = $0.method else { 822 | return false 823 | } 824 | return (key == "name" && comparison == .equals && value == Node("not_unique")) 825 | }) else { 826 | return 0 827 | } 828 | return 1 829 | default: 830 | return 0 831 | } 832 | } 833 | func schema(_ schema: Schema) throws {} 834 | @discardableResult 835 | public func raw(_ query: String, _ values: [Node] = []) throws -> Node { 836 | return .null 837 | } 838 | 839 | func makeConnection() throws -> Connection { 840 | return self 841 | } 842 | } 843 | 844 | // Mock Entity to test DB validators 845 | final class TestUser: Entity { 846 | let storage = Storage() 847 | 848 | var name: String 849 | init(name: String) { 850 | self.name = name 851 | } 852 | 853 | init(node: Node) throws { 854 | name = try node.get("name") 855 | } 856 | 857 | convenience init(row: Row) throws { 858 | try self.init(node: Node(row)) 859 | } 860 | 861 | func makeNode(in context: Vapor.Context?) throws -> Node { 862 | var node = Node(context) 863 | try node.set("name", name) 864 | return node 865 | } 866 | func makeRow() throws -> Row { 867 | return try makeNode(in: rowContext).converted() 868 | } 869 | static func prepare(_ database: Database) throws {} 870 | static func revert(_ database: Database) throws {} 871 | } 872 | --------------------------------------------------------------------------------