Skip to content

Add support for date, time, and date-time validators #118

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
wants to merge 20 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
/.build/
/.swiftpm/
1 change: 1 addition & 0 deletions Sources/Draft201909Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ public class Draft201909Validator: Validator {
"ipv4": validateIPv4,
"ipv6": validateIPv6,
"uri": validateURI,
"date-time": validateDateTime,
"uuid": validateUUID,
"regex": validateRegex,
"json-pointer": validateJSONPointer,
Expand Down
1 change: 1 addition & 0 deletions Sources/Draft202012Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ public class Draft202012Validator: Validator {
]

let formats: [String: (Context, String) -> (AnySequence<ValidationError>)] = [
"date-time": validateDateTime,
"ipv4": validateIPv4,
"ipv6": validateIPv6,
"uri": validateURI,
Expand Down
1 change: 1 addition & 0 deletions Sources/Draft7Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,7 @@ public class Draft7Validator: Validator {
"ipv4": validateIPv4,
"ipv6": validateIPv6,
"uri": validateURI,
"date-time": validateDateTime,
"json-pointer": validateJSONPointer,
"regex": validateRegex,
"time": validateTime,
Expand Down
46 changes: 46 additions & 0 deletions Sources/Validation/datetime.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import Foundation

func validateDateTime(_ context: Context, _ value: Any) -> AnySequence<ValidationError> {
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While testing some of the examples from rfc3339 (https://tools.ietf.org/html/rfc3339#section-5.8), appears this isn't behaving as expected with leap seconds, the test below seems to fail.

import XCTest
import JSONSchema


class DateTimeFormatTests: XCTestCase {
  func testDateTimeWithLeapSecond() throws {
    let schema: [String: Any] = [
      "$schema": "https://json-schema.org/draft/2020-12/schema",
      "format": "date-time",
    ]

    let result = try validate("1990-12-31T23:59:60Z", schema: schema)

    XCTAssertTrue(result.valid)
  }
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kylef I'm a bit confused about this one as it's called out as invalid in the jsonschema.org tests repo - see https://github.com/json-schema-org/JSON-Schema-Test-Suite/blob/6bc53e60c3de5d2e2ab15a84bcd3a79ca12c66a9/tests/draft2020-12/optional/format/date-time.json#L28

This seems to be at odds with the spec though:

   1990-12-31T15:59:60-08:00

This represents the same leap second in Pacific Standard Time, 8
hours behind UTC.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting, unsure if that's test is trying to disallow a leap second or instead there being 31 days in February. We can try to clarify that in the tests suite.

Started with json-schema-org/JSON-Schema-Test-Suite#481

Copy link
Owner

@kylef kylef Apr 22, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1990-02-31T15:59:60.123-08:00 this particular date would be invalid even for a leap second, because leap seconds wouldn't be allowed in that date or time, but it should be possible in other times. There's further explanation at https://tools.ietf.org/html/rfc3339#appendix-D.

The leap second would need to be at the end of the day.

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())
}
1 change: 0 additions & 1 deletion Sources/Validators.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
35 changes: 35 additions & 0 deletions Sources/format.swift
Original file line number Diff line number Diff line change
Expand Up @@ -146,3 +146,38 @@ func validateJSONPointer(_ context: Context, _ value: String) -> AnySequence<Val

return AnySequence(EmptyCollection())
}

func validateDate(_ context: Context, _ value: Any) -> AnySequence<ValidationError> {
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())
}
2 changes: 1 addition & 1 deletion Tests/Cases
Submodule Cases updated from 812f1f to 77f1d1
16 changes: 16 additions & 0 deletions Tests/JSONSchemaTests/Validation/TestTime.swift
Original file line number Diff line number Diff line change
@@ -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)
}
}