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 29f6f2b..0d2df86 100644 --- a/Sources/Draft201909Validator.swift +++ b/Sources/Draft201909Validator.swift @@ -58,6 +58,7 @@ public class Draft201909Validator: Validator { "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, + "date-time": validateDateTime, "uuid": validateUUID, "regex": validateRegex, "json-pointer": validateJSONPointer, diff --git a/Sources/Draft202012Validator.swift b/Sources/Draft202012Validator.swift index eb2acda..21f72f4 100644 --- a/Sources/Draft202012Validator.swift +++ b/Sources/Draft202012Validator.swift @@ -56,6 +56,7 @@ public class Draft202012Validator: Validator { ] let formats: [String: (Context, String) -> (AnySequence)] = [ + "date-time": validateDateTime, "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, diff --git a/Sources/Draft7Validator.swift b/Sources/Draft7Validator.swift index ae0cceb..071f28d 100644 --- a/Sources/Draft7Validator.swift +++ b/Sources/Draft7Validator.swift @@ -48,6 +48,7 @@ public class Draft7Validator: Validator { "ipv4": validateIPv4, "ipv6": validateIPv6, "uri": validateURI, + "date-time": validateDateTime, "json-pointer": validateJSONPointer, "regex": validateRegex, "time": validateTime, 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/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 136c990..d460c8f 100644 --- a/Sources/format.swift +++ b/Sources/format.swift @@ -146,3 +146,38 @@ func validateJSONPointer(_ context: Context, _ value: String) -> 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/Cases b/Tests/Cases index 812f1f0..77f1d10 160000 --- a/Tests/Cases +++ b/Tests/Cases @@ -1 +1 @@ -Subproject commit 812f1f08c3dbf9c760c716ced2f314ca15afb198 +Subproject commit 77f1d10cbb23b2f4d2bbf50ca1d5cf3804cb2ce4 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) + } +}