diff --git a/Sources/OpenAPIKit/JSONReference.swift b/Sources/OpenAPIKit/JSONReference.swift index 94040b437..9d89efd83 100644 --- a/Sources/OpenAPIKit/JSONReference.swift +++ b/Sources/OpenAPIKit/JSONReference.swift @@ -385,6 +385,16 @@ extension OpenAPI { } } +public extension JSONReference { + /// Create an OpenAPI.Reference from the given JSONReference. + func openAPIReference(withDescription description: String? = nil) -> OpenAPI.Reference { + OpenAPI.Reference( + self, + description: description + ) + } +} + /// `SummaryOverridable` exists to provide a parent protocol to `OpenAPIDescribable` /// and `OpenAPISummarizable`. The structure is designed to provide default no-op /// implementations of both the members of this protocol to all types that implement either diff --git a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift index 3af51bf5e..1f847ffe6 100644 --- a/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift @@ -142,6 +142,36 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext { // See `JSONSchemaContext` public var deprecated: Bool { jsonSchema.deprecated } + + /// Returns a version of this `DereferencedJSONSchema` that has the given description. + public func with(description: String) -> DereferencedJSONSchema { + switch self { + case .null: + return .null + case .boolean(let context): + return .boolean(context.with(description: description)) + case .object(let coreContext, let objectContext): + return .object(coreContext.with(description: description), objectContext) + case .array(let coreContext, let arrayContext): + return .array(coreContext.with(description: description), arrayContext) + case .number(let coreContext, let numberContext): + return .number(coreContext.with(description: description), numberContext) + case .integer(let coreContext, let integerContext): + return .integer(coreContext.with(description: description), integerContext) + case .string(let coreContext, let stringContext): + return .string(coreContext.with(description: description), stringContext) + case .all(of: let schemas, core: let coreContext): + return .all(of: schemas, core: coreContext.with(description: description)) + case .one(of: let schemas, core: let coreContext): + return .one(of: schemas, core: coreContext.with(description: description)) + case .any(of: let schemas, core: let coreContext): + return .any(of: schemas, core: coreContext.with(description: description)) + case .not(let schema, core: let coreContext): + return .not(schema, core: coreContext.with(description: description)) + case .fragment(let context): + return .fragment(context.with(description: description)) + } + } } extension DereferencedJSONSchema { @@ -368,6 +398,10 @@ extension JSONSchema: LocallyDereferenceable { if !context.required { dereferenced = dereferenced.optionalSchemaObject() } + if let refDescription = context.description { + dereferenced = dereferenced.with(description: refDescription) + } + // TODO: consider which other core context properties to override here as with description ^ return dereferenced case .boolean(let context): return .boolean(context) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift index ced481b47..98a225363 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchema.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchema.swift @@ -68,7 +68,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { public static func not(_ schema: JSONSchema, core: CoreContext) -> Self { .init(schema: .not(schema, core: core)) } - public static func reference(_ reference: JSONReference, _ context: ReferenceContext) -> Self { + public static func reference(_ reference: JSONReference, _ context: CoreContext) -> Self { .init(schema: .reference(reference, context)) } /// Schemas without a `type`. @@ -90,7 +90,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { indirect case one(of: [JSONSchema], core: CoreContext) indirect case any(of: [JSONSchema], core: CoreContext) indirect case not(JSONSchema, core: CoreContext) - case reference(JSONReference, ReferenceContext) + case reference(JSONReference, CoreContext) /// Schemas without a `type`. case fragment(CoreContext) // This allows for the "{}" case and also fragments of schemas that will later be combined with `all(of:)`. } @@ -196,7 +196,9 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable { .any(of: _, core: let context as JSONSchemaContext), .not(_, core: let context as JSONSchemaContext): return context.description - case .reference, .null: + case .reference(_, let referenceContext): + return referenceContext.description + case .null: return nil } } @@ -375,7 +377,9 @@ extension JSONSchema { .any(of: _, core: let context as JSONSchemaContext), .not(_, core: let context as JSONSchemaContext): return context - case .reference, .null: + case .reference(_, let context as JSONSchemaContext): + return context + case .null: return nil } } @@ -433,15 +437,6 @@ extension JSONSchema { } return context } - - /// Get the context specific to a `reference` schema. If not a - /// reference schema, returns `nil`. - public var referenceContext: ReferenceContext? { - guard case .reference(_, let context) = value else { - return nil - } - return context - } } // MARK: - Transformations @@ -1053,7 +1048,13 @@ extension JSONSchema { schema: .fragment(fragment.with(description: description)), vendorExtensions: vendorExtensions ) - case .reference, .null: + case .reference(let ref, let referenceContext): + return .init( + warnings: warnings, + schema: .reference(ref, referenceContext.with(description: description)), + vendorExtensions: vendorExtensions + ) + case .null: return self } } @@ -1701,9 +1702,10 @@ extension JSONSchema { /// Construct a reference schema public static func reference( _ reference: JSONReference, - required: Bool = true + required: Bool = true, + description: String? = nil ) -> JSONSchema { - return .reference(reference, .init(required: required)) + return .reference(reference, .init(required: required, description: description)) } } @@ -1795,10 +1797,9 @@ extension JSONSchema: Encodable { try container.encode(node, forKey: .not) try core.encode(to: encoder) - case .reference(let reference, _): - var container = encoder.singleValueContainer() - - try container.encode(reference) + case .reference(let reference, let core): + try core.encode(to: encoder) + try reference.encode(to: encoder) case .fragment(let context): var container = encoder.singleValueContainer() @@ -1833,11 +1834,10 @@ extension JSONSchema: Decodable { public init(from decoder: Decoder) throws { - if let singleValueContainer = try? decoder.singleValueContainer() { - if let ref = try? singleValueContainer.decode(JSONReference.self) { - self = .reference(ref, required: true) - return - } + if let ref = try? JSONReference(from: decoder) { + let coreContext = try CoreContext(from: decoder) + self = .reference(ref, coreContext) + return } let container = try decoder.container(keyedBy: SubschemaCodingKeys.self) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 393469e86..0f2aa235e 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -634,7 +634,11 @@ extension OpenAPIKit30.JSONSchema: To31 { case .not(let not, core: let core): schema = .not(not.to31(), core: core.to31()) case .reference(let ref, let context): - schema = .reference(ref.to31(), context) + let coreContext: OpenAPIKit.JSONSchema.CoreContext + coreContext = .init( + required: context.required + ) + schema = .reference(ref.to31(), coreContext) case .fragment(let core): schema = .fragment(core.to31()) } diff --git a/Tests/OpenAPIKitTests/JSONReferenceTests.swift b/Tests/OpenAPIKitTests/JSONReferenceTests.swift index 45ab24285..53c7a8b64 100644 --- a/Tests/OpenAPIKitTests/JSONReferenceTests.swift +++ b/Tests/OpenAPIKitTests/JSONReferenceTests.swift @@ -128,6 +128,48 @@ final class JSONReferenceTests: XCTestCase { XCTAssertEqual(JSONReference.component(named: "hello").absoluteString, "#/components/callbacks/hello") XCTAssertEqual(JSONReference.component(named: "hello").absoluteString, "#/components/pathItems/hello") } + + func test_toOpenAPIReference() { + let t1 = JSONReference.component(named: "hello") + let t2 = JSONReference.component(named: "hello") + let t3 = JSONReference.component(named: "hello") + let t4 = JSONReference.component(named: "hello") + let t5 = JSONReference.component(named: "hello") + let t6 = JSONReference.component(named: "hello") + let t7 = JSONReference.component(named: "hello") + let t8 = JSONReference.component(named: "hello") + let t9 = JSONReference.component(named: "hello") + + XCTAssertEqual(t1.openAPIReference().jsonReference, t1) + XCTAssertEqual(t2.openAPIReference().jsonReference, t2) + XCTAssertEqual(t3.openAPIReference().jsonReference, t3) + XCTAssertEqual(t4.openAPIReference().jsonReference, t4) + XCTAssertEqual(t5.openAPIReference().jsonReference, t5) + XCTAssertEqual(t6.openAPIReference().jsonReference, t6) + XCTAssertEqual(t7.openAPIReference().jsonReference, t7) + XCTAssertEqual(t8.openAPIReference().jsonReference, t8) + XCTAssertEqual(t9.openAPIReference().jsonReference, t9) + + XCTAssertNil(t1.openAPIReference().description) + XCTAssertNil(t2.openAPIReference().description) + XCTAssertNil(t3.openAPIReference().description) + XCTAssertNil(t4.openAPIReference().description) + XCTAssertNil(t5.openAPIReference().description) + XCTAssertNil(t6.openAPIReference().description) + XCTAssertNil(t7.openAPIReference().description) + XCTAssertNil(t8.openAPIReference().description) + XCTAssertNil(t9.openAPIReference().description) + + XCTAssertEqual(t1.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t2.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t3.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t4.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t5.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t6.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t7.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t8.openAPIReference(withDescription: "hi").description, "hi") + XCTAssertEqual(t9.openAPIReference(withDescription: "hi").description, "hi") + } } // MARK: Codable diff --git a/Tests/OpenAPIKitTests/Schema Object/DereferencedSchemaObjectTests.swift b/Tests/OpenAPIKitTests/Schema Object/DereferencedSchemaObjectTests.swift index 8234d7e62..899a61d68 100644 --- a/Tests/OpenAPIKitTests/Schema Object/DereferencedSchemaObjectTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/DereferencedSchemaObjectTests.swift @@ -281,6 +281,14 @@ final class DereferencedSchemaObjectTests: XCTestCase { XCTAssertEqual(t1, .string(.init(), .init())) } + func test_throwingReferenceWithOverriddenDescription() throws { + let components = OpenAPI.Components( + schemas: ["test": .string] + ) + let t1 = try JSONSchema.reference(.component(named: "test"), description: "hello").dereferenced(in: components) + XCTAssertEqual(t1, .string(.init(description: "hello"), .init())) + } + func test_optionalObjectWithoutReferences() { let t1 = JSONSchema.object(properties: ["test": .string]).dereferenced() XCTAssertEqual( @@ -504,4 +512,37 @@ final class DereferencedSchemaObjectTests: XCTestCase { ) } } + + func test_withDescription() throws { + let null = JSONSchema.null.dereferenced()!.with(description: "test") + let object = JSONSchema.object.dereferenced()!.with(description: "test") + let array = JSONSchema.array.dereferenced()!.with(description: "test") + + let boolean = JSONSchema.boolean.dereferenced()!.with(description: "test") + let number = JSONSchema.number.dereferenced()!.with(description: "test") + let integer = JSONSchema.integer.dereferenced()!.with(description: "test") + let string = JSONSchema.string.dereferenced()!.with(description: "test") + let fragment = JSONSchema.fragment(.init()).dereferenced()!.with(description: "test") + let all = JSONSchema.all(of: .string).dereferenced()!.with(description: "test") + let one = JSONSchema.one(of: .string).dereferenced()!.with(description: "test") + let any = JSONSchema.any(of: .string).dereferenced()!.with(description: "test") + let not = JSONSchema.not(.string).dereferenced()!.with(description: "test") + + XCTAssertEqual(object.description, "test") + XCTAssertEqual(array.description, "test") + + XCTAssertEqual(boolean.description, "test") + XCTAssertEqual(number.description, "test") + XCTAssertEqual(integer.description, "test") + XCTAssertEqual(string.description, "test") + XCTAssertEqual(fragment.description, "test") + + XCTAssertEqual(all.description, "test") + XCTAssertEqual(one.description, "test") + XCTAssertEqual(any.description, "test") + XCTAssertEqual(not.description, "test") + + XCTAssertNil(null.description) + } + } diff --git a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift index 3848c9889..8218c3c2b 100644 --- a/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift +++ b/Tests/OpenAPIKitTests/Schema Object/JSONSchemaTests.swift @@ -643,7 +643,7 @@ final class SchemaObjectTests: XCTestCase { let anyOf = JSONSchema.any(of: [boolean], core: .init(description: "hello")) let oneOf = JSONSchema.one(of: [boolean], core: .init(description: "hello")) let not = JSONSchema.not(boolean, core: .init(description: "hello")) - let reference = JSONSchema.reference(.external(URL(string: "hello/world.json#/hello")!)) + let reference = JSONSchema.reference(.external(URL(string: "hello/world.json#/hello")!), description: "hello") let fragment = JSONSchema.fragment(.init(description: nil)) let fragmentWithDescription = JSONSchema.fragment(.init(description: "hello")) @@ -659,8 +659,8 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(anyOf.description, "hello") XCTAssertEqual(oneOf.description, "hello") XCTAssertEqual(not.description, "hello") + XCTAssertEqual(reference.description, "hello") - XCTAssertNil(reference.description) XCTAssertNil(fragment.description) XCTAssertNil(null.description) } @@ -761,8 +761,8 @@ final class SchemaObjectTests: XCTestCase { XCTAssertEqual(anyOf.coreContext as? JSONSchema.CoreContext, .init()) XCTAssertEqual(oneOf.coreContext as? JSONSchema.CoreContext, .init()) XCTAssertEqual(not.coreContext as? JSONSchema.CoreContext, .init()) + XCTAssertEqual(reference.coreContext as? JSONSchema.CoreContext, .init()) - XCTAssertNil(reference.coreContext) XCTAssertNil(null.coreContext) } @@ -1348,6 +1348,40 @@ final class SchemaObjectTests: XCTestCase { XCTAssertNil(reference.discriminator) } + func test_withDescription() throws { + let null = JSONSchema.null.with(description: "test") + let object = JSONSchema.object.with(description: "test") + let array = JSONSchema.array.with(description: "test") + + let boolean = JSONSchema.boolean.with(description: "test") + let number = JSONSchema.number.with(description: "test") + let integer = JSONSchema.integer.with(description: "test") + let string = JSONSchema.string.with(description: "test") + let fragment = JSONSchema.fragment(.init()).with(description: "test") + let all = JSONSchema.all(of: .string).with(description: "test") + let one = JSONSchema.one(of: .string).with(description: "test") + let any = JSONSchema.any(of: .string).with(description: "test") + let not = JSONSchema.not(.string).with(description: "test") + let reference = JSONSchema.reference(.component(named: "test")).with(description: "test") + + XCTAssertEqual(object.description, "test") + XCTAssertEqual(array.description, "test") + + XCTAssertEqual(boolean.description, "test") + XCTAssertEqual(number.description, "test") + XCTAssertEqual(integer.description, "test") + XCTAssertEqual(string.description, "test") + XCTAssertEqual(fragment.description, "test") + + XCTAssertEqual(all.description, "test") + XCTAssertEqual(one.description, "test") + XCTAssertEqual(any.description, "test") + XCTAssertEqual(not.description, "test") + XCTAssertEqual(reference.description, "test") + + XCTAssertNil(null.description) + } + func test_minObjectProperties() { let obj1 = JSONSchema.ObjectContext( properties: [:], @@ -5734,6 +5768,89 @@ extension SchemaObjectTests { ) } + func test_encodeReferenceDescription() { + let nodeRef = JSONSchema.reference(.component(named: "requiredBool"), description: "hello") + + testEncodingPropertyLines(entity: nodeRef, propertyLines: [ + "\"$ref\" : \"#\\/components\\/schemas\\/requiredBool\",", + "\"description\" : \"hello\"" + ]) + } + + func test_decodeReferenceDescription() throws { + let nodeRefData = ##"{ "$ref": "#/components/schemas/requiredBool", "description": "hello" }"##.data(using: .utf8)! + + let nodeRef = try orderUnstableDecode(JSONSchema.self, from: nodeRefData) + + XCTAssertEqual( + nodeRef, + JSONSchema.reference(.component(named: "requiredBool"), description: "hello") + ) + } + + func test_encodeReferenceDeprecated() { + let nodeRef = JSONSchema.reference(.component(named: "requiredBool"), .init(deprecated: true)) + + testEncodingPropertyLines(entity: nodeRef, propertyLines: [ + "\"$ref\" : \"#\\/components\\/schemas\\/requiredBool\",", + "\"deprecated\" : true" + ]) + } + + func test_decodeReferenceDeprecated() throws { + let nodeRefData = ##"{ "$ref": "#/components/schemas/requiredBool", "deprecated": true }"##.data(using: .utf8)! + + let nodeRef = try orderUnstableDecode(JSONSchema.self, from: nodeRefData) + + XCTAssertEqual( + nodeRef, + JSONSchema.reference(.component(named: "requiredBool"), .init(deprecated: true)) + ) + } + + func test_encodeReferenceDefault() { + let nodeRef = JSONSchema.reference(.component(named: "requiredBool"), .init(defaultValue: "hello")) + + testEncodingPropertyLines(entity: nodeRef, propertyLines: [ + "\"$ref\" : \"#\\/components\\/schemas\\/requiredBool\",", + "\"default\" : \"hello\"" + ]) + } + + func test_decodeReferenceDefault() throws { + let nodeRefData = ##"{ "$ref": "#/components/schemas/requiredBool", "default": "hello" }"##.data(using: .utf8)! + + let nodeRef = try orderUnstableDecode(JSONSchema.self, from: nodeRefData) + + XCTAssertEqual( + nodeRef, + JSONSchema.reference(.component(named: "requiredBool"), .init(defaultValue: "hello")) + ) + } + + + func test_encodeReferenceExamples() { + let nodeRef = JSONSchema.reference(.component(named: "requiredBool"), .init(examples: ["hello"])) + + testEncodingPropertyLines(entity: nodeRef, propertyLines: [ + "\"$ref\" : \"#\\/components\\/schemas\\/requiredBool\",", + "\"examples\" : [", + " \"hello\"", + "]", + ]) + } + + func test_decodeReferenceExamples() throws { + let nodeRefData = ##"{ "$ref": "#/components/schemas/requiredBool", "examples": ["hello"] }"##.data(using: .utf8)! + + let nodeRef = try orderUnstableDecode(JSONSchema.self, from: nodeRefData) + + XCTAssertEqual( + nodeRef, + JSONSchema.reference(.component(named: "requiredBool"), .init(examples: ["hello"])) + ) + } + func test_encodeReferenceOptionality() { let optionalReference = JSONSchema.reference(.component(named: "optionalBool")) .optionalSchemaObject()