From ecbc0836959952290100667c9d033d2d3d9a8931 Mon Sep 17 00:00:00 2001 From: Robert Payne Date: Fri, 9 Jun 2017 16:52:46 +1200 Subject: [PATCH] Fix JSON bug and add JSON schema * Fix long string values in json parser This fixes a core issue in the JSON parser, it uses a shared buffer of 8kb to avoid excessive allocations while it null-terminates strings coming from YAJL. Unfortunately I never actually implemented it properly for values over 8kb as the code never memcpy'd or did anything to actually represent the string. * add JSONSchema --- Sources/Content/JSON/JSON.swift | 62 + Sources/Content/JSON/JSONConvertible.swift | 36 + Sources/Content/JSON/JSONParser.swift | 18 +- Sources/Content/JSON/JSONSchema.swift | 1388 +++++++++++--------- Tests/ContentTests/JSONTests.swift | 30 + 5 files changed, 894 insertions(+), 640 deletions(-) create mode 100644 Tests/ContentTests/JSONTests.swift diff --git a/Sources/Content/JSON/JSON.swift b/Sources/Content/JSON/JSON.swift index 0e013953..2552cdfb 100644 --- a/Sources/Content/JSON/JSON.swift +++ b/Sources/Content/JSON/JSON.swift @@ -97,6 +97,68 @@ extension JSON { } } +extension JSON { + public var isNull: Bool { + if case .null = self { + return true + } + + return false + } + + public var isBool: Bool { + if case .bool = self { + return true + } + + return false + } + + public var isNumber: Bool { + return isDouble || isInt + } + + public var isDouble: Bool { + if case .double = self { + return true + } + + return false + } + + public var isInt: Bool { + if case .int = self { + return true + } + + return false + } + + public var isString: Bool { + if case .string = self { + return true + } + + return false + } + + public var isArray: Bool { + if case .array = self { + return true + } + + return false + } + + public var isObject: Bool { + if case .object = self { + return true + } + + return false + } +} + extension JSON { public func get(_ indexPath: IndexPathComponent...) throws -> T { let content = try _get(indexPath as [IndexPathComponent]) diff --git a/Sources/Content/JSON/JSONConvertible.swift b/Sources/Content/JSON/JSONConvertible.swift index 14a67fd6..2439261b 100644 --- a/Sources/Content/JSON/JSONConvertible.swift +++ b/Sources/Content/JSON/JSONConvertible.swift @@ -154,3 +154,39 @@ extension Array : JSONInitializable { self = this } } + +protocol MapDictionaryKeyInitializable { + init(mapDictionaryKey: String) +} + +extension String : MapDictionaryKeyInitializable { + init(mapDictionaryKey: String) { + self = mapDictionaryKey + } +} + +extension Dictionary : JSONInitializable { + public init(json: JSON) throws { + guard case .object(let object) = json else { + throw JSONError.cannotInitialize(type: type(of: self), json: json) + } + + guard let keyInitializable = Key.self as? MapDictionaryKeyInitializable.Type else { + throw JSONError.cannotInitialize(type: type(of: self), json: json) + } + + guard let valueInitializable = Value.self as? JSONInitializable.Type else { + throw JSONError.cannotInitialize(type: type(of: self), json: json) + } + + var this = Dictionary(minimumCapacity: object.count) + + for (key, value) in object { + if let key = keyInitializable.init(mapDictionaryKey: key) as? Key { + this[key] = try valueInitializable.init(json: value) as? Value + } + } + + self = this + } +} diff --git a/Sources/Content/JSON/JSONParser.swift b/Sources/Content/JSON/JSONParser.swift index a9f2d0c2..5b2c1a22 100755 --- a/Sources/Content/JSON/JSONParser.swift +++ b/Sources/Content/JSON/JSONParser.swift @@ -339,10 +339,11 @@ fileprivate func yajl_string( parser.buffer[bufferLength] = 0 string = String(cString: UnsafePointer(parser.buffer)) } else { - var buffer = UnsafeMutablePointer.allocate(capacity: bufferLength + 1) - defer { buffer.deallocate(capacity: bufferLength + 1) } - buffer[bufferLength] = 0 - string = String(cString: UnsafePointer(buffer)) + var newBuffer = UnsafeMutablePointer.allocate(capacity: bufferLength + 1) + defer { newBuffer.deallocate(capacity: bufferLength + 1) } + memcpy(UnsafeMutableRawPointer(newBuffer), buffer, bufferLength) + newBuffer[bufferLength] = 0 + string = String(cString: UnsafePointer(newBuffer)) } } else { string = "" @@ -374,10 +375,11 @@ fileprivate func yajl_map_key( parser.buffer[bufferLength] = 0 string = String(cString: UnsafePointer(parser.buffer)) } else { - var buffer = UnsafeMutablePointer.allocate(capacity: bufferLength + 1) - defer { buffer.deallocate(capacity: bufferLength + 1) } - buffer[bufferLength] = 0 - string = String(cString: UnsafePointer(buffer)) + var newBuffer = UnsafeMutablePointer.allocate(capacity: bufferLength + 1) + defer { newBuffer.deallocate(capacity: bufferLength + 1) } + memcpy(UnsafeMutableRawPointer(newBuffer), buffer, bufferLength) + newBuffer[bufferLength] = 0 + string = String(cString: UnsafePointer(newBuffer)) } } else { string = "" diff --git a/Sources/Content/JSON/JSONSchema.swift b/Sources/Content/JSON/JSONSchema.swift index 7a6d520c..54ebb6dc 100644 --- a/Sources/Content/JSON/JSONSchema.swift +++ b/Sources/Content/JSON/JSONSchema.swift @@ -1,251 +1,366 @@ -//public enum JSONType : String { -// case object = "object" -// case array = "array" -// case string = "string" -// case integer = "integer" -// case number = "number" -// case boolean = "boolean" -// case null = "null" -//} -// -//extension String { -// func stringByRemovingPrefix(_ prefix: String) -> String? { -// if hasPrefix(prefix) { -// let index = characters.index(startIndex, offsetBy: prefix.characters.count) -// return substring(from: index) -// } -// -// return nil -// } -//} -// -//public struct Schema { -// public let title: String? -// public let description: String? -// -// public let type: [JSONType]? -// -// let formats: [String: Validator] -// -// let schema: [String: Any] -// -// public init(_ schema: JSON) { -// title = try? schema.get("title") -// description = try? schema.get("description") -// -// if let type = try? schema.get("type") as String { -// if let type = JSONType(rawValue: type) { -// self.type = [type] -// } else { -// self.type = [] -// } -// } else if let types = try? schema.get("type") as [String] { -// self.type = types.map { Type(rawValue: $0) }.filter { $0 != nil }.map { $0! } -// } else { -// self.type = [] -// } -// -// self.schema = schema -// -// formats = [ -// "ipv4": validateIPv4, -// "ipv6": validateIPv6, -// ] -// } -// -// public func validate(_ data:Any) -> ValidationResult { -// let validator = allOf(validators(self)(schema)) -// let result = validator(data) -// return result -// } -// -// func validatorForReference(_ reference:String) -> Validator { -// // TODO: Rewrite this whole block: https://github.com/kylef/JSONSchema.swift/issues/12 -// if let reference = reference.stringByRemovingPrefix("#") { // Document relative -// if let tmp = reference.stringByRemovingPrefix("/"), let reference = (tmp as NSString).removingPercentEncoding { -// var components = reference.components(separatedBy: "/") -// var schema = self.schema -// while let component = components.first { -// components.remove(at: components.startIndex) -// -// if let subschema = schema[component] as? [String:Any] { -// schema = subschema -// continue -// } else if let schemas = schema[component] as? [[String:Any]] { -// if let component = components.first, let index = Int(component) { -// components.remove(at: components.startIndex) -// -// if schemas.count > index { -// schema = schemas[index] -// continue -// } -// } -// } -// -// return invalidValidation("Reference not found '\(component)' in '\(reference)'") -// } -// -// return allOf(JSONSchema.validators(self)(schema)) -// } else if reference == "" { -// return { value in -// let validators = JSONSchema.validators(self)(self.schema) -// return allOf(validators)(value) -// } -// } -// } -// -// return invalidValidation("Remote $ref '\(reference)' is not yet supported") -// } -//} -// -///// Returns a set of validators for a schema and document -//func validators(_ root: Schema) -> (_ schema: [String:Any]) -> [Validator] { -// return { schema in -// var validators = [Validator]() -// -// if let ref = schema["$ref"] as? String { -// validators.append(root.validatorForReference(ref)) -// } -// -// if let type = schema["type"] { -// // Rewrite this and most of the validator to use the `type` property, see https://github.com/kylef/JSONSchema.swift/issues/12 -// validators.append(validateType(type)) -// } -// -// if let allOf = schema["allOf"] as? [[String:Any]] { -// validators += allOf.map(JSONSchema.validators(root)).reduce([], +) -// } -// -// if let anyOfSchemas = schema["anyOf"] as? [[String:Any]] { -// let anyOfValidators = anyOfSchemas.map(JSONSchema.validators(root)).map(allOf) as [Validator] -// validators.append(anyOf(anyOfValidators)) -// } -// -// if let oneOfSchemas = schema["oneOf"] as? [[String:Any]] { -// let oneOfValidators = oneOfSchemas.map(JSONSchema.validators(root)).map(allOf) as [Validator] -// validators.append(oneOf(oneOfValidators)) -// } -// -// if let notSchema = schema["not"] as? [String:Any] { -// let notValidator = allOf(JSONSchema.validators(root)(notSchema)) -// validators.append(not(notValidator)) -// } -// -// if let enumValues = schema["enum"] as? [Any] { -// validators.append(validateEnum(enumValues)) -// } -// -// // String -// if let maxLength = schema["maxLength"] as? Int { -// validators.append(validateLength(<=, length: maxLength, error: "Length of string is larger than max length \(maxLength)")) -// } -// -// if let minLength = schema["minLength"] as? Int { -// validators.append(validateLength(>=, length: minLength, error: "Length of string is smaller than minimum length \(minLength)")) -// } -// -// if let pattern = schema["pattern"] as? String { -// validators.append(validatePattern(pattern)) -// } -// -// // Numerical -// if let multipleOf = schema["multipleOf"] as? Double { -// validators.append(validateMultipleOf(multipleOf)) -// } -// -// if let minimum = schema["minimum"] as? Double { -// validators.append(validateNumericLength(minimum, comparitor: >=, exclusiveComparitor: >, exclusive: schema["exclusiveMinimum"] as? Bool, error: "Value is lower than minimum value of \(minimum)")) -// } -// -// if let maximum = schema["maximum"] as? Double { -// validators.append(validateNumericLength(maximum, comparitor: <=, exclusiveComparitor: <, exclusive: schema["exclusiveMaximum"] as? Bool, error: "Value exceeds maximum value of \(maximum)")) -// } -// -// // Array -// if let minItems = schema["minItems"] as? Int { -// validators.append(validateArrayLength(minItems, comparitor: >=, error: "Length of array is smaller than the minimum \(minItems)")) -// } -// -// if let maxItems = schema["maxItems"] as? Int { -// validators.append(validateArrayLength(maxItems, comparitor: <=, error: "Length of array is greater than maximum \(maxItems)")) -// } -// -// if let uniqueItems = schema["uniqueItems"] as? Bool { +// Copyright (c) 2015, Kyle Fuller +// All rights reserved. +// +// Redistribution and use in source and binary forms, with or without +// modification, are permitted provided that the following conditions are met: +// +// * Redistributions of source code must retain the above copyright notice, this +// list of conditions and the following disclaimer. +// +// * Redistributions in binary form must reproduce the above copyright notice, +// this list of conditions and the following disclaimer in the documentation +// and/or other materials provided with the distribution. +// +// * Neither the name of Mockingjay nor the names of its +// contributors may be used to endorse or promote products derived from +// this software without specific prior written permission. +// +// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +// AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +// IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +// DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +// FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +// DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +// CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +// OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +import Foundation + +public enum JSONType : String { + case object = "object" + case array = "array" + case string = "string" + case integer = "integer" + case number = "number" + case boolean = "boolean" + case null = "null" +} + +public enum ValidationResult { + case valid + case invalid([String]) + + public var isValid: Bool { + switch self { + case .valid: + return true + case .invalid: + return false + } + } + + public var errors: [String] { + switch self { + case .valid: + return [] + case .invalid(let errors): + return errors + } + } +} + +typealias Validate = (JSON) -> ValidationResult + +extension JSON { + public struct Schema { + public let title: String? + public let description: String? + + public let type: [JSONType]? + + let formats: [String: Validate] + + let schema: JSON + + public init(_ schema: JSON) { + title = try? schema.get("title") + description = try? schema.get("description") + + if let type = try? schema.get("type") as String { + if let type = JSONType(rawValue: type) { + self.type = [type] + } else { + self.type = [] + } + } else if let types = try? schema.get("type") as [String] { + self.type = types.map { JSONType(rawValue: $0) }.filter { $0 != nil }.map { $0! } + } else { + self.type = [] + } + + self.schema = schema + + formats = [ + "ipv4": validateIPv4, + "ipv6": validateIPv6, + ] + } + + public func validate(_ data: JSON) -> ValidationResult { + let validate = allOf(getValidators(self)(schema)) + return validate(data) + } + + func validatorForReference(_ reference: String) -> Validate { + if let reference = reference.stringByRemovingPrefix("#") { // Document relative + if + let tmp = reference.stringByRemovingPrefix("/"), + let reference = tmp.removingPercentEncoding + { + var components = reference.components(separatedBy: "/") + var schema = self.schema + + while let component = components.first { + components.remove(at: components.startIndex) + + if let _ = try? schema.get(component) as [String: JSON] { + schema = try! schema.get(component) as JSON + continue + } else if let _ = try? schema.get(component) as [[String: JSON]] { + let schemas = try! schema.get(component) as [JSON] + + if let component = components.first, let index = Int(component) { + components.remove(at: components.startIndex) + + if schemas.count > index { + schema = schemas[index] + continue + } + } + } + + return invalidValidation("Reference not found '\(component)' in '\(reference)'") + } + + return allOf(getValidators(self)(schema)) + } else if reference == "" { + return { value in + let validators = getValidators(self)(self.schema) + return allOf(validators)(value) + } + } + } + + return invalidValidation("Remote $ref '\(reference)' is not yet supported") + } + } +} + +/// Returns a set of validators for a schema and document +func getValidators(_ root: JSON.Schema) -> (_ schema: JSON) -> [Validate] { + return { schema in + var validators: [Validate] = [] + + if let ref = try? schema.get("$ref") as String { + validators.append(root.validatorForReference(ref)) + } + + if let type = try? schema.get("type") as String { + validators.append(validateType(type)) + } + + if let _ = try? schema.get("allOf") as [[String: JSON]] { + let allOf = try! schema.get("allOf") as [JSON] + validators += allOf.map(getValidators(root)).reduce([], +) + } + + if let _ = try? schema.get("anyOf") as [[String: JSON]] { + let anyOfSchemas = try! schema.get("anyOf") as [JSON] + let anyOfValidators = anyOfSchemas.map(getValidators(root)).map(allOf) as [Validate] + validators.append(anyOf(anyOfValidators)) + } + + if let _ = try? schema.get("oneOf") as [[String: JSON]] { + let oneOfSchemas = try! schema.get("oneOf") as [JSON] + let oneOfValidators = oneOfSchemas.map(getValidators(root)).map(allOf) as [Validate] + validators.append(oneOf(oneOfValidators)) + } + + if let _ = try? schema.get("not") as [String: JSON] { + let notSchema = try! schema.get("not") as JSON + let notValidator = allOf(getValidators(root)(notSchema)) + validators.append(not(notValidator)) + } + + if let enumValues = try? schema.get("enum") as [JSON] { + validators.append(validateEnum(enumValues)) + } + + // String + + if let maxLength = try? schema.get("maxLength") as Int { + validators.append( + validateLength( + <=, + length: maxLength, + error: "Length of string is larger than max length \(maxLength)" + ) + ) + } + + if let minLength = try? schema.get("minLength") as Int { + validators.append( + validateLength( + >=, + length: minLength, + error: "Length of string is smaller than minimum length \(minLength)" + ) + ) + } + + if let pattern = try? schema.get("pattern") as String { + validators.append(validatePattern(pattern)) + } + + // Numerical + + if let multipleOf = try? schema.get("multipleOf") as Double { + validators.append(validateMultipleOf(multipleOf)) + } + + if let minimum = try? schema.get("minimum") as Double { + validators.append( + validateNumericLength( + minimum, + comparator: >=, + exclusiveComparitor: >, + exclusive: try? schema.get("exclusiveMinimum") as Bool, + error: "Value is lower than minimum value of \(minimum)" + ) + ) + } + + if let maximum = try? schema.get("maximum") as Double { + validators.append( + validateNumericLength( + maximum, + comparator: <=, + exclusiveComparitor: <, + exclusive: try? schema.get("exclusiveMaximum") as Bool, + error: "Value exceeds maximum value of \(maximum)" + ) + ) + } + + // Array + + if let minItems = try? schema.get("minItems") as Int { + validators.append( + validateArrayLength( + minItems, + comparator: >=, + error: "Length of array is smaller than the minimum \(minItems)" + ) + ) + } + + if let maxItems = try? schema.get("maxItems") as Int { + validators.append( + validateArrayLength( + maxItems, + comparator: <=, + error: "Length of array is greater than maximum \(maxItems)" + ) + ) + } + +// if let uniqueItems = try? schema.get("uniqueItems") as Bool { // if uniqueItems { // validators.append(validateUniqueItems) // } // } -// -// if let items = schema["items"] as? [String:Any] { -// let itemsValidators = allOf(JSONSchema.validators(root)(items)) -// -// func validateItems(_ document:Any) -> ValidationResult { -// if let document = document as? [Any] { -// return flatten(document.map(itemsValidators)) -// } -// -// return .Valid -// } -// -// validators.append(validateItems) -// } else if let items = schema["items"] as? [[String:Any]] { -// func createAdditionalItemsValidator(_ additionalItems:Any?) -> Validator { -// if let additionalItems = additionalItems as? [String:Any] { -// return allOf(JSONSchema.validators(root)(additionalItems)) -// } -// -// let additionalItems = additionalItems as? Bool ?? true -// if additionalItems { -// return validValidation -// } -// -// return invalidValidation("Additional results are not permitted in this array.") -// } -// -// let additionalItemsValidator = createAdditionalItemsValidator(schema["additionalItems"]) -// let itemValidators = items.map(JSONSchema.validators(root)) -// -// func validateItems(_ value:Any) -> ValidationResult { -// if let value = value as? [Any] { -// var results = [ValidationResult]() -// -// for (index, element) in value.enumerated() { -// if index >= itemValidators.count { -// results.append(additionalItemsValidator(element)) -// } else { -// let validators = allOf(itemValidators[index]) -// results.append(validators(element)) -// } -// } -// -// return flatten(results) -// } -// -// return .Valid -// } -// -// validators.append(validateItems) -// } -// -// if let maxProperties = schema["maxProperties"] as? Int { -// validators.append(validatePropertiesLength(maxProperties, comparitor: >=, error: "Amount of properties is greater than maximum permitted")) -// } -// -// if let minProperties = schema["minProperties"] as? Int { -// validators.append(validatePropertiesLength(minProperties, comparitor: <=, error: "Amount of properties is less than the required amount")) -// } -// -// if let required = schema["required"] as? [String] { -// validators.append(validateRequired(required)) -// } -// -// if (schema["properties"] != nil) || (schema["patternProperties"] != nil) || (schema["additionalProperties"] != nil) { -// func createAdditionalPropertiesValidator(_ additionalProperties:Any?) -> Validator { -// if let additionalProperties = additionalProperties as? [String:Any] { -// return allOf(JSONSchema.validators(root)(additionalProperties)) + + if let _ = try? schema.get("items") as [String: JSON] { + let items = try! schema.get("items") as JSON + let itemsValidators = allOf(getValidators(root)(items)) + validators.append(validateItems(itemsValidators)) + } else if let _ = try? schema.get("items") as [[String: JSON]] { + func createAdditionalItemsValidator(_ additionalItems: JSON?) -> Validate { + if let items = additionalItems, let _ = try? items.get() as [String: JSON] { + let additionalItems = try! additionalItems!.get() as JSON + return allOf(getValidators(root)(additionalItems)) + } + + let additionalItems = additionalItems.flatMap({ try? $0.get() as Bool }) ?? true + + if additionalItems { + return validValidation + } + + return invalidValidation("Additional results are not permitted in this array.") + } + + let additionalItemsValidator = createAdditionalItemsValidator( + try? schema.get("additionalItems") + ) + + let items = try! schema.get("items") as [JSON] + let itemValidators = items.map(getValidators(root)) + + func validateItems(_ value: JSON) -> ValidationResult { + if let value = try? value.get() as [JSON] { + var results: [ValidationResult] = [] + + for (index, element) in value.enumerated() { + if index >= itemValidators.count { + results.append(additionalItemsValidator(element)) + } else { + let validators = allOf(itemValidators[index]) + results.append(validators(element)) + } + } + + return flatten(results) + } + + return .valid + } + + validators.append(validateItems) + } + + if let maxProperties = try? schema.get("maxProperties") as Int { + validators.append( + validatePropertiesLength( + maxProperties, + comparator: >=, + error: "Amount of properties is greater than maximum permitted" + ) + ) + } + + if let minProperties = try? schema.get("minProperties") as Int { + validators.append( + validatePropertiesLength( + minProperties, + comparator: <=, + error: "Amount of properties is less than the required amount" + ) + ) + } + + if let required = try? schema.get("required") as [String] { + validators.append(validateRequired(required)) + } + +// let properties = try? schema.get("properties") +// let patternProperties = try? schema.get("patternProperties") +// let additionalProperties = try? schema.get("additionalProperties") +// +// if +// properties != nil || +// patternProperties != nil || +// additionalProperties != nil +// { +// func createAdditionalPropertiesValidator(_ additionalProperties: JSON?) -> Validate { +// if let additionalProperties = additionalProperties as? [String: JSON] { +// return allOf(getValidators(root)(additionalProperties)) // } // // let additionalProperties = additionalProperties as? Bool ?? true +// // if additionalProperties { // return validValidation // } @@ -253,7 +368,7 @@ // return invalidValidation("Additional properties are not permitted in this object.") // } // -// func createPropertiesValidators(_ properties:[String:[String:Any]]?) -> [String:Validator]? { +// func createPropertiesValidators(_ properties:[String:[String:Any]]?) -> [String: Validate]? { // if let properties = properties { // return Dictionary(properties.keys.map { // key in (key, allOf(JSONSchema.validators(root)(properties[key]!))) @@ -268,180 +383,120 @@ // let patternProperties = createPropertiesValidators(schema["patternProperties"] as? [String:[String:Any]]) // validators.append(validateProperties(properties, patternProperties: patternProperties, additionalProperties: additionalPropertyValidator)) // } -// -// func validateDependency(_ key: String, validator: @escaping Validator) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? [String:Any] { -// if (value[key] != nil) { -// return validator(value) -// } -// } -// -// return .Valid -// } -// } -// -// func validateDependencies(_ key: String, dependencies: [String]) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? [String:Any] { -// if (value[key] != nil) { -// return flatten(dependencies.map { dependency in -// if value[dependency] == nil { -// return .invalid(["'\(key)' is missing it's dependency of '\(dependency)'"]) -// } -// return .Valid -// }) -// } -// } -// -// return .Valid -// } -// } -// -// if let dependencies = schema["dependencies"] as? [String:Any] { -// for (key, dependencies) in dependencies { -// if let dependencies = dependencies as? [String: Any] { -// let schema = allOf(JSONSchema.validators(root)(dependencies)) -// validators.append(validateDependency(key, validator: schema)) -// } else if let dependencies = dependencies as? [String] { -// validators.append(validateDependencies(key, dependencies: dependencies)) -// } -// } -// } -// -// if let format = schema["format"] as? String { -// if let validator = root.formats[format] { -// validators.append(validator) -// } else { -// validators.append(invalidValidation("'format' validation of '\(format)' is not yet supported.")) -// } -// } -// -// return validators -// } -//} -// -//public func validate(_ value:Any, schema:[String:Any]) -> ValidationResult { -// let root = Schema(schema) -// let validator = allOf(validators(root)(schema)) -// let result = validator(value) -// return result -//} -// -///// Extension for dictionary providing initialization from array of elements -//extension Dictionary { -// init(_ pairs: [Element]) { -// self.init() -// -// for (key, value) in pairs { -// self[key] = value -// } -// } -//} -// -//public enum ValidationResult { -// case Valid -// case invalid([String]) -// -// public var valid: Bool { -// switch self { -// case .Valid: -// return true -// case .invalid: -// return false -// } -// } -// -// public var errors:[String]? { -// switch self { -// case .Valid: -// return nil -// case .invalid(let errors): -// return errors -// } -// } -//} -// -//typealias LegacyValidator = (Any) -> (Bool) -//typealias Validator = (Any) -> (ValidationResult) -// -///// Flatten an array of results into a single result (combining all errors) -//func flatten(_ results:[ValidationResult]) -> ValidationResult { -// let failures = results.filter { result in !result.valid } -// if failures.count > 0 { -// let errors = failures.reduce([String]()) { (accumulator, failure) in -// if let errors = failure.errors { -// return accumulator + errors -// } -// -// return accumulator -// } -// -// return .invalid(errors) -// } -// -// return .Valid -//} -// -///// Creates a Validator which always returns an valid result -//func validValidation(_ value:Any) -> ValidationResult { -// return .Valid -//} -// -///// Creates a Validator which always returns an invalid result with the given error -//func invalidValidation(_ error: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// return .invalid([error]) -// } -//} -// -//// MARK: Shared -///// Validate the given value is of the given type -//func validateType(_ type: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// switch type { -// case "integer": -// if let number = value as? NSNumber { -// if !CFNumberIsFloatType(number) && CFGetTypeID(number) != CFBooleanGetTypeID() { -// return .Valid -// } -// } -// case "number": -// if let number = value as? NSNumber { -// if CFGetTypeID(number) != CFBooleanGetTypeID() { -// return .Valid -// } -// } -// case "string": -// if value is String { -// return .Valid -// } -// case "object": -// if value is NSDictionary { -// return .Valid -// } -// case "array": -// if value is NSArray { -// return .Valid -// } -// case "boolean": -// if let number = value as? NSNumber { -// if CFGetTypeID(number) == CFBooleanGetTypeID() { -// return .Valid -// } -// } -// case "null": -// if value is NSNull { -// return .Valid -// } -// default: -// break -// } -// -// return .invalid(["'\(value)' is not of type '\(type)'"]) -// } -//} -// + + if let dependencies = try? schema.get("dependencies") as [String: JSON] { + for (key, dependencies) in dependencies { + if let _ = try? dependencies.get() as [String: JSON] { + let dependencies = try! dependencies.get() as JSON + let schema = allOf(getValidators(root)(dependencies)) + validators.append(validateDependency(key, validator: schema)) + } else if let dependencies = try? dependencies.get() as [String] { + validators.append(validateDependencies(key, dependencies: dependencies)) + } + } + } + + if let format = try? schema.get("format") as String { + if let validator = root.formats[format] { + validators.append(validator) + } else { + validators.append( + invalidValidation("'format' validation of '\(format)' is not yet supported.") + ) + } + } + + return validators + } +} + +public func validate(_ value: JSON, schema: [String: JSON]) -> ValidationResult { + let schema = JSON(schema) + let root = JSON.Schema(schema) + let validator = allOf(getValidators(root)(schema)) + let result = validator(value) + return result +} + +/// Extension for dictionary providing initialization from array of elements +extension Dictionary { + init(_ pairs: [Element]) { + self.init() + + for (key, value) in pairs { + self[key] = value + } + } +} + +/// Flatten an array of results into a single result (combining all errors) +func flatten(_ results:[ValidationResult]) -> ValidationResult { + let failures = results.filter({ result in !result.isValid }) + + if failures.count > 0 { + let errors: [String] = failures.reduce([]) { (accumulator, failure) in + return accumulator + failure.errors + } + + return .invalid(errors) + } + + return .valid +} + +/// Creates a Validator which always returns an valid result +func validValidation(_ value: JSON) -> ValidationResult { + return .valid +} + +/// Creates a Validator which always returns an invalid result with the given error +func invalidValidation(_ error: String) -> (_ value: JSON) -> ValidationResult { + return { value in + return .invalid([error]) + } +} + +// MARK: Shared +/// Validate the given value is of the given type +func validateType(_ type: String) -> (_ value: JSON) -> ValidationResult { + return { value in + switch type { + case "integer": + if value.isInt { + return .valid + } + case "number": + if value.isInt || value.isDouble { + return .valid + } + case "string": + if value.isString { + return .valid + } + case "object": + if value.isObject { + return .valid + } + case "array": + if value.isArray { + return .valid + } + case "boolean": + if value.isBool { + return .valid + } + case "null": + if value.isNull { + return .valid + } + default: + break + } + + return .invalid(["'\(value)' is not of type '\(type)'"]) + } +} + ///// Validate the given value is one of the given types //func validateType(_ type:[String]) -> Validator { // let typeValidators = type.map(validateType) as [Validator] @@ -457,196 +512,249 @@ // // return invalidValidation("'\(type)' is not a valid 'type'") //} -// -// -///// Validate that a value is valid for any of the given validation rules -//func anyOf(_ validators:[Validator], error:String? = nil) -> (_ value: Any) -> ValidationResult { -// return { value in -// for validator in validators { -// let result = validator(value) -// if result.valid { -// return .Valid -// } -// } -// -// if let error = error { -// return .invalid([error]) -// } -// -// return .invalid(["\(value) does not meet anyOf validation rules."]) -// } -//} -// -//func oneOf(_ validators: [Validator]) -> (_ value: Any) -> ValidationResult { -// return { value in -// let results = validators.map { validator in validator(value) } -// let validValidators = results.filter { $0.valid }.count -// -// if validValidators == 1 { -// return .Valid -// } -// -// return .invalid(["\(validValidators) validates instead `oneOf`."]) -// } -//} -// -///// Creates a validator that validates that the given validation rules are not met -//func not(_ validator: @escaping Validator) -> (_ value: Any) -> ValidationResult { -// return { value in -// if validator(value).valid { -// return .invalid(["'\(value)' does not match 'not' validation."]) -// } -// -// return .Valid -// } -//} -// -//func allOf(_ validators: [Validator]) -> (_ value: Any) -> ValidationResult { -// return { value in -// return flatten(validators.map { validator in validator(value) }) -// } -//} -// -//func validateEnum(_ values: [Any]) -> (_ value: Any) -> ValidationResult { -// return { value in -// if (values as! [NSObject]).contains(value as! NSObject) { -// return .Valid -// } -// -// return .invalid(["'\(value)' is not a valid enumeration value of '\(values)'"]) -// } -//} -// -//// MARK: String -//func validateLength(_ comparitor: @escaping ((Int, Int) -> (Bool)), length: Int, error: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? String { -// if !comparitor(value.characters.count, length) { -// return .invalid([error]) -// } -// } -// -// return .Valid -// } -//} -// -//func validatePattern(_ pattern: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? String { -// let expression = try? NSRegularExpression(pattern: pattern, options: NSRegularExpression.Options(rawValue: 0)) -// if let expression = expression { -// let range = NSMakeRange(0, value.characters.count) -// if expression.matches(in: value, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: range).count == 0 { -// return .invalid(["'\(value)' does not match pattern: '\(pattern)'"]) -// } -// } else { -// return .invalid(["[Schema] Regex pattern '\(pattern)' is not valid"]) -// } -// } -// -// return .Valid -// } -//} -// + + +/// Validate that a value is valid for any of the given validation rules +func anyOf(_ validators: [Validate], error: String? = nil) -> (_ value: JSON) -> ValidationResult { + return { value in + for validator in validators { + let result = validator(value) + + if result.isValid { + return .valid + } + } + + if let error = error { + return .invalid([error]) + } + + return .invalid(["\(value) does not meet anyOf validation rules."]) + } +} + +func oneOf(_ validators: [Validate]) -> (_ value: JSON) -> ValidationResult { + return { value in + let results = validators.map({ validator in validator(value) }) + let validValidators = results.filter({ $0.isValid }).count + + if validValidators == 1 { + return .valid + } + + return .invalid(["\(validValidators) validates instead `oneOf`."]) + } +} + +/// Creates a validator that validates that the given validation rules are not met +func not(_ validator: @escaping Validate) -> (_ value: JSON) -> ValidationResult { + return { value in + if validator(value).isValid { + return .invalid(["'\(value)' does not match 'not' validation."]) + } + + return .valid + } +} + +func allOf(_ validators: [Validate]) -> (_ value: JSON) -> ValidationResult { + return { value in + return flatten(validators.map { validator in validator(value) }) + } +} + +func validateEnum(_ values: [JSON]) -> (_ value: JSON) -> ValidationResult { + return { value in + if values.contains(value) { + return .valid + } + + return .invalid(["'\(value)' is not a valid enumeration value of '\(values)'"]) + } +} + +// MARK: String + +func validateLength( + _ comparator: @escaping ((Int, Int) -> (Bool)), + length: Int, + error: String +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as String { + if !comparator(value.characters.count, length) { + return .invalid([error]) + } + } + + // TODO: Maybe this should be an error? + return .valid + } +} + +func validatePattern(_ pattern: String) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as String { + let expression = try? NSRegularExpression( + pattern: pattern, + options: [] + ) + + if let expression = expression { + let range = NSMakeRange(0, value.characters.count) + + if expression.matches(in: value, options: [], range: range).count == 0 { + return .invalid(["'\(value)' does not match pattern: '\(pattern)'"]) + } + } else { + return .invalid(["[Schema] Regex pattern '\(pattern)' is not valid"]) + } + } + + return .valid + } +} + //// MARK: Numerical -//func validateMultipleOf(_ number: Double) -> (_ value: Any) -> ValidationResult { -// return { value in -// if number > 0.0 { -// if let value = value as? Double { -// let result = value / number -// if result != floor(result) { -// return .invalid(["\(value) is not a multiple of \(number)"]) -// } -// } -// } -// -// return .Valid -// } -//} -// -//func validateNumericLength(_ length: Double, comparitor: @escaping ((Double, Double) -> (Bool)), exclusiveComparitor: @escaping ((Double, Double) -> (Bool)), exclusive: Bool?, error: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? Double { -// if exclusive ?? false { -// if !exclusiveComparitor(value, length) { -// return .invalid([error]) -// } -// } -// -// if !comparitor(value, length) { -// return .invalid([error]) -// } -// } -// -// return .Valid -// } -//} -// + +func validateMultipleOf(_ number: Double) -> (_ value: JSON) -> ValidationResult { + return { value in + if number > 0.0 { + if let value = try? value.get() as Double { + let result = value / number + + if result != floor(result) { + return .invalid(["\(value) is not a multiple of \(number)"]) + } + } + } + + return .valid + } +} + +func validateNumericLength( + _ length: Double, + comparator: @escaping ((Double, Double) -> (Bool)), + exclusiveComparitor: @escaping ((Double, Double) -> (Bool)), + exclusive: Bool?, + error: String +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as Double { + if exclusive ?? false { + if !exclusiveComparitor(value, length) { + return .invalid([error]) + } + } + + if !comparator(value, length) { + return .invalid([error]) + } + } + + return .valid + } +} + //// MARK: Array -//func validateArrayLength(_ rhs: Int, comparitor: @escaping ((Int, Int) -> Bool), error: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? [Any] { -// if !comparitor(value.count, rhs) { -// return .invalid([error]) -// } -// } -// -// return .Valid -// } -//} -// -//func validateUniqueItems(_ value: Any) -> ValidationResult { -// if let value = value as? [Any] { -// // 1 and true, 0 and false are isEqual for NSNumber's, so logic to count for that below -// func isBoolean(_ number:NSNumber) -> Bool { -// return CFGetTypeID(number) != CFBooleanGetTypeID() -// } -// -// let numbers = value.filter { value in value is NSNumber } as! [NSNumber] -// let numerBooleans = numbers.filter(isBoolean) -// let booleans = numerBooleans as [Bool] -// let nonBooleans = numbers.filter { number in !isBoolean(number) } -// let hasTrueAndOne = booleans.filter { v in v }.count > 0 && nonBooleans.filter { v in v == 1 }.count > 0 -// let hasFalseAndZero = booleans.filter { v in !v }.count > 0 && nonBooleans.filter { v in v == 0 }.count > 0 -// let delta = (hasTrueAndOne ? 1 : 0) + (hasFalseAndZero ? 1 : 0) -// -// if (NSSet(array: value).count + delta) == value.count { -// return .Valid -// } -// -// return .invalid(["\(value) does not have unique items"]) -// } -// -// return .Valid -//} -// -//// MARK: object -//func validatePropertiesLength(_ length: Int, comparitor: @escaping ((Int, Int) -> (Bool)), error: String) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? [String:Any] { -// if !comparitor(length, value.count) { -// return .invalid([error]) -// } -// } -// -// return .Valid -// } -//} -// -//func validateRequired(_ required: [String]) -> (_ value: Any) -> ValidationResult { -// return { value in -// if let value = value as? [String:Any] { -// if (required.filter { r in !value.keys.contains(r) }.count == 0) { -// return .Valid -// } -// -// return .invalid(["Required properties are missing '\(required)'"]) -// } -// -// return .Valid -// } -//} -// + +func validateArrayLength( + _ rhs: Int, + comparator: @escaping ((Int, Int) -> Bool), + error: String +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as [JSON] { + if !comparator(value.count, rhs) { + return .invalid([error]) + } + } + + return .valid + } +} + +// MARK: object + +func validatePropertiesLength( + _ length: Int, + comparator: @escaping ((Int, Int) -> (Bool)), + error: String +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as [String: JSON] { + if !comparator(length, value.count) { + return .invalid([error]) + } + } + + return .valid + } +} + +func validateRequired(_ required: [String]) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as [String: JSON] { + if (required.filter { r in !value.keys.contains(r) }.count == 0) { + return .valid + } + + return .invalid(["Required properties are missing '\(required)'"]) + } + + return .valid + } +} + +func validateDependency( + _ key: String, + validator: @escaping Validate +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let object = try? value.get() as [String: JSON] { + if object[key] != nil { + return validator(value) + } + } + + return .valid + } +} + +func validateDependencies( + _ key: String, + dependencies: [String] +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let value = try? value.get() as [String: JSON] { + if value[key] != nil { + return flatten(dependencies.map { dependency in + if value[dependency] == nil { + return .invalid(["'\(key)' is missing it's dependency of '\(dependency)'"]) + } + + return .valid + }) + } + } + + return .valid + } +} + +func validateItems( + _ itemsValidators: @escaping (JSON) -> ValidationResult +) -> (_ value: JSON) -> ValidationResult { + return { value in + if let document = try? value.get() as [JSON] { + return flatten(document.map(itemsValidators)) + } + + return .valid + } +} + //func validateProperties(_ properties: [String:Validator]?, patternProperties: [String:Validator]?, additionalProperties: Validator?) -> (_ value: Any) -> ValidationResult { // return { value in // if let value = value as? [String:Any] { @@ -718,31 +826,47 @@ // return true // } //} -// -//// MARK: Format -//func validateIPv4(_ value:Any) -> ValidationResult { -// if let ipv4 = value as? String { -// if let expression = try? NSRegularExpression(pattern: "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", options: NSRegularExpression.Options(rawValue: 0)) { -// if expression.matches(in: ipv4, options: NSRegularExpression.MatchingOptions(rawValue: 0), range: NSMakeRange(0, ipv4.characters.count)).count == 1 { -// return .Valid -// } -// } -// -// return .invalid(["'\(ipv4)' is not valid IPv4 address."]) -// } -// -// return .Valid -//} -// -//func validateIPv6(_ value:Any) -> ValidationResult { -// if let ipv6 = value as? String { -// var buf = UnsafeMutablePointer.allocate(capacity: Int(INET6_ADDRSTRLEN)) -// if inet_pton(AF_INET6, ipv6, &buf) == 1 { -// return .Valid -// } -// -// return .invalid(["'\(ipv6)' is not valid IPv6 address."]) -// } -// -// return .Valid -//} + +// MARK: Format + +func validateIPv4(_ value:Any) -> ValidationResult { + if let ipv4 = value as? String { + if let expression = try? NSRegularExpression( + pattern: "^(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$", + options: [] + ) { + if expression.matches(in: ipv4, options: [], range: NSMakeRange(0, ipv4.characters.count)).count == 1 { + return .valid + } + } + + return .invalid(["'\(ipv4)' is not valid IPv4 address."]) + } + + return .valid +} + +func validateIPv6(_ value:Any) -> ValidationResult { + if let ipv6 = value as? String { + var buf = UnsafeMutablePointer.allocate(capacity: Int(INET6_ADDRSTRLEN)) + + if inet_pton(AF_INET6, ipv6, &buf) == 1 { + return .valid + } + + return .invalid(["'\(ipv6)' is not valid IPv6 address."]) + } + + return .valid +} + +extension String { + func stringByRemovingPrefix(_ prefix: String) -> String? { + if hasPrefix(prefix) { + let index = characters.index(startIndex, offsetBy: prefix.characters.count) + return substring(from: index) + } + + return nil + } +} diff --git a/Tests/ContentTests/JSONTests.swift b/Tests/ContentTests/JSONTests.swift new file mode 100644 index 00000000..14be99dc --- /dev/null +++ b/Tests/ContentTests/JSONTests.swift @@ -0,0 +1,30 @@ +import XCTest +@testable import Content +import Foundation + +public class JSONTests: XCTestCase { + func testJSONSchema() throws { + let schema = JSON.Schema([ + "type": "object", + "properties": [ + "name": ["type": "string"], + "price": ["type": "number"], + ], + "required": ["name"], + ]) + + var result = schema.validate(["name": "Eggs", "price": 34.99]) + XCTAssert(result.isValid) + + result = schema.validate(["price": 34.99]) + XCTAssertEqual(result.errors, ["Required properties are missing '[\"name\"]\'"]) + } +} + +extension JSONTests { + public static var allTests: [(String, (JSONTests) -> () throws -> Void)] { + return [ + ("testJSONSchema", testJSONSchema), + ] + } +}