Skip to content

Commit 1d853b0

Browse files
authored
Add convenience method for checking compatibility of value against schema (#7)
* Add convenience method for checking compatibility of value against schema * Add test coverage
1 parent f30aec4 commit 1d853b0

2 files changed

Lines changed: 138 additions & 0 deletions

File tree

Sources/JSONSchema/JSONValue.swift

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,58 @@ extension String {
422422
}
423423
}
424424
}
425+
426+
// MARK: - Schema Compatibility
427+
428+
extension JSONValue {
429+
/// Checks if the basic type of this JSON value matches the type expected by a JSON schema.
430+
///
431+
/// This method primarily checks for type alignment (e.g., JSON object vs. schema object type).
432+
/// It does not perform deep validation of constraints like `minimum`, `maxLength`, etc.
433+
/// For composite schemas (`anyOf`, `allOf`, `oneOf`, `not`), references, `any`, or `empty`,
434+
/// this method generally returns `true`, as compatibility depends on further evaluation.
435+
///
436+
/// - Parameters:
437+
/// - schema: The `JSONSchema` to check compatibility against.
438+
/// - strict: If `true` (default), requires exact numeric type matching (`.int` for `.integer`,
439+
/// `.double` for `.number`). If `false`, allows `.int` to be compatible with `.number`.
440+
/// - Returns: `true` if the value's basic type is compatible with the schema's expected type, `false` otherwise.
441+
public func isCompatible(with schema: JSONSchema, strict: Bool = true) -> Bool {
442+
switch schema {
443+
case .object:
444+
// Check if the JSONValue is an object
445+
guard case .object = self else { return false }
446+
return true
447+
case .array:
448+
// Check if the JSONValue is an array
449+
guard case .array = self else { return false }
450+
return true
451+
case .string:
452+
// Check if the JSONValue is a string
453+
guard case .string = self else { return false }
454+
return true
455+
case .number:
456+
// Compatible if it's a double, or if not strict and it's an int
457+
switch self {
458+
case .double: return true
459+
case .int: return !strict
460+
default: return false
461+
}
462+
case .integer:
463+
// Check if the JSONValue is an int
464+
guard case .int = self else { return false }
465+
return true
466+
case .boolean:
467+
// Check if the JSONValue is a bool
468+
guard case .bool = self else { return false }
469+
return true
470+
case .null:
471+
// Check if the JSONValue is null
472+
return self == .null
473+
case .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any:
474+
// Compatibility for these types depends on deeper validation or context.
475+
// Assume basic type compatibility is met, deferring to full validation.
476+
return true
477+
}
478+
}
479+
}

Tests/JSONSchemaTests/JSONValueTests.swift

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -414,3 +414,86 @@ import Testing
414414
dict[.string("key")] = "value"
415415
#expect(dict[.string("key")] == "value")
416416
}
417+
418+
@Test func testJSONValueIsCompatible() {
419+
// Test object compatibility
420+
let objectValue: JSONValue = .object(["key": .string("value")])
421+
#expect(objectValue.isCompatible(with: .object))
422+
#expect(!objectValue.isCompatible(with: .array))
423+
#expect(!objectValue.isCompatible(with: .string))
424+
#expect(!objectValue.isCompatible(with: .number))
425+
#expect(!objectValue.isCompatible(with: .integer))
426+
#expect(!objectValue.isCompatible(with: .boolean))
427+
#expect(!objectValue.isCompatible(with: .null))
428+
429+
// Test array compatibility
430+
let arrayValue: JSONValue = .array([.int(1), .string("test")])
431+
#expect(!arrayValue.isCompatible(with: .object))
432+
#expect(arrayValue.isCompatible(with: .array))
433+
#expect(!arrayValue.isCompatible(with: .string))
434+
#expect(!arrayValue.isCompatible(with: .number))
435+
#expect(!arrayValue.isCompatible(with: .integer))
436+
#expect(!arrayValue.isCompatible(with: .boolean))
437+
#expect(!arrayValue.isCompatible(with: .null))
438+
439+
// Test string compatibility
440+
let stringValue: JSONValue = .string("test")
441+
#expect(!stringValue.isCompatible(with: .object))
442+
#expect(!stringValue.isCompatible(with: .array))
443+
#expect(stringValue.isCompatible(with: .string))
444+
#expect(!stringValue.isCompatible(with: .number))
445+
#expect(!stringValue.isCompatible(with: .integer))
446+
#expect(!stringValue.isCompatible(with: .boolean))
447+
#expect(!stringValue.isCompatible(with: .null))
448+
449+
// Test number compatibility
450+
let doubleValue: JSONValue = .double(3.14)
451+
#expect(!doubleValue.isCompatible(with: .object))
452+
#expect(!doubleValue.isCompatible(with: .array))
453+
#expect(!doubleValue.isCompatible(with: .string))
454+
#expect(doubleValue.isCompatible(with: .number))
455+
#expect(!doubleValue.isCompatible(with: .integer))
456+
#expect(!doubleValue.isCompatible(with: .boolean))
457+
#expect(!doubleValue.isCompatible(with: .null))
458+
459+
// Test integer compatibility
460+
let intValue: JSONValue = .int(42)
461+
#expect(!intValue.isCompatible(with: .object))
462+
#expect(!intValue.isCompatible(with: .array))
463+
#expect(!intValue.isCompatible(with: .string))
464+
#expect(intValue.isCompatible(with: .number, strict: false))
465+
#expect(!intValue.isCompatible(with: .number, strict: true))
466+
#expect(intValue.isCompatible(with: .integer))
467+
#expect(!intValue.isCompatible(with: .boolean))
468+
#expect(!intValue.isCompatible(with: .null))
469+
470+
// Test boolean compatibility
471+
let boolValue: JSONValue = .bool(true)
472+
#expect(!boolValue.isCompatible(with: .object))
473+
#expect(!boolValue.isCompatible(with: .array))
474+
#expect(!boolValue.isCompatible(with: .string))
475+
#expect(!boolValue.isCompatible(with: .number))
476+
#expect(!boolValue.isCompatible(with: .integer))
477+
#expect(boolValue.isCompatible(with: .boolean))
478+
#expect(!boolValue.isCompatible(with: .null))
479+
480+
// Test null compatibility
481+
let nullValue: JSONValue = .null
482+
#expect(!nullValue.isCompatible(with: .object))
483+
#expect(!nullValue.isCompatible(with: .array))
484+
#expect(!nullValue.isCompatible(with: .string))
485+
#expect(!nullValue.isCompatible(with: .number))
486+
#expect(!nullValue.isCompatible(with: .integer))
487+
#expect(!nullValue.isCompatible(with: .boolean))
488+
#expect(nullValue.isCompatible(with: .null))
489+
490+
// Test composite schema types (should always return true)
491+
let anyValue: JSONValue = .string("any")
492+
#expect(anyValue.isCompatible(with: .reference("$ref")))
493+
#expect(anyValue.isCompatible(with: .anyOf([])))
494+
#expect(anyValue.isCompatible(with: .allOf([])))
495+
#expect(anyValue.isCompatible(with: .oneOf([])))
496+
#expect(anyValue.isCompatible(with: .not(.string)))
497+
#expect(anyValue.isCompatible(with: .empty))
498+
#expect(anyValue.isCompatible(with: .any))
499+
}

0 commit comments

Comments
 (0)