diff --git a/.gitignore b/.gitignore index 0e03e15..af13669 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,2 @@ /.build/ +/.swiftpm/ diff --git a/Sources/Draft201909Validator.swift b/Sources/Draft201909Validator.swift index 51ba5ce..070f7bc 100644 --- a/Sources/Draft201909Validator.swift +++ b/Sources/Draft201909Validator.swift @@ -58,6 +58,9 @@ public class Draft201909Validator: Validator { "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, + "date-time": validateDateTime, + "date": validateDate, + "time": validateTime, "uuid": validateUUID, "regex": validateRegex, "json-pointer": validateJSONPointer, diff --git a/Sources/Draft202012Validator.swift b/Sources/Draft202012Validator.swift index ee1aeb0..1440c0c 100644 --- a/Sources/Draft202012Validator.swift +++ b/Sources/Draft202012Validator.swift @@ -56,11 +56,14 @@ public class Draft202012Validator: Validator { ] let formats: [String: (Context, String) -> (AnySequence)] = [ + "date-time": validateDateTime, + "date": validateDate, "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, "uuid": validateUUID, "regex": validateRegex, + "time": validateTime, "json-pointer": validateJSONPointer, ] diff --git a/Sources/Draft7Validator.swift b/Sources/Draft7Validator.swift index 40ccf7d..71fa1be 100644 --- a/Sources/Draft7Validator.swift +++ b/Sources/Draft7Validator.swift @@ -48,6 +48,9 @@ public class Draft7Validator: Validator { "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, + "date-time": validateDateTime, + "date": validateDate, + "time": validateTime, "json-pointer": validateJSONPointer, "regex": validateRegex, ] diff --git a/Sources/JSONSchema.swift b/Sources/JSONSchema.swift index e452e39..2749bb1 100644 --- a/Sources/JSONSchema.swift +++ b/Sources/JSONSchema.swift @@ -64,9 +64,10 @@ public struct Schema { func validator(for schema: [String: Any]) -> Validator { - guard let schemaURI = schema["$schema"] as? String else { + guard var schemaURI = schema["$schema"] as? String else { return Draft4Validator(schema: schema) } + schemaURI = schemaURI.replacingOccurrences(of: "http://", with: "https://").trimmingCharacters(in: ["#", "/"]) if let id = DRAFT_2020_12_META_SCHEMA["$id"] as? String, schemaURI == id { return Draft202012Validator(schema: schema) diff --git a/Sources/Validation/datetime.swift b/Sources/Validation/datetime.swift new file mode 100644 index 0000000..45250d6 --- /dev/null +++ b/Sources/Validation/datetime.swift @@ -0,0 +1,46 @@ +import Foundation + +func validateDateTime(_ context: Context, _ value: Any) -> AnySequence { + if let date = value as? String { + if let regularExpression = try? NSRegularExpression(pattern: "^\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}", options: .caseInsensitive) { + let range = NSRange(location: 0, length: date.utf16.count) + let result = regularExpression.matches(in: date, options: [], range: range) + if result.isEmpty { + return AnySequence([ + ValidationError( + "'\(date)' is not a valid RFC 3339 formatted date.", + instanceLocation: context.instanceLocation, + keywordLocation: context.keywordLocation + ) + ]) + } + } + + let rfc3339DateTimeFormatter = DateFormatter() + + rfc3339DateTimeFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ssZZZZZ" + if rfc3339DateTimeFormatter.date(from: date) != nil { + return AnySequence(EmptyCollection()) + } + + rfc3339DateTimeFormatter.dateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSSZZZZZ" + if rfc3339DateTimeFormatter.date(from: date) != nil { + return AnySequence(EmptyCollection()) + } + + rfc3339DateTimeFormatter.dateFormat = "yyyy-MM-dd't'HH:mm:ss.SSS'z'" + if rfc3339DateTimeFormatter.date(from: date) != nil { + return AnySequence(EmptyCollection()) + } + + return AnySequence([ + ValidationError( + "'\(date)' is not a valid RFC 3339 formatted date-time.", + instanceLocation: context.instanceLocation, + keywordLocation: context.keywordLocation + ) + ]) + } + + return AnySequence(EmptyCollection()) +} diff --git a/Sources/Validation/time.swift b/Sources/Validation/time.swift new file mode 100644 index 0000000..6a9bc7a --- /dev/null +++ b/Sources/Validation/time.swift @@ -0,0 +1,27 @@ +import Foundation + +func validateTime(_ context: Context, _ value: Any) -> AnySequence { + if let date = value as? String { + let rfc3339DateTimeFormatter = DateFormatter() + + rfc3339DateTimeFormatter.dateFormat = "HH:mm:ss.SSSZZZZZ" + if rfc3339DateTimeFormatter.date(from: date) != nil { + return AnySequence(EmptyCollection()) + } + + rfc3339DateTimeFormatter.dateFormat = "HH:mm:ssZZZZZ" + if rfc3339DateTimeFormatter.date(from: date) != nil { + return AnySequence(EmptyCollection()) + } + + return AnySequence([ + ValidationError( + "'\(date)' is not a valid RFC 3339 formatted time.", + instanceLocation: context.instanceLocation, + keywordLocation: context.keywordLocation + ) + ]) + } + + return AnySequence(EmptyCollection()) +} diff --git a/Sources/Validators.swift b/Sources/Validators.swift index b289ce6..aacf912 100644 --- a/Sources/Validators.swift +++ b/Sources/Validators.swift @@ -145,7 +145,6 @@ func isEqual(_ lhs: NSObject, _ rhs: NSObject) -> Bool { return lhs == rhs } - extension Sequence where Iterator.Element == ValidationError { func validationResult() -> ValidationResult { let errors = Array(self) diff --git a/Sources/format.swift b/Sources/format.swift index 44cf7a1..cb890fa 100644 --- a/Sources/format.swift +++ b/Sources/format.swift @@ -161,3 +161,38 @@ func validateJSONPointer(_ context: Context, _ value: Any) -> AnySequence AnySequence { + if let date = value as? String { + if let regularExpression = try? NSRegularExpression(pattern: "^\\d{4}-\\d{2}-\\d{2}$", options: []) { + let range = NSRange(location: 0, length: date.utf16.count) + let result = regularExpression.matches(in: date, options: [], range: range) + if result.isEmpty { + return AnySequence([ + ValidationError( + "'\(date)' is not a valid RFC 3339 formatted date.", + instanceLocation: context.instanceLocation, + keywordLocation: context.keywordLocation + ) + ]) + } + } + + let rfc3339DateTimeFormatter = DateFormatter() + + rfc3339DateTimeFormatter.dateFormat = "yyyy-MM-dd" + if rfc3339DateTimeFormatter.date (from: date) != nil { + return AnySequence(EmptyCollection()) + } + + return AnySequence([ + ValidationError( + "'\(date)' is not a valid RFC 3339 formatted date.", + instanceLocation: context.instanceLocation, + keywordLocation: context.keywordLocation + ) + ]) + } + + return AnySequence(EmptyCollection()) +} diff --git a/Tests/JSONSchemaTests/JSONSchemaCases.swift b/Tests/JSONSchemaTests/JSONSchemaCases.swift index e4ac246..67e1dc8 100644 --- a/Tests/JSONSchemaTests/JSONSchemaCases.swift +++ b/Tests/JSONSchemaTests/JSONSchemaCases.swift @@ -117,7 +117,6 @@ class JSONSchemaCases: XCTestCase { "infinite-loop-detection.json", // optional formats - "date-time.json", "email.json", "hostname.json", "uri-reference.json", @@ -137,8 +136,6 @@ class JSONSchemaCases: XCTestCase { "infinite-loop-detection.json", // optional, format - "date-time.json", - "date.json", "email.json", "hostname.json", "idn-email.json", @@ -146,7 +143,6 @@ class JSONSchemaCases: XCTestCase { "iri-reference.json", "iri.json", "relative-json-pointer.json", - "time.json", "uri-reference.json", "uri-template.json", ] + additionalExclusions) @@ -173,8 +169,6 @@ class JSONSchemaCases: XCTestCase { // optional, format "format.json", - "date-time.json", - "date.json", "duration.json", "email.json", "hostname.json", @@ -183,7 +177,6 @@ class JSONSchemaCases: XCTestCase { "iri-reference.json", "iri.json", "relative-json-pointer.json", - "time.json", "uri-reference.json", "uri-template.json", ] + additionalExclusions) @@ -212,8 +205,6 @@ class JSONSchemaCases: XCTestCase { // optional, format "format.json", - "date-time.json", - "date.json", "duration.json", "email.json", "hostname.json", @@ -222,7 +213,6 @@ class JSONSchemaCases: XCTestCase { "iri-reference.json", "iri.json", "relative-json-pointer.json", - "time.json", "uri-reference.json", "uri-template.json", ] + additionalExclusions) @@ -245,7 +235,7 @@ class JSONSchemaCases: XCTestCase { return cases.filter { if let schema = $0.schema as? [String: Any] { let format = schema["format"] as! String - return !["date-time", "email", "hostname"].contains(format) + return !["email", "hostname"].contains(format) } return true diff --git a/Tests/JSONSchemaTests/Validation/TestTime.swift b/Tests/JSONSchemaTests/Validation/TestTime.swift new file mode 100644 index 0000000..1817cbe --- /dev/null +++ b/Tests/JSONSchemaTests/Validation/TestTime.swift @@ -0,0 +1,16 @@ +import XCTest +@testable import JSONSchema + + +class TimeFormatTests: XCTestCase { + func testTimeWithoutSecondFraction() throws { + let schema: [String: Any] = [ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "format": "time", + ] + + let result = try validate("23:59:50Z", schema: schema) + + XCTAssertTrue(result.valid) + } +}