Skip to content
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/
3 changes: 3 additions & 0 deletions Sources/Draft201909Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
3 changes: 3 additions & 0 deletions Sources/Draft202012Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -56,11 +56,14 @@ public class Draft202012Validator: Validator {
]

let formats: [String: (Context, String) -> (AnySequence<ValidationError>)] = [
"date-time": validateDateTime,
"date": validateDate,
"ipv4": validateIPv4,
"ipv6": validateIPv6,
"uri": validateURI,
"uuid": validateUUID,
"regex": validateRegex,
"time": validateTime,
"json-pointer": validateJSONPointer,
]

Expand Down
3 changes: 3 additions & 0 deletions Sources/Draft7Validator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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,
]
Expand Down
3 changes: 2 additions & 1 deletion Sources/JSONSchema.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
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> {
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())
}
27 changes: 27 additions & 0 deletions Sources/Validation/time.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import Foundation

func validateTime(_ context: Context, _ value: Any) -> AnySequence<ValidationError> {
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())
}
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 @@ -161,3 +161,38 @@ func validateJSONPointer(_ context: Context, _ value: Any) -> AnySequence<Valida

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())
}
12 changes: 1 addition & 11 deletions Tests/JSONSchemaTests/JSONSchemaCases.swift
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,6 @@ class JSONSchemaCases: XCTestCase {
"infinite-loop-detection.json",

// optional formats
"date-time.json",
"email.json",
"hostname.json",
"uri-reference.json",
Expand All @@ -137,16 +136,13 @@ class JSONSchemaCases: XCTestCase {
"infinite-loop-detection.json",

// optional, format
"date-time.json",
"date.json",
"email.json",
"hostname.json",
"idn-email.json",
"idn-hostname.json",
"iri-reference.json",
"iri.json",
"relative-json-pointer.json",
"time.json",
"uri-reference.json",
"uri-template.json",
] + additionalExclusions)
Expand All @@ -173,8 +169,6 @@ class JSONSchemaCases: XCTestCase {

// optional, format
"format.json",
"date-time.json",
"date.json",
"duration.json",
"email.json",
"hostname.json",
Expand All @@ -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)
Expand Down Expand Up @@ -212,8 +205,6 @@ class JSONSchemaCases: XCTestCase {

// optional, format
"format.json",
"date-time.json",
"date.json",
"duration.json",
"email.json",
"hostname.json",
Expand All @@ -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)
Expand All @@ -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
Expand Down
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)
}
}