diff --git a/Sources/VaporForms/Validators/DatabaseValidators.swift b/Sources/VaporForms/Validators/DatabaseValidators.swift new file mode 100644 index 0000000..18dabdf --- /dev/null +++ b/Sources/VaporForms/Validators/DatabaseValidators.swift @@ -0,0 +1,37 @@ +import Vapor +import Fluent + +/** + Validates if the value exists on the database + */ +public class UniqueFieldValidator: FieldValidator { + let column: String + let additionalFilters: [(column:String, comparison:Filter.Comparison, value:String)]? + let message: String? + public init(column: String, additionalFilters: [(column:String, comparison:Filter.Comparison, value:String)]?=nil, message: String?=nil) { + self.column = column + self.additionalFilters = additionalFilters + self.message = message + } + public override func validate(input value: String) -> FieldValidationResult { + // Let's create the main filter + do { + let query = try ModelType.query() + try query.filter(self.column, value) + // If we have addition filters, add them + if let filters = self.additionalFilters { + for filter in filters { + try query.filter(filter.column, filter.comparison, filter.value) + } + } + // Check if any record exists + if(try query.count() > 0){ + return .failure([.validationFailed(message: message ?? "Value \(self.column) must be unique.")]) + } + // If not we have green light + return .success(Node(value)) + } catch { + return .failure([.validationFailed(message: message ?? "Value \(self.column) must be unique.")]) + } + } +} diff --git a/Sources/VaporForms/Validators/String+Validators.swift b/Sources/VaporForms/Validators/String+Validators.swift index 57ce305..4c81799 100644 --- a/Sources/VaporForms/Validators/String+Validators.swift +++ b/Sources/VaporForms/Validators/String+Validators.swift @@ -77,4 +77,23 @@ extension String { } } + /** + Validates a string against the given regex + */ + public class RegexValidator: FieldValidator { + let regex: String? + let message: String? + public init(regex: String?=nil, message: String?=nil) { + self.regex = regex + self.message = message + } + override public func validate(input value: String) -> FieldValidationResult { + if let regex = self.regex { + if let _ = value.range(of: regex, options: .regularExpression) { + return .success(Node(value)) + } + } + return .failure([.validationFailed(message: message ?? "Value did not match required format.")]) + } + } } diff --git a/Tests/VaporFormsTests/VaporFormsTests.swift b/Tests/VaporFormsTests/VaporFormsTests.swift index beb27f7..0200d06 100644 --- a/Tests/VaporFormsTests/VaporFormsTests.swift +++ b/Tests/VaporFormsTests/VaporFormsTests.swift @@ -2,6 +2,7 @@ import XCTest @testable import VaporForms @testable import Vapor import Leaf +import Fluent /** Layout of the vapor-forms library @@ -45,6 +46,7 @@ class VaporFormsTests: XCTestCase { ("testFieldUnsignedIntegerValidation", testFieldUnsignedIntegerValidation), ("testFieldDoubleValidation", testFieldDoubleValidation), ("testFieldBoolValidation", testFieldBoolValidation), + ("testUniqueFieldValidation", testUniqueFieldValidation), // Fieldset ("testSimpleFieldset", testSimpleFieldset), ("testSimpleFieldsetGetInvalidData", testSimpleFieldsetGetInvalidData), @@ -68,6 +70,10 @@ class VaporFormsTests: XCTestCase { ("testSampleLoginFormWithMultipart", testSampleLoginFormWithMultipart), ] } + + override func setUp(){ + Database.default = Database(TestDriver()) + } func expectMatch(_ test: FieldValidationResult, _ match: Node, fail: () -> Void) { switch test { @@ -144,6 +150,10 @@ class VaporFormsTests: XCTestCase { expectFailure(StringField(String.MaximumLengthValidator(characters: 6)).validate("maxi string")) { XCTFail() } // Value not exact size should fail expectFailure(StringField(String.ExactLengthValidator(characters: 6)).validate("wrong size")) { XCTFail() } + // Value in regex should succeed + expectSuccess(StringField(String.RegexValidator(regex: "^[a-z]{6}$")).validate("string")) { XCTFail() } + // Value in regex should failure + expectFailure(StringField(String.RegexValidator(regex: "^[a-z]{6}$")).validate("string 1s wr0ng")) { XCTFail() } } func testFieldEmailValidation() { @@ -234,7 +244,14 @@ class VaporFormsTests: XCTestCase { expectMatch(BoolField().validate("f"), Node(false)) { XCTFail() } expectMatch(BoolField().validate(0), Node(false)) { XCTFail() } } - + + func testUniqueFieldValidation() { + // Expect success because this count should return 0 + expectSuccess(StringField(UniqueFieldValidator(column: "name")).validate("filter_applied")) { XCTFail() } + // Expect failure because this count should return 1 + expectFailure(StringField(UniqueFieldValidator(column: "name")).validate("not_unique")) { XCTFail() } + } + // MARK: Fieldset func testSimpleFieldset() { @@ -807,3 +824,53 @@ class VaporFormsTests: XCTestCase { } catch { XCTFail() } } } + +// MARK: Mocks + +// Mock Driver to test DB validators +class TestDriver: Driver { + var idKey: String = "id" + func query(_ query: Query) throws -> Node { + switch query.action { + case .count: + // If we have this specific filter consider it's not unique + guard query.filters.contains(where: { + guard case .compare(let key, let comparison, let value) = $0.method else { + return false + } + return (key == "name" && comparison == .equals && value == Node("not_unique")) + }) else { + return 0 + } + return 1 + default: + return 0 + } + } + func schema(_ schema: Schema) throws {} + @discardableResult + public func raw(_ query: String, _ values: [Node] = []) throws -> Node { + return .null + } +} + +// Mock Entity to test DB validators +struct TestUser: Entity { + var id: Node? + var name: String + init(name: String) { + self.name = name + } + init(node: Node, in context: Vapor.Context) throws { + id = try node.extract("id") + name = try node.extract("name") + } + func makeNode(context: Vapor.Context) throws -> Node { + return try Node(node: [ + "id": id, + "name": name + ]) + } + static func prepare(_ database: Database) throws {} + static func revert(_ database: Database) throws {} +}