Skip to content

Commit be89ec8

Browse files
authored
Merge pull request #454 from mattpolzin/feature/377/support-refs-in-components
Support references in Components Object entries
2 parents ade52d7 + 99831b8 commit be89ec8

25 files changed

+676
-146
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ OpenAPIKit follows semantic versioning despite the fact that the OpenAPI specifi
1212
|------------|-------|--------------------|-----------------------------------|--------------|
1313
| v3.x | 5.1+ || | |
1414
| v4.x | 5.8+ ||| |
15-
| v4.x | 5.8+ ||||
15+
| v5.x | 5.10+ ||||
1616

1717
- [Usage](#usage)
1818
- [Migration](#migration)

Sources/OpenAPIKit/Components Object/Components+JSONReference.swift

Lines changed: 234 additions & 33 deletions
Large diffs are not rendered by default.

Sources/OpenAPIKit/Components Object/Components+Locatable.swift

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,57 +16,57 @@ public protocol ComponentDictionaryLocatable: SummaryOverridable {
1616
/// This can be used to create a JSON path
1717
/// like `#/name1/name2/name3`
1818
static var openAPIComponentsKey: String { get }
19-
static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { get }
19+
static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { get }
2020
}
2121

2222
extension JSONSchema: ComponentDictionaryLocatable {
2323
public static var openAPIComponentsKey: String { "schemas" }
24-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.schemas }
24+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .a(\.schemas) }
2525
}
2626

2727
extension OpenAPI.Response: ComponentDictionaryLocatable {
2828
public static var openAPIComponentsKey: String { "responses" }
29-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.responses }
29+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.responses) }
3030
}
3131

3232
extension OpenAPI.Callbacks: ComponentDictionaryLocatable & SummaryOverridable {
3333
public static var openAPIComponentsKey: String { "callbacks" }
34-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.callbacks }
34+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.callbacks) }
3535
}
3636

3737
extension OpenAPI.Parameter: ComponentDictionaryLocatable {
3838
public static var openAPIComponentsKey: String { "parameters" }
39-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.parameters }
39+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.parameters) }
4040
}
4141

4242
extension OpenAPI.Example: ComponentDictionaryLocatable {
4343
public static var openAPIComponentsKey: String { "examples" }
44-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.examples }
44+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.examples) }
4545
}
4646

4747
extension OpenAPI.Request: ComponentDictionaryLocatable {
4848
public static var openAPIComponentsKey: String { "requestBodies" }
49-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.requestBodies }
49+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.requestBodies) }
5050
}
5151

5252
extension OpenAPI.Header: ComponentDictionaryLocatable {
5353
public static var openAPIComponentsKey: String { "headers" }
54-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.headers }
54+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.headers) }
5555
}
5656

5757
extension OpenAPI.SecurityScheme: ComponentDictionaryLocatable {
5858
public static var openAPIComponentsKey: String { "securitySchemes" }
59-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.securitySchemes }
59+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.securitySchemes) }
6060
}
6161

6262
extension OpenAPI.Link: ComponentDictionaryLocatable {
6363
public static var openAPIComponentsKey: String { "links" }
64-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.links }
64+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.links) }
6565
}
6666

6767
extension OpenAPI.PathItem: ComponentDictionaryLocatable {
6868
public static var openAPIComponentsKey: String { "pathItems" }
69-
public static var openAPIComponentsKeyPath: WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>> { \.pathItems }
69+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .a(\.pathItems) }
7070
}
7171

7272
/// A dereferenceable type can be recursively looked up in

Sources/OpenAPIKit/Components Object/Components.swift

Lines changed: 97 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,47 @@ extension OpenAPI {
1515
///
1616
/// This is a place to put reusable components to
1717
/// be referenced from other parts of the spec.
18+
///
19+
/// Most of the components dictionaries can contain either the component
20+
/// directly or a $ref to the component. This distinction can be seen in
21+
/// the types as either `ComponentDictionary<T>` (direct) or
22+
/// `ComponentReferenceDictionary<T>` (direct or by-reference).
23+
///
24+
/// If you are building a Components Object in Swift you may choose to make
25+
/// all of your components direct in which case the
26+
/// `OpenAPI.Components.direct()` convenience constructor will save you
27+
/// some typing and verbosity.
28+
///
29+
/// **Example**
30+
/// OpenAPI.Components(
31+
/// parameters: [ "my_param": .parameter(.cookie(name: "my_param", schema: .string)) ]
32+
/// )
33+
///
34+
/// // The above value is the same as the below value
35+
///
36+
/// OpenAPI.Components.direct(
37+
/// parameters: [ "my_param": .cookie(name: "my_param", schema: .string) ]
38+
/// )
39+
///
40+
/// // However, the `init()` initializer does allow you to use references where desired
41+
///
42+
/// OpenAPI.Components(
43+
/// parameters: [
44+
/// "my_direct_param": .parameter(.cookie(name: "my_param", schema: .string)),
45+
/// "my_param": .reference(.component(named: "my_direct_param"))
46+
/// ]
47+
/// )
1848
public struct Components: Equatable, CodableVendorExtendable, Sendable {
1949

2050
public var schemas: ComponentDictionary<JSONSchema>
21-
public var responses: ComponentDictionary<Response>
22-
public var parameters: ComponentDictionary<Parameter>
23-
public var examples: ComponentDictionary<Example>
24-
public var requestBodies: ComponentDictionary<Request>
25-
public var headers: ComponentDictionary<Header>
26-
public var securitySchemes: ComponentDictionary<SecurityScheme>
27-
public var links: ComponentDictionary<Link>
28-
public var callbacks: ComponentDictionary<Callbacks>
51+
public var responses: ComponentReferenceDictionary<Response>
52+
public var parameters: ComponentReferenceDictionary<Parameter>
53+
public var examples: ComponentReferenceDictionary<Example>
54+
public var requestBodies: ComponentReferenceDictionary<Request>
55+
public var headers: ComponentReferenceDictionary<Header>
56+
public var securitySchemes: ComponentReferenceDictionary<SecurityScheme>
57+
public var links: ComponentReferenceDictionary<Link>
58+
public var callbacks: ComponentReferenceDictionary<Callbacks>
2959

3060
public var pathItems: ComponentDictionary<PathItem>
3161

@@ -38,14 +68,14 @@ extension OpenAPI {
3868

3969
public init(
4070
schemas: ComponentDictionary<JSONSchema> = [:],
41-
responses: ComponentDictionary<Response> = [:],
42-
parameters: ComponentDictionary<Parameter> = [:],
43-
examples: ComponentDictionary<Example> = [:],
44-
requestBodies: ComponentDictionary<Request> = [:],
45-
headers: ComponentDictionary<Header> = [:],
46-
securitySchemes: ComponentDictionary<SecurityScheme> = [:],
47-
links: ComponentDictionary<Link> = [:],
48-
callbacks: ComponentDictionary<Callbacks> = [:],
71+
responses: ComponentReferenceDictionary<Response> = [:],
72+
parameters: ComponentReferenceDictionary<Parameter> = [:],
73+
examples: ComponentReferenceDictionary<Example> = [:],
74+
requestBodies: ComponentReferenceDictionary<Request> = [:],
75+
headers: ComponentReferenceDictionary<Header> = [:],
76+
securitySchemes: ComponentReferenceDictionary<SecurityScheme> = [:],
77+
links: ComponentReferenceDictionary<Link> = [:],
78+
callbacks: ComponentReferenceDictionary<Callbacks> = [:],
4979
pathItems: ComponentDictionary<PathItem> = [:],
5080
vendorExtensions: [String: AnyCodable] = [:]
5181
) {
@@ -62,6 +92,37 @@ extension OpenAPI {
6292
self.vendorExtensions = vendorExtensions
6393
}
6494

95+
/// Construct components as "direct" entries (no references). When
96+
/// building a document in Swift code, this is often sufficient and it
97+
/// means you don't need to wrap every entry in an `Either`.
98+
public static func direct(
99+
schemas: ComponentDictionary<JSONSchema> = [:],
100+
responses: ComponentDictionary<Response> = [:],
101+
parameters: ComponentDictionary<Parameter> = [:],
102+
examples: ComponentDictionary<Example> = [:],
103+
requestBodies: ComponentDictionary<Request> = [:],
104+
headers: ComponentDictionary<Header> = [:],
105+
securitySchemes: ComponentDictionary<SecurityScheme> = [:],
106+
links: ComponentDictionary<Link> = [:],
107+
callbacks: ComponentDictionary<Callbacks> = [:],
108+
pathItems: ComponentDictionary<PathItem> = [:],
109+
vendorExtensions: [String: AnyCodable] = [:]
110+
) -> Self {
111+
.init(
112+
schemas: schemas,
113+
responses: responses.mapValues { .b($0) },
114+
parameters: parameters.mapValues { .b($0) },
115+
examples: examples.mapValues { .b($0) },
116+
requestBodies: requestBodies.mapValues { .b($0) },
117+
headers: headers.mapValues { .b($0) },
118+
securitySchemes: securitySchemes.mapValues { .b($0) },
119+
links: links.mapValues { .b($0) },
120+
callbacks: callbacks.mapValues { .b($0) },
121+
pathItems: pathItems,
122+
vendorExtensions: vendorExtensions
123+
)
124+
}
125+
65126
/// An empty OpenAPI Components Object.
66127
public static let noComponents: Components = .init()
67128

@@ -71,6 +132,12 @@ extension OpenAPI {
71132
}
72133
}
73134

135+
extension OpenAPI {
136+
137+
public typealias ComponentDictionary<T> = OrderedDictionary<ComponentKey, T>
138+
public typealias ComponentReferenceDictionary<T: ComponentDictionaryLocatable> = OrderedDictionary<ComponentKey, Either<OpenAPI.Reference<T>, T>>
139+
}
140+
74141
extension OpenAPI.Components {
75142
public struct ComponentCollision: Swift.Error {
76143
public let componentType: String
@@ -130,11 +197,6 @@ extension OpenAPI.Components {
130197
public static let componentNameExtension: String = "x-component-name"
131198
}
132199

133-
extension OpenAPI {
134-
135-
public typealias ComponentDictionary<T> = OrderedDictionary<ComponentKey, T>
136-
}
137-
138200
// MARK: - Codable
139201
extension OpenAPI.Components: Encodable {
140202
public func encode(to encoder: Encoder) throws {
@@ -194,30 +256,36 @@ extension OpenAPI.Components: Decodable {
194256
schemas = try container.decodeIfPresent(OpenAPI.ComponentDictionary<JSONSchema>.self, forKey: .schemas)
195257
?? [:]
196258

197-
responses = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Response>.self, forKey: .responses)
259+
responses = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Response>.self, forKey: .responses)
198260
?? [:]
199261

200-
parameters = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Parameter>.self, forKey: .parameters)
262+
parameters = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Parameter>.self, forKey: .parameters)
201263
?? [:]
202264

203-
examples = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Example>.self, forKey: .examples)
265+
examples = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Example>.self, forKey: .examples)
204266
?? [:]
205267

206-
requestBodies = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Request>.self, forKey: .requestBodies)
268+
requestBodies = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Request>.self, forKey: .requestBodies)
207269
?? [:]
208270

209-
headers = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Header>.self, forKey: .headers)
271+
headers = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Header>.self, forKey: .headers)
210272
?? [:]
211273

212-
securitySchemes = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.SecurityScheme>.self, forKey: .securitySchemes) ?? [:]
274+
securitySchemes = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.SecurityScheme>.self, forKey: .securitySchemes) ?? [:]
213275

214-
links = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Link>.self, forKey: .links) ?? [:]
276+
links = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Link>.self, forKey: .links) ?? [:]
215277

216-
callbacks = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.Callbacks>.self, forKey: .callbacks) ?? [:]
278+
callbacks = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Callbacks>.self, forKey: .callbacks) ?? [:]
217279

218280
pathItems = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.PathItem>.self, forKey: .pathItems) ?? [:]
219281

220282
vendorExtensions = try Self.extensions(from: decoder)
283+
} catch let error as EitherDecodeNoTypesMatchedError {
284+
if let underlyingError = OpenAPI.Error.Decoding.Document.eitherBranchToDigInto(error) {
285+
throw (underlyingError.underlyingError ?? underlyingError)
286+
}
287+
288+
throw error
221289
} catch let error as DecodingError {
222290
if let underlyingError = error.underlyingError as? KeyDecodingError {
223291
throw GenericError(

Sources/OpenAPIKit/Either/Either+Convenience.swift

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,16 @@ extension Either where B == OpenAPI.Header {
131131
public var headerValue: B? { b }
132132
}
133133

134+
extension Either where B == OpenAPI.Callbacks {
135+
/// Retrieve the callbacks if that is what this property contains.
136+
public var callbacksValue: B? { b }
137+
}
138+
139+
extension Either where B == OpenAPI.SecurityScheme {
140+
/// Retrieve the security scheme if that is what this property contains.
141+
public var securitySchemeValue: B? { b }
142+
}
143+
134144
// MARK: - Convenience constructors
135145
extension Either where A == Bool {
136146
/// Construct a boolean value.
@@ -220,7 +230,22 @@ extension Either where B == OpenAPI.Response {
220230
public static func response(_ response: OpenAPI.Response) -> Self { .b(response) }
221231
}
222232

233+
extension Either where B == OpenAPI.Link {
234+
/// Construct a link value.
235+
public static func link(_ link: OpenAPI.Link) -> Self { .b(link) }
236+
}
237+
223238
extension Either where B == OpenAPI.Header {
224239
/// Construct a header value.
225240
public static func header(_ header: OpenAPI.Header) -> Self { .b(header) }
226241
}
242+
243+
extension Either where B == OpenAPI.Callbacks {
244+
/// Construct a callbacks value.
245+
public static func callbacks(_ callbacks: OpenAPI.Callbacks) -> Self { .b(callbacks) }
246+
}
247+
248+
extension Either where B == OpenAPI.SecurityScheme {
249+
/// Construct a security scheme value.
250+
public static func securityScheme(_ securityScheme: OpenAPI.SecurityScheme) -> Self { .b(securityScheme) }
251+
}

Sources/OpenAPIKit/JSONReference.swift

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -428,6 +428,20 @@ public protocol OpenAPISummarizable: OpenAPIDescribable {
428428
func overriddenNonNil(summary: String?) -> Self
429429
}
430430

431+
extension OpenAPI.Reference: OpenAPISummarizable {
432+
public func overriddenNonNil(summary: String?) -> Self {
433+
guard let summary else { return self }
434+
435+
return .init(jsonReference, summary: summary, description: description)
436+
}
437+
438+
public func overriddenNonNil(description: String?) -> Self {
439+
guard let description else { return self }
440+
441+
return .init(jsonReference, summary: summary, description: description)
442+
}
443+
}
444+
431445
// MARK: - Codable
432446

433447
extension JSONReference {
@@ -558,7 +572,12 @@ extension JSONReference: ExternallyDereferenceable where ReferenceType: External
558572
let componentKey = try loader.componentKey(type: ReferenceType.self, at: url)
559573
let (component, messages): (ReferenceType, [Loader.Message]) = try await loader.load(url)
560574
var components = OpenAPI.Components()
561-
components[keyPath: ReferenceType.openAPIComponentsKeyPath][componentKey] = component
575+
switch ReferenceType.openAPIComponentsKeyPath {
576+
case .a(let directPath):
577+
components[keyPath: directPath][componentKey] = component
578+
case .b(let referencePath):
579+
components[keyPath: referencePath][componentKey] = .b(component)
580+
}
562581
return (try components.reference(named: componentKey.rawValue, ofType: ReferenceType.self).jsonReference, components, messages)
563582
}
564583
}

Sources/OpenAPIKitCompat/Compat30To31.swift

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -652,14 +652,14 @@ extension OpenAPIKit30.OpenAPI.Components: To31 {
652652
fileprivate func to31() -> OpenAPIKit.OpenAPI.Components {
653653
OpenAPIKit.OpenAPI.Components(
654654
schemas: schemas.mapValues { $0.to31() },
655-
responses: responses.mapValues { $0.to31() },
656-
parameters: parameters.mapValues { $0.to31() },
657-
examples: examples.mapValues { $0.to31() },
658-
requestBodies: requestBodies.mapValues { $0.to31() },
659-
headers: headers.mapValues { $0.to31() },
660-
securitySchemes: securitySchemes.mapValues { $0.to31() },
661-
links: links.mapValues { $0.to31() },
662-
callbacks: callbacks.mapValues { $0.to31() },
655+
responses: responses.mapValues { .b($0.to31()) },
656+
parameters: parameters.mapValues { .b($0.to31()) },
657+
examples: examples.mapValues { .b($0.to31()) },
658+
requestBodies: requestBodies.mapValues { .b($0.to31()) },
659+
headers: headers.mapValues { .b($0.to31()) },
660+
securitySchemes: securitySchemes.mapValues { .b($0.to31()) },
661+
links: links.mapValues { .b($0.to31()) },
662+
callbacks: callbacks.mapValues { .b($0.to31()) },
663663
vendorExtensions: vendorExtensions
664664
)
665665
}

0 commit comments

Comments
 (0)