Skip to content

Commit 97618fa

Browse files
authored
Merge pull request #299 from mattpolzin/support-schema-ref-descriptions
Support schema reference description overrides
2 parents 1ade856 + f0967ab commit 97618fa

File tree

7 files changed

+277
-29
lines changed

7 files changed

+277
-29
lines changed

Sources/OpenAPIKit/JSONReference.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -385,6 +385,16 @@ extension OpenAPI {
385385
}
386386
}
387387

388+
public extension JSONReference {
389+
/// Create an OpenAPI.Reference from the given JSONReference.
390+
func openAPIReference(withDescription description: String? = nil) -> OpenAPI.Reference<ReferenceType> {
391+
OpenAPI.Reference(
392+
self,
393+
description: description
394+
)
395+
}
396+
}
397+
388398
/// `SummaryOverridable` exists to provide a parent protocol to `OpenAPIDescribable`
389399
/// and `OpenAPISummarizable`. The structure is designed to provide default no-op
390400
/// implementations of both the members of this protocol to all types that implement either

Sources/OpenAPIKit/Schema Object/DereferencedJSONSchema.swift

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,36 @@ public enum DereferencedJSONSchema: Equatable, JSONSchemaContext {
142142

143143
// See `JSONSchemaContext`
144144
public var deprecated: Bool { jsonSchema.deprecated }
145+
146+
/// Returns a version of this `DereferencedJSONSchema` that has the given description.
147+
public func with(description: String) -> DereferencedJSONSchema {
148+
switch self {
149+
case .null:
150+
return .null
151+
case .boolean(let context):
152+
return .boolean(context.with(description: description))
153+
case .object(let coreContext, let objectContext):
154+
return .object(coreContext.with(description: description), objectContext)
155+
case .array(let coreContext, let arrayContext):
156+
return .array(coreContext.with(description: description), arrayContext)
157+
case .number(let coreContext, let numberContext):
158+
return .number(coreContext.with(description: description), numberContext)
159+
case .integer(let coreContext, let integerContext):
160+
return .integer(coreContext.with(description: description), integerContext)
161+
case .string(let coreContext, let stringContext):
162+
return .string(coreContext.with(description: description), stringContext)
163+
case .all(of: let schemas, core: let coreContext):
164+
return .all(of: schemas, core: coreContext.with(description: description))
165+
case .one(of: let schemas, core: let coreContext):
166+
return .one(of: schemas, core: coreContext.with(description: description))
167+
case .any(of: let schemas, core: let coreContext):
168+
return .any(of: schemas, core: coreContext.with(description: description))
169+
case .not(let schema, core: let coreContext):
170+
return .not(schema, core: coreContext.with(description: description))
171+
case .fragment(let context):
172+
return .fragment(context.with(description: description))
173+
}
174+
}
145175
}
146176

147177
extension DereferencedJSONSchema {
@@ -368,6 +398,10 @@ extension JSONSchema: LocallyDereferenceable {
368398
if !context.required {
369399
dereferenced = dereferenced.optionalSchemaObject()
370400
}
401+
if let refDescription = context.description {
402+
dereferenced = dereferenced.with(description: refDescription)
403+
}
404+
// TODO: consider which other core context properties to override here as with description ^
371405
return dereferenced
372406
case .boolean(let context):
373407
return .boolean(context)

Sources/OpenAPIKit/Schema Object/JSONSchema.swift

Lines changed: 25 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
6868
public static func not(_ schema: JSONSchema, core: CoreContext<JSONTypeFormat.AnyFormat>) -> Self {
6969
.init(schema: .not(schema, core: core))
7070
}
71-
public static func reference(_ reference: JSONReference<JSONSchema>, _ context: ReferenceContext) -> Self {
71+
public static func reference(_ reference: JSONReference<JSONSchema>, _ context: CoreContext<JSONTypeFormat.AnyFormat>) -> Self {
7272
.init(schema: .reference(reference, context))
7373
}
7474
/// Schemas without a `type`.
@@ -90,7 +90,7 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
9090
indirect case one(of: [JSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
9191
indirect case any(of: [JSONSchema], core: CoreContext<JSONTypeFormat.AnyFormat>)
9292
indirect case not(JSONSchema, core: CoreContext<JSONTypeFormat.AnyFormat>)
93-
case reference(JSONReference<JSONSchema>, ReferenceContext)
93+
case reference(JSONReference<JSONSchema>, CoreContext<JSONTypeFormat.AnyFormat>)
9494
/// Schemas without a `type`.
9595
case fragment(CoreContext<JSONTypeFormat.AnyFormat>) // This allows for the "{}" case and also fragments of schemas that will later be combined with `all(of:)`.
9696
}
@@ -196,7 +196,9 @@ public struct JSONSchema: JSONSchemaContext, HasWarnings, VendorExtendable {
196196
.any(of: _, core: let context as JSONSchemaContext),
197197
.not(_, core: let context as JSONSchemaContext):
198198
return context.description
199-
case .reference, .null:
199+
case .reference(_, let referenceContext):
200+
return referenceContext.description
201+
case .null:
200202
return nil
201203
}
202204
}
@@ -375,7 +377,9 @@ extension JSONSchema {
375377
.any(of: _, core: let context as JSONSchemaContext),
376378
.not(_, core: let context as JSONSchemaContext):
377379
return context
378-
case .reference, .null:
380+
case .reference(_, let context as JSONSchemaContext):
381+
return context
382+
case .null:
379383
return nil
380384
}
381385
}
@@ -433,15 +437,6 @@ extension JSONSchema {
433437
}
434438
return context
435439
}
436-
437-
/// Get the context specific to a `reference` schema. If not a
438-
/// reference schema, returns `nil`.
439-
public var referenceContext: ReferenceContext? {
440-
guard case .reference(_, let context) = value else {
441-
return nil
442-
}
443-
return context
444-
}
445440
}
446441

447442
// MARK: - Transformations
@@ -1053,7 +1048,13 @@ extension JSONSchema {
10531048
schema: .fragment(fragment.with(description: description)),
10541049
vendorExtensions: vendorExtensions
10551050
)
1056-
case .reference, .null:
1051+
case .reference(let ref, let referenceContext):
1052+
return .init(
1053+
warnings: warnings,
1054+
schema: .reference(ref, referenceContext.with(description: description)),
1055+
vendorExtensions: vendorExtensions
1056+
)
1057+
case .null:
10571058
return self
10581059
}
10591060
}
@@ -1701,9 +1702,10 @@ extension JSONSchema {
17011702
/// Construct a reference schema
17021703
public static func reference(
17031704
_ reference: JSONReference<JSONSchema>,
1704-
required: Bool = true
1705+
required: Bool = true,
1706+
description: String? = nil
17051707
) -> JSONSchema {
1706-
return .reference(reference, .init(required: required))
1708+
return .reference(reference, .init(required: required, description: description))
17071709
}
17081710
}
17091711

@@ -1795,10 +1797,9 @@ extension JSONSchema: Encodable {
17951797
try container.encode(node, forKey: .not)
17961798
try core.encode(to: encoder)
17971799

1798-
case .reference(let reference, _):
1799-
var container = encoder.singleValueContainer()
1800-
1801-
try container.encode(reference)
1800+
case .reference(let reference, let core):
1801+
try core.encode(to: encoder)
1802+
try reference.encode(to: encoder)
18021803

18031804
case .fragment(let context):
18041805
var container = encoder.singleValueContainer()
@@ -1833,11 +1834,10 @@ extension JSONSchema: Decodable {
18331834

18341835
public init(from decoder: Decoder) throws {
18351836

1836-
if let singleValueContainer = try? decoder.singleValueContainer() {
1837-
if let ref = try? singleValueContainer.decode(JSONReference<JSONSchema>.self) {
1838-
self = .reference(ref, required: true)
1839-
return
1840-
}
1837+
if let ref = try? JSONReference<JSONSchema>(from: decoder) {
1838+
let coreContext = try CoreContext<JSONTypeFormat.AnyFormat>(from: decoder)
1839+
self = .reference(ref, coreContext)
1840+
return
18411841
}
18421842

18431843
let container = try decoder.container(keyedBy: SubschemaCodingKeys.self)

Sources/OpenAPIKitCompat/Compat30To31.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -634,7 +634,11 @@ extension OpenAPIKit30.JSONSchema: To31 {
634634
case .not(let not, core: let core):
635635
schema = .not(not.to31(), core: core.to31())
636636
case .reference(let ref, let context):
637-
schema = .reference(ref.to31(), context)
637+
let coreContext: OpenAPIKit.JSONSchema.CoreContext<OpenAPIKit.JSONTypeFormat.AnyFormat>
638+
coreContext = .init(
639+
required: context.required
640+
)
641+
schema = .reference(ref.to31(), coreContext)
638642
case .fragment(let core):
639643
schema = .fragment(core.to31())
640644
}

Tests/OpenAPIKitTests/JSONReferenceTests.swift

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,6 +128,48 @@ final class JSONReferenceTests: XCTestCase {
128128
XCTAssertEqual(JSONReference<OpenAPI.Callbacks>.component(named: "hello").absoluteString, "#/components/callbacks/hello")
129129
XCTAssertEqual(JSONReference<OpenAPI.PathItem>.component(named: "hello").absoluteString, "#/components/pathItems/hello")
130130
}
131+
132+
func test_toOpenAPIReference() {
133+
let t1 = JSONReference<JSONSchema>.component(named: "hello")
134+
let t2 = JSONReference<OpenAPI.Response>.component(named: "hello")
135+
let t3 = JSONReference<OpenAPI.Parameter>.component(named: "hello")
136+
let t4 = JSONReference<OpenAPI.Example>.component(named: "hello")
137+
let t5 = JSONReference<OpenAPI.Request>.component(named: "hello")
138+
let t6 = JSONReference<OpenAPI.Header>.component(named: "hello")
139+
let t7 = JSONReference<OpenAPI.SecurityScheme>.component(named: "hello")
140+
let t8 = JSONReference<OpenAPI.Callbacks>.component(named: "hello")
141+
let t9 = JSONReference<OpenAPI.PathItem>.component(named: "hello")
142+
143+
XCTAssertEqual(t1.openAPIReference().jsonReference, t1)
144+
XCTAssertEqual(t2.openAPIReference().jsonReference, t2)
145+
XCTAssertEqual(t3.openAPIReference().jsonReference, t3)
146+
XCTAssertEqual(t4.openAPIReference().jsonReference, t4)
147+
XCTAssertEqual(t5.openAPIReference().jsonReference, t5)
148+
XCTAssertEqual(t6.openAPIReference().jsonReference, t6)
149+
XCTAssertEqual(t7.openAPIReference().jsonReference, t7)
150+
XCTAssertEqual(t8.openAPIReference().jsonReference, t8)
151+
XCTAssertEqual(t9.openAPIReference().jsonReference, t9)
152+
153+
XCTAssertNil(t1.openAPIReference().description)
154+
XCTAssertNil(t2.openAPIReference().description)
155+
XCTAssertNil(t3.openAPIReference().description)
156+
XCTAssertNil(t4.openAPIReference().description)
157+
XCTAssertNil(t5.openAPIReference().description)
158+
XCTAssertNil(t6.openAPIReference().description)
159+
XCTAssertNil(t7.openAPIReference().description)
160+
XCTAssertNil(t8.openAPIReference().description)
161+
XCTAssertNil(t9.openAPIReference().description)
162+
163+
XCTAssertEqual(t1.openAPIReference(withDescription: "hi").description, "hi")
164+
XCTAssertEqual(t2.openAPIReference(withDescription: "hi").description, "hi")
165+
XCTAssertEqual(t3.openAPIReference(withDescription: "hi").description, "hi")
166+
XCTAssertEqual(t4.openAPIReference(withDescription: "hi").description, "hi")
167+
XCTAssertEqual(t5.openAPIReference(withDescription: "hi").description, "hi")
168+
XCTAssertEqual(t6.openAPIReference(withDescription: "hi").description, "hi")
169+
XCTAssertEqual(t7.openAPIReference(withDescription: "hi").description, "hi")
170+
XCTAssertEqual(t8.openAPIReference(withDescription: "hi").description, "hi")
171+
XCTAssertEqual(t9.openAPIReference(withDescription: "hi").description, "hi")
172+
}
131173
}
132174

133175
// MARK: Codable

Tests/OpenAPIKitTests/Schema Object/DereferencedSchemaObjectTests.swift

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,14 @@ final class DereferencedSchemaObjectTests: XCTestCase {
281281
XCTAssertEqual(t1, .string(.init(), .init()))
282282
}
283283

284+
func test_throwingReferenceWithOverriddenDescription() throws {
285+
let components = OpenAPI.Components(
286+
schemas: ["test": .string]
287+
)
288+
let t1 = try JSONSchema.reference(.component(named: "test"), description: "hello").dereferenced(in: components)
289+
XCTAssertEqual(t1, .string(.init(description: "hello"), .init()))
290+
}
291+
284292
func test_optionalObjectWithoutReferences() {
285293
let t1 = JSONSchema.object(properties: ["test": .string]).dereferenced()
286294
XCTAssertEqual(
@@ -504,4 +512,37 @@ final class DereferencedSchemaObjectTests: XCTestCase {
504512
)
505513
}
506514
}
515+
516+
func test_withDescription() throws {
517+
let null = JSONSchema.null.dereferenced()!.with(description: "test")
518+
let object = JSONSchema.object.dereferenced()!.with(description: "test")
519+
let array = JSONSchema.array.dereferenced()!.with(description: "test")
520+
521+
let boolean = JSONSchema.boolean.dereferenced()!.with(description: "test")
522+
let number = JSONSchema.number.dereferenced()!.with(description: "test")
523+
let integer = JSONSchema.integer.dereferenced()!.with(description: "test")
524+
let string = JSONSchema.string.dereferenced()!.with(description: "test")
525+
let fragment = JSONSchema.fragment(.init()).dereferenced()!.with(description: "test")
526+
let all = JSONSchema.all(of: .string).dereferenced()!.with(description: "test")
527+
let one = JSONSchema.one(of: .string).dereferenced()!.with(description: "test")
528+
let any = JSONSchema.any(of: .string).dereferenced()!.with(description: "test")
529+
let not = JSONSchema.not(.string).dereferenced()!.with(description: "test")
530+
531+
XCTAssertEqual(object.description, "test")
532+
XCTAssertEqual(array.description, "test")
533+
534+
XCTAssertEqual(boolean.description, "test")
535+
XCTAssertEqual(number.description, "test")
536+
XCTAssertEqual(integer.description, "test")
537+
XCTAssertEqual(string.description, "test")
538+
XCTAssertEqual(fragment.description, "test")
539+
540+
XCTAssertEqual(all.description, "test")
541+
XCTAssertEqual(one.description, "test")
542+
XCTAssertEqual(any.description, "test")
543+
XCTAssertEqual(not.description, "test")
544+
545+
XCTAssertNil(null.description)
546+
}
547+
507548
}

0 commit comments

Comments
 (0)