diff --git a/Sources/JSONSchema/JSONValue.swift b/Sources/JSONSchema/JSONValue.swift index 3645e81..38e40fc 100644 --- a/Sources/JSONSchema/JSONValue.swift +++ b/Sources/JSONSchema/JSONValue.swift @@ -422,3 +422,58 @@ extension String { } } } + +// MARK: - Schema Compatibility + +extension JSONValue { + /// Checks if the basic type of this JSON value matches the type expected by a JSON schema. + /// + /// This method primarily checks for type alignment (e.g., JSON object vs. schema object type). + /// It does not perform deep validation of constraints like `minimum`, `maxLength`, etc. + /// For composite schemas (`anyOf`, `allOf`, `oneOf`, `not`), references, `any`, or `empty`, + /// this method generally returns `true`, as compatibility depends on further evaluation. + /// + /// - Parameters: + /// - schema: The `JSONSchema` to check compatibility against. + /// - strict: If `true` (default), requires exact numeric type matching (`.int` for `.integer`, + /// `.double` for `.number`). If `false`, allows `.int` to be compatible with `.number`. + /// - Returns: `true` if the value's basic type is compatible with the schema's expected type, `false` otherwise. + public func isCompatible(with schema: JSONSchema, strict: Bool = true) -> Bool { + switch schema { + case .object: + // Check if the JSONValue is an object + guard case .object = self else { return false } + return true + case .array: + // Check if the JSONValue is an array + guard case .array = self else { return false } + return true + case .string: + // Check if the JSONValue is a string + guard case .string = self else { return false } + return true + case .number: + // Compatible if it's a double, or if not strict and it's an int + switch self { + case .double: return true + case .int: return !strict + default: return false + } + case .integer: + // Check if the JSONValue is an int + guard case .int = self else { return false } + return true + case .boolean: + // Check if the JSONValue is a bool + guard case .bool = self else { return false } + return true + case .null: + // Check if the JSONValue is null + return self == .null + case .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + // Compatibility for these types depends on deeper validation or context. + // Assume basic type compatibility is met, deferring to full validation. + return true + } + } +} diff --git a/Tests/JSONSchemaTests/JSONValueTests.swift b/Tests/JSONSchemaTests/JSONValueTests.swift index 5a47fbf..7367bd1 100644 --- a/Tests/JSONSchemaTests/JSONValueTests.swift +++ b/Tests/JSONSchemaTests/JSONValueTests.swift @@ -414,3 +414,86 @@ import Testing dict[.string("key")] = "value" #expect(dict[.string("key")] == "value") } + +@Test func testJSONValueIsCompatible() { + // Test object compatibility + let objectValue: JSONValue = .object(["key": .string("value")]) + #expect(objectValue.isCompatible(with: .object)) + #expect(!objectValue.isCompatible(with: .array)) + #expect(!objectValue.isCompatible(with: .string)) + #expect(!objectValue.isCompatible(with: .number)) + #expect(!objectValue.isCompatible(with: .integer)) + #expect(!objectValue.isCompatible(with: .boolean)) + #expect(!objectValue.isCompatible(with: .null)) + + // Test array compatibility + let arrayValue: JSONValue = .array([.int(1), .string("test")]) + #expect(!arrayValue.isCompatible(with: .object)) + #expect(arrayValue.isCompatible(with: .array)) + #expect(!arrayValue.isCompatible(with: .string)) + #expect(!arrayValue.isCompatible(with: .number)) + #expect(!arrayValue.isCompatible(with: .integer)) + #expect(!arrayValue.isCompatible(with: .boolean)) + #expect(!arrayValue.isCompatible(with: .null)) + + // Test string compatibility + let stringValue: JSONValue = .string("test") + #expect(!stringValue.isCompatible(with: .object)) + #expect(!stringValue.isCompatible(with: .array)) + #expect(stringValue.isCompatible(with: .string)) + #expect(!stringValue.isCompatible(with: .number)) + #expect(!stringValue.isCompatible(with: .integer)) + #expect(!stringValue.isCompatible(with: .boolean)) + #expect(!stringValue.isCompatible(with: .null)) + + // Test number compatibility + let doubleValue: JSONValue = .double(3.14) + #expect(!doubleValue.isCompatible(with: .object)) + #expect(!doubleValue.isCompatible(with: .array)) + #expect(!doubleValue.isCompatible(with: .string)) + #expect(doubleValue.isCompatible(with: .number)) + #expect(!doubleValue.isCompatible(with: .integer)) + #expect(!doubleValue.isCompatible(with: .boolean)) + #expect(!doubleValue.isCompatible(with: .null)) + + // Test integer compatibility + let intValue: JSONValue = .int(42) + #expect(!intValue.isCompatible(with: .object)) + #expect(!intValue.isCompatible(with: .array)) + #expect(!intValue.isCompatible(with: .string)) + #expect(intValue.isCompatible(with: .number, strict: false)) + #expect(!intValue.isCompatible(with: .number, strict: true)) + #expect(intValue.isCompatible(with: .integer)) + #expect(!intValue.isCompatible(with: .boolean)) + #expect(!intValue.isCompatible(with: .null)) + + // Test boolean compatibility + let boolValue: JSONValue = .bool(true) + #expect(!boolValue.isCompatible(with: .object)) + #expect(!boolValue.isCompatible(with: .array)) + #expect(!boolValue.isCompatible(with: .string)) + #expect(!boolValue.isCompatible(with: .number)) + #expect(!boolValue.isCompatible(with: .integer)) + #expect(boolValue.isCompatible(with: .boolean)) + #expect(!boolValue.isCompatible(with: .null)) + + // Test null compatibility + let nullValue: JSONValue = .null + #expect(!nullValue.isCompatible(with: .object)) + #expect(!nullValue.isCompatible(with: .array)) + #expect(!nullValue.isCompatible(with: .string)) + #expect(!nullValue.isCompatible(with: .number)) + #expect(!nullValue.isCompatible(with: .integer)) + #expect(!nullValue.isCompatible(with: .boolean)) + #expect(nullValue.isCompatible(with: .null)) + + // Test composite schema types (should always return true) + let anyValue: JSONValue = .string("any") + #expect(anyValue.isCompatible(with: .reference("$ref"))) + #expect(anyValue.isCompatible(with: .anyOf([]))) + #expect(anyValue.isCompatible(with: .allOf([]))) + #expect(anyValue.isCompatible(with: .oneOf([]))) + #expect(anyValue.isCompatible(with: .not(.string))) + #expect(anyValue.isCompatible(with: .empty)) + #expect(anyValue.isCompatible(with: .any)) +}