diff --git a/Sources/JSONSchema/JSONSchema.swift b/Sources/JSONSchema/JSONSchema.swift index 84738e8..b0183c4 100644 --- a/Sources/JSONSchema/JSONSchema.swift +++ b/Sources/JSONSchema/JSONSchema.swift @@ -250,6 +250,155 @@ case any } +extension JSONSchema { + /// The title of the schema, if present. + public var title: String? { + switch self { + case .object(let title, _, _, _, _, _, _, _, _), + .array(let title, _, _, _, _, _, _, _, _, _), + .string(let title, _, _, _, _, _, _, _, _, _), + .number(let title, _, _, _, _, _, _, _, _, _, _), + .integer(let title, _, _, _, _, _, _, _, _, _, _), + .boolean(let title, _, _): + return title + case .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The description of the schema, if present. + public var description: String? { + switch self { + case .object(_, let description, _, _, _, _, _, _, _), + .array(_, let description, _, _, _, _, _, _, _, _), + .string(_, let description, _, _, _, _, _, _, _, _), + .number(_, let description, _, _, _, _, _, _, _, _, _), + .integer(_, let description, _, _, _, _, _, _, _, _, _), + .boolean(_, let description, _): + return description + case .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The default value of the schema, if present. + public var `default`: JSONValue? { + switch self { + case .object(_, _, let `default`, _, _, _, _, _, _), + .array(_, _, let `default`, _, _, _, _, _, _, _), + .string(_, _, let `default`, _, _, _, _, _, _, _), + .number(_, _, let `default`, _, _, _, _, _, _, _, _), + .integer(_, _, let `default`, _, _, _, _, _, _, _, _), + .boolean(_, _, let `default`): + return `default` + case .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The examples of the schema, if present. + public var examples: [JSONValue]? { + switch self { + case .object(_, _, _, let examples, _, _, _, _, _), + .array(_, _, _, let examples, _, _, _, _, _, _), + .string(_, _, _, let examples, _, _, _, _, _, _), + .number(_, _, _, let examples, _, _, _, _, _, _, _), + .integer(_, _, _, let examples, _, _, _, _, _, _, _): + return examples + case .boolean, .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The enum values of the schema, if present. + public var `enum`: [JSONValue]? { + switch self { + case .object(_, _, _, _, let `enum`, _, _, _, _), + .array(_, _, _, _, let `enum`, _, _, _, _, _), + .string(_, _, _, _, let `enum`, _, _, _, _, _), + .number(_, _, _, _, let `enum`, _, _, _, _, _, _), + .integer(_, _, _, _, let `enum`, _, _, _, _, _, _): + return `enum` + case .boolean, .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The const value of the schema, if present. + public var const: JSONValue? { + switch self { + case .object(_, _, _, _, _, let const, _, _, _), + .array(_, _, _, _, _, let const, _, _, _, _), + .string(_, _, _, _, _, let const, _, _, _, _), + .number(_, _, _, _, _, let const, _, _, _, _, _), + .integer(_, _, _, _, _, let const, _, _, _, _, _): + return const + case .boolean, .null, .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } + + /// The name of the schema type. + public var typeName: String { + switch self { + case .object: return "Object" + case .array: return "Array" + case .string: return "String" + case .number: return "Number" + case .integer: return "Integer" + case .boolean: return "Boolean" + case .null: return "Null" + case .reference: return "Reference" + case .anyOf: return "AnyOf" + case .allOf: return "AllOf" + case .oneOf: return "OneOf" + case .not: return "Not" + case .empty: return "Empty" + case .any: return "Any" + @unknown default: + return "Unknown" + } + } + + /// The default value that the schema infers from its type. + /// + /// - For objects, this property returns an empty object (`{}`). + /// - For arrays, this property returns an empty array (`[]`). + /// - For strings, this property returns an empty string (`""`). + /// - For numbers, this property returns `0.0`. + /// - For integers, this property returns `0`. + /// - For booleans, this property returns `false`. + /// - For null values, this property returns `null`. + /// - For references and composite schemas (anyOf, allOf, oneOf, not, empty, or any), this property returns `nil`. + public var typeDefault: JSONValue? { + switch self { + case .object: return .object([:]) + case .array: return .array([]) + case .string: return .string("") + case .number: return .double(0.0) + case .integer: return .int(0) + case .boolean: return .bool(false) + case .null: return .null + case .reference, .anyOf, .allOf, .oneOf, .not, .empty, .any: + return nil + @unknown default: + return nil + } + } +} + extension JSONSchema: Codable { private enum CodingKeys: String, CodingKey { // Any diff --git a/Tests/JSONSchemaTests/JSONSchemaTests.swift b/Tests/JSONSchemaTests/JSONSchemaTests.swift index b000321..4604582 100644 --- a/Tests/JSONSchemaTests/JSONSchemaTests.swift +++ b/Tests/JSONSchemaTests/JSONSchemaTests.swift @@ -544,10 +544,10 @@ import Testing if case let .string(_, _, _, _, _, _, minLength, _, _, _) = additionalPropsSchema { #expect(minLength == 1) } else { - Issue.record("additionalProperties schema should be a string schema") + Issue.record("Expected string schema") } } else { - Issue.record("additionalProperties should be .schema") + Issue.record("Expected schema variant") } } else { Issue.record("Decoded schema should be an object with additionalProperties") @@ -615,9 +615,8 @@ import Testing #expect(decodedVariant == schemaVariant) if case let .schema(decodedSchema) = decodedVariant { - if case let .string(_, _, _, _, _, _, minLength, maxLength, _, _) = decodedSchema { + if case let .string(_, _, _, _, _, _, minLength, _, _, _) = decodedSchema { #expect(minLength == 1) - #expect(maxLength == 100) } else { Issue.record("Expected string schema") } @@ -843,3 +842,301 @@ import Testing #expect(decodedValue == objectValue) } + +@Test func testConvenienceProperties() { + // Test object schema + let objectSchema: JSONSchema = .object( + title: "Person", + description: "A person object", + default: ["name": "John"], + examples: [["name": "Jane"]], + enum: [["name": "Option 1"]], + const: ["name": "Constant"] + ) + + #expect(objectSchema.title == "Person") + #expect(objectSchema.description == "A person object") + #expect(objectSchema.default?.objectValue?["name"]?.stringValue == "John") + #expect(objectSchema.examples?.count == 1) + #expect(objectSchema.examples?.first?.objectValue?["name"]?.stringValue == "Jane") + #expect(objectSchema.enum?.count == 1) + #expect(objectSchema.enum?.first?.objectValue?["name"]?.stringValue == "Option 1") + #expect(objectSchema.const?.objectValue?["name"]?.stringValue == "Constant") + + // Test array schema + let arraySchema: JSONSchema = .array( + title: "Numbers", + description: "An array of numbers", + default: [1, 2, 3], + examples: [[4, 5, 6]], + enum: [[7, 8, 9]], + const: [10, 11, 12] + ) + + #expect(arraySchema.title == "Numbers") + #expect(arraySchema.description == "An array of numbers") + #expect(arraySchema.default?.arrayValue?.count == 3) + #expect(arraySchema.examples?.count == 1) + #expect(arraySchema.examples?.first?.arrayValue?.count == 3) + #expect(arraySchema.enum?.count == 1) + #expect(arraySchema.enum?.first?.arrayValue?.count == 3) + #expect(arraySchema.const?.arrayValue?.count == 3) + + // Test string schema + let stringSchema: JSONSchema = .string( + title: "Email", + description: "An email address", + default: "john@example.com", + examples: ["jane@example.com"], + enum: ["admin@example.com"], + const: "constant@example.com" + ) + + #expect(stringSchema.title == "Email") + #expect(stringSchema.description == "An email address") + #expect(stringSchema.default?.stringValue == "john@example.com") + #expect(stringSchema.examples?.count == 1) + #expect(stringSchema.examples?.first?.stringValue == "jane@example.com") + #expect(stringSchema.enum?.count == 1) + #expect(stringSchema.enum?.first?.stringValue == "admin@example.com") + #expect(stringSchema.const?.stringValue == "constant@example.com") + + // Test number schema + let numberSchema: JSONSchema = .number( + title: "Temperature", + description: "Temperature in Celsius", + default: 20.5, + examples: [18.0], + enum: [0.0], + const: 37.0 + ) + + #expect(numberSchema.title == "Temperature") + #expect(numberSchema.description == "Temperature in Celsius") + #expect(numberSchema.default?.doubleValue == 20.5) + #expect(numberSchema.examples?.count == 1) + #expect(numberSchema.examples?.first?.doubleValue == 18.0) + #expect(numberSchema.enum?.count == 1) + #expect(numberSchema.enum?.first?.doubleValue == 0.0) + #expect(numberSchema.const?.doubleValue == 37.0) + + // Test integer schema + let integerSchema: JSONSchema = .integer( + title: "Age", + description: "Age in years", + default: 30, + examples: [25], + enum: [18], + const: 42 + ) + + #expect(integerSchema.title == "Age") + #expect(integerSchema.description == "Age in years") + #expect(integerSchema.default?.intValue == 30) + #expect(integerSchema.examples?.count == 1) + #expect(integerSchema.examples?.first?.intValue == 25) + #expect(integerSchema.enum?.count == 1) + #expect(integerSchema.enum?.first?.intValue == 18) + #expect(integerSchema.const?.intValue == 42) + + // Test boolean schema + let booleanSchema: JSONSchema = .boolean( + title: "Active", + description: "Whether the user is active", + default: true + ) + + #expect(booleanSchema.title == "Active") + #expect(booleanSchema.description == "Whether the user is active") + #expect(booleanSchema.default?.boolValue == true) + + // Test schemas without metadata + let emptyObjectSchema: JSONSchema = .object() + #expect(emptyObjectSchema.title == nil) + #expect(emptyObjectSchema.description == nil) + #expect(emptyObjectSchema.default == nil) + #expect(emptyObjectSchema.examples == nil) + #expect(emptyObjectSchema.enum == nil) + #expect(emptyObjectSchema.const == nil) + + // Test special schemas + let nullSchema: JSONSchema = .null + #expect(nullSchema.title == nil) + #expect(nullSchema.description == nil) + #expect(nullSchema.default == nil) + #expect(nullSchema.examples == nil) + #expect(nullSchema.enum == nil) + #expect(nullSchema.const == nil) + + let anySchema: JSONSchema = .any + #expect(anySchema.title == nil) + #expect(anySchema.description == nil) + #expect(anySchema.default == nil) + #expect(anySchema.examples == nil) + #expect(anySchema.enum == nil) + #expect(anySchema.const == nil) + + let emptySchema: JSONSchema = .empty + #expect(emptySchema.title == nil) + #expect(emptySchema.description == nil) + #expect(emptySchema.default == nil) + #expect(emptySchema.examples == nil) + #expect(emptySchema.enum == nil) + #expect(emptySchema.const == nil) + + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.title == nil) + #expect(referenceSchema.description == nil) + #expect(referenceSchema.default == nil) + #expect(referenceSchema.examples == nil) + #expect(referenceSchema.enum == nil) + #expect(referenceSchema.const == nil) + + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.title == nil) + #expect(anyOfSchema.description == nil) + #expect(anyOfSchema.default == nil) + #expect(anyOfSchema.examples == nil) + #expect(anyOfSchema.enum == nil) + #expect(anyOfSchema.const == nil) + + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.title == nil) + #expect(allOfSchema.description == nil) + #expect(allOfSchema.default == nil) + #expect(allOfSchema.examples == nil) + #expect(allOfSchema.enum == nil) + #expect(allOfSchema.const == nil) + + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.title == nil) + #expect(oneOfSchema.description == nil) + #expect(oneOfSchema.default == nil) + #expect(oneOfSchema.examples == nil) + #expect(oneOfSchema.enum == nil) + #expect(oneOfSchema.const == nil) + + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.title == nil) + #expect(notSchema.description == nil) + #expect(notSchema.default == nil) + #expect(notSchema.examples == nil) + #expect(notSchema.enum == nil) + #expect(notSchema.const == nil) +} + +@Test func testTypeName() { + // Test object schema + let objectSchema: JSONSchema = .object() + #expect(objectSchema.typeName == "Object") + + // Test array schema + let arraySchema: JSONSchema = .array() + #expect(arraySchema.typeName == "Array") + + // Test string schema + let stringSchema: JSONSchema = .string() + #expect(stringSchema.typeName == "String") + + // Test number schema + let numberSchema: JSONSchema = .number() + #expect(numberSchema.typeName == "Number") + + // Test integer schema + let integerSchema: JSONSchema = .integer() + #expect(integerSchema.typeName == "Integer") + + // Test boolean schema + let booleanSchema: JSONSchema = .boolean() + #expect(booleanSchema.typeName == "Boolean") + + // Test null schema + let nullSchema: JSONSchema = .null + #expect(nullSchema.typeName == "Null") + + // Test reference schema + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.typeName == "Reference") + + // Test anyOf schema + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.typeName == "AnyOf") + + // Test allOf schema + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.typeName == "AllOf") + + // Test oneOf schema + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.typeName == "OneOf") + + // Test not schema + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.typeName == "Not") + + // Test empty schema + let emptySchema: JSONSchema = .empty + #expect(emptySchema.typeName == "Empty") + + // Test any schema + let anySchema: JSONSchema = .any + #expect(anySchema.typeName == "Any") +} + +@Test func testTypeDefault() { + // Test object schema + let objectSchema: JSONSchema = .object() + #expect(objectSchema.typeDefault?.objectValue?.isEmpty == true) + + // Test array schema + let arraySchema: JSONSchema = .array() + #expect(arraySchema.typeDefault?.arrayValue?.isEmpty == true) + + // Test string schema + let stringSchema: JSONSchema = .string() + #expect(stringSchema.typeDefault?.stringValue == "") + + // Test number schema + let numberSchema: JSONSchema = .number() + #expect(numberSchema.typeDefault?.doubleValue == 0.0) + + // Test integer schema + let integerSchema: JSONSchema = .integer() + #expect(integerSchema.typeDefault?.intValue == 0) + + // Test boolean schema + let booleanSchema: JSONSchema = .boolean() + #expect(booleanSchema.typeDefault?.boolValue == false) + + // Test null schema + let nullSchema: JSONSchema = .null + #expect(nullSchema.typeDefault?.isNull == true) + + // Test reference schema + let referenceSchema: JSONSchema = .reference("#/definitions/Person") + #expect(referenceSchema.typeDefault == nil) + + // Test anyOf schema + let anyOfSchema: JSONSchema = .anyOf([.string(), .integer()]) + #expect(anyOfSchema.typeDefault == nil) + + // Test allOf schema + let allOfSchema: JSONSchema = .allOf([.string(), .integer()]) + #expect(allOfSchema.typeDefault == nil) + + // Test oneOf schema + let oneOfSchema: JSONSchema = .oneOf([.string(), .integer()]) + #expect(oneOfSchema.typeDefault == nil) + + // Test not schema + let notSchema: JSONSchema = .not(.string()) + #expect(notSchema.typeDefault == nil) + + // Test empty schema + let emptySchema: JSONSchema = .empty + #expect(emptySchema.typeDefault == nil) + + // Test any schema + let anySchema: JSONSchema = .any + #expect(anySchema.typeDefault == nil) +}