Skip to content

Commit 5ce9592

Browse files
committed
Add additionalOperations to PathItem type. Expand HttpMethod type to support additional methods not built into library.
1 parent f08f10f commit 5ce9592

File tree

13 files changed

+241
-109
lines changed

13 files changed

+241
-109
lines changed

Sources/OpenAPIKit/Path Item/DereferencedPathItem.swift

Lines changed: 32 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,11 @@ public struct DereferencedPathItem: Equatable {
3737
/// The dereferenced QUERY operation, if defined.
3838
public let query: DereferencedOperation?
3939

40+
/// Additional operations, keyed by all-caps HTTP method names. This
41+
/// map MUST NOT contain any entries that can be represented by the
42+
/// fixed fields on this type (e.g. `post`, `get`, etc.).
43+
public var additionalOperations: OrderedDictionary<OpenAPI.HttpMethod, DereferencedOperation>
44+
4045
public subscript<T>(dynamicMember path: KeyPath<OpenAPI.PathItem, T>) -> T {
4146
return underlyingPathItem[keyPath: path]
4247
}
@@ -68,6 +73,8 @@ public struct DereferencedPathItem: Equatable {
6873
self.trace = try pathItem.trace.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
6974
self.query = try pathItem.query.map { try DereferencedOperation($0, resolvingIn: components, following: references) }
7075

76+
self.additionalOperations = try pathItem.additionalOperations.mapValues { try DereferencedOperation($0, resolvingIn: components, following: references) }
77+
7178
var pathItem = pathItem
7279
if let name {
7380
pathItem.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name)
@@ -83,24 +90,20 @@ extension DereferencedPathItem {
8390
/// Retrieve the operation for the given verb, if one is set for this path.
8491
public func `for`(_ verb: OpenAPI.HttpMethod) -> DereferencedOperation? {
8592
switch verb {
86-
case .delete:
87-
return self.delete
88-
case .get:
89-
return self.get
90-
case .head:
91-
return self.head
92-
case .options:
93-
return self.options
94-
case .patch:
95-
return self.patch
96-
case .post:
97-
return self.post
98-
case .put:
99-
return self.put
100-
case .trace:
101-
return self.trace
102-
case .query:
103-
return self.query
93+
case .builtin(let builtin):
94+
switch builtin {
95+
case .delete: self.delete
96+
case .get: self.get
97+
case .head: self.head
98+
case .options: self.options
99+
case .patch: self.patch
100+
case .post: self.post
101+
case .put: self.put
102+
case .trace: self.trace
103+
case .query: self.query
104+
}
105+
case .other(let other):
106+
additionalOperations[.other(other)]
104107
}
105108
}
106109

@@ -122,9 +125,11 @@ extension DereferencedPathItem {
122125
/// - Returns: An array of `Endpoints` with the method (i.e. `.get`) and the operation for
123126
/// the method.
124127
public var endpoints: [Endpoint] {
125-
return OpenAPI.HttpMethod.allCases.compactMap { method in
126-
self.for(method).map { .init(method: method, operation: $0) }
128+
let builtins = OpenAPI.BuiltinHttpMethod.allCases.compactMap { method -> Endpoint? in
129+
self.for(.builtin(method)).map { .init(method: .builtin(method), operation: $0) }
127130
}
131+
132+
return builtins + additionalOperations.map { key, value in .init(method: key, operation: value) }
128133
}
129134
}
130135

@@ -158,6 +163,8 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {
158163
let oldTrace = trace
159164
let oldQuery = query
160165

166+
let oldAdditionalOperations = additionalOperations
167+
161168
async let (newParameters, c1, m1) = oldParameters.externallyDereferenced(with: loader)
162169
// async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader)
163170
async let (newGet, c3, m3) = oldGet.externallyDereferenced(with: loader)
@@ -170,6 +177,8 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {
170177
async let (newTrace, c10, m10) = oldTrace.externallyDereferenced(with: loader)
171178
async let (newQuery, c11, m11) = oldQuery.externallyDereferenced(with: loader)
172179

180+
async let (newAdditionalOperations, c12, m12) = oldAdditionalOperations.externallyDereferenced(with: loader)
181+
173182
var pathItem = self
174183
var newComponents = try await c1
175184
var newMessages = try await m1
@@ -187,6 +196,7 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {
187196
pathItem.patch = try await newPatch
188197
pathItem.trace = try await newTrace
189198
pathItem.query = try await newQuery
199+
pathItem.additionalOperations = try await newAdditionalOperations
190200

191201
try await newComponents.merge(c3)
192202
try await newComponents.merge(c4)
@@ -197,6 +207,7 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {
197207
try await newComponents.merge(c9)
198208
try await newComponents.merge(c10)
199209
try await newComponents.merge(c11)
210+
try await newComponents.merge(c12)
200211

201212
try await newMessages += m3
202213
try await newMessages += m4
@@ -207,6 +218,7 @@ extension OpenAPI.PathItem: ExternallyDereferenceable {
207218
try await newMessages += m9
208219
try await newMessages += m10
209220
try await newMessages += m11
221+
try await newMessages += m12
210222

211223
if let oldServers {
212224
async let (newServers, c2, m2) = oldServers.externallyDereferenced(with: loader)

Sources/OpenAPIKit/Path Item/PathItem.swift

Lines changed: 79 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,11 @@ extension OpenAPI {
5757
/// The `QUERY` endpoint at this path, if one exists.
5858
public var query: Operation?
5959

60+
/// Additional operations, keyed by all-caps HTTP method names. This
61+
/// map MUST NOT contain any entries that can be represented by the
62+
/// fixed fields on this type (e.g. `post`, `get`, etc.).
63+
public var additionalOperations: OrderedDictionary<OpenAPI.HttpMethod, Operation>
64+
6065
/// Dictionary of vendor extensions.
6166
///
6267
/// These should be of the form:
@@ -84,6 +89,7 @@ extension OpenAPI {
8489
patch: Operation? = nil,
8590
trace: Operation? = nil,
8691
query: Operation? = nil,
92+
additionalOperations: OrderedDictionary<OpenAPI.HttpMethod, Operation> = [:],
8793
vendorExtensions: [String: AnyCodable] = [:]
8894
) {
8995
self.summary = summary
@@ -100,11 +106,14 @@ extension OpenAPI {
100106
self.patch = patch
101107
self.trace = trace
102108
self.query = query
109+
self.additionalOperations = additionalOperations
103110
self.vendorExtensions = vendorExtensions
104111

105112
self.conditionalWarnings = [
106113
// If query is non-nil, the document must be OAS version 3.2.0 or greater
107-
nonNilVersionWarning(fieldName: "query", value: query, minimumVersion: .v3_2_0)
114+
nonNilVersionWarning(fieldName: "query", value: query, minimumVersion: .v3_2_0),
115+
// If there are additionalOperations defiend, the document must be OAS version 3.2.0 or greater
116+
nonEmptyVersionWarning(fieldName: "additionalOperations", value: additionalOperations, minimumVersion: .v3_2_0)
108117
].compactMap { $0 }
109118
}
110119

@@ -170,6 +179,7 @@ extension OpenAPI.PathItem: Equatable {
170179
&& lhs.patch == rhs.patch
171180
&& lhs.trace == rhs.trace
172181
&& lhs.query == rhs.query
182+
&& lhs.additionalOperations == rhs.additionalOperations
173183
&& lhs.vendorExtensions == rhs.vendorExtensions
174184
}
175185
}
@@ -183,6 +193,15 @@ fileprivate func nonNilVersionWarning<Subject>(fieldName: String, value: Subject
183193
}
184194
}
185195

196+
fileprivate func nonEmptyVersionWarning<Key, Value>(fieldName: String, value: OrderedDictionary<Key, Value>, minimumVersion: OpenAPI.Document.Version) -> (any Condition, OpenAPI.Warning)? {
197+
if value.isEmpty { return nil }
198+
199+
return OpenAPI.Document.ConditionalWarnings.version(
200+
lessThan: minimumVersion,
201+
doesNotSupport: "The PathItem \(fieldName) map"
202+
)
203+
}
204+
186205
extension OpenAPI.PathItem {
187206
public typealias Map = OrderedDictionary<OpenAPI.Path, Either<OpenAPI.Reference<OpenAPI.PathItem>, OpenAPI.PathItem>>
188207
}
@@ -198,48 +217,49 @@ extension OpenAPI.PathItem {
198217
/// Retrieve the operation for the given verb, if one is set for this path.
199218
public func `for`(_ verb: OpenAPI.HttpMethod) -> OpenAPI.Operation? {
200219
switch verb {
201-
case .delete:
202-
return self.delete
203-
case .get:
204-
return self.get
205-
case .head:
206-
return self.head
207-
case .options:
208-
return self.options
209-
case .patch:
210-
return self.patch
211-
case .post:
212-
return self.post
213-
case .put:
214-
return self.put
215-
case .trace:
216-
return self.trace
217-
case .query:
218-
return self.query
220+
case .builtin(let builtin):
221+
switch builtin {
222+
case .delete: self.delete
223+
case .get: self.get
224+
case .head: self.head
225+
case .options: self.options
226+
case .patch: self.patch
227+
case .post: self.post
228+
case .put: self.put
229+
case .trace: self.trace
230+
case .query: self.query
231+
}
232+
case .other(let other):
233+
additionalOperations[.other(other)]
219234
}
220235
}
221236

222237
/// Set the operation for the given verb, overwriting any already set operation for the same verb.
223238
public mutating func set(operation: OpenAPI.Operation?, for verb: OpenAPI.HttpMethod) {
224239
switch verb {
225-
case .delete:
226-
self.delete(operation)
227-
case .get:
228-
self.get(operation)
229-
case .head:
230-
self.head(operation)
231-
case .options:
232-
self.options(operation)
233-
case .patch:
234-
self.patch(operation)
235-
case .post:
236-
self.post(operation)
237-
case .put:
238-
self.put(operation)
239-
case .trace:
240-
self.trace(operation)
241-
case .query:
242-
self.query(operation)
240+
case .builtin(let builtin):
241+
switch builtin {
242+
case .delete:
243+
self.delete(operation)
244+
case .get:
245+
self.get(operation)
246+
case .head:
247+
self.head(operation)
248+
case .options:
249+
self.options(operation)
250+
case .patch:
251+
self.patch(operation)
252+
case .post:
253+
self.post(operation)
254+
case .put:
255+
self.put(operation)
256+
case .trace:
257+
self.trace(operation)
258+
case .query:
259+
self.query(operation)
260+
}
261+
case .other(let other):
262+
self.additionalOperations[.other(other)] = operation
243263
}
244264
}
245265

@@ -264,9 +284,11 @@ extension OpenAPI.PathItem {
264284
/// - Returns: An array of `Endpoints` with the method (i.e. `.get`) and the operation for
265285
/// the method.
266286
public var endpoints: [Endpoint] {
267-
return OpenAPI.HttpMethod.allCases.compactMap { method in
268-
self.for(method).map { .init(method: method, operation: $0) }
287+
let builtins = OpenAPI.BuiltinHttpMethod.allCases.compactMap { method -> Endpoint? in
288+
self.for(.builtin(method)).map { .init(method: .builtin(method), operation: $0) }
269289
}
290+
291+
return builtins + additionalOperations.map { key, value in .init(method: key, operation: value) }
270292
}
271293
}
272294

@@ -312,6 +334,10 @@ extension OpenAPI.PathItem: Encodable {
312334
try container.encodeIfPresent(trace, forKey: .trace)
313335
try container.encodeIfPresent(query, forKey: .query)
314336

337+
if !additionalOperations.isEmpty {
338+
try container.encode(additionalOperations, forKey: .additionalOperations)
339+
}
340+
315341
if VendorExtensionsConfiguration.isEnabled(for: encoder) {
316342
try encodeExtensions(to: &container)
317343
}
@@ -338,11 +364,15 @@ extension OpenAPI.PathItem: Decodable {
338364
trace = try container.decodeIfPresent(OpenAPI.Operation.self, forKey: .trace)
339365
query = try container.decodeIfPresent(OpenAPI.Operation.self, forKey: .query)
340366

367+
additionalOperations = try container.decodeIfPresent(OrderedDictionary<OpenAPI.HttpMethod, OpenAPI.Operation>.self, forKey: .additionalOperations) ?? [:]
368+
341369
vendorExtensions = try Self.extensions(from: decoder)
342370

343371
self.conditionalWarnings = [
344372
// If query is non-nil, the document must be OAS version 3.2.0 or greater
345-
nonNilVersionWarning(fieldName: "query", value: query, minimumVersion: .v3_2_0)
373+
nonNilVersionWarning(fieldName: "query", value: query, minimumVersion: .v3_2_0),
374+
// If there are additionalOperations defiend, the document must be OAS version 3.2.0 or greater
375+
nonEmptyVersionWarning(fieldName: "additionalOperations", value: additionalOperations, minimumVersion: .v3_2_0)
346376
].compactMap { $0 }
347377
} catch let error as DecodingError {
348378

@@ -377,6 +407,8 @@ extension OpenAPI.PathItem {
377407
case trace
378408
case query
379409

410+
case additionalOperations
411+
380412
case extended(String)
381413

382414
static var allBuiltinKeys: [CodingKeys] {
@@ -394,7 +426,9 @@ extension OpenAPI.PathItem {
394426
.head,
395427
.patch,
396428
.trace,
397-
.query
429+
.query,
430+
431+
.additionalOperations
398432
]
399433
}
400434

@@ -430,6 +464,8 @@ extension OpenAPI.PathItem {
430464
self = .trace
431465
case "query":
432466
self = .query
467+
case "additionalOperations":
468+
self = .additionalOperations
433469
default:
434470
self = .extendedKey(for: stringValue)
435471
}
@@ -463,6 +499,8 @@ extension OpenAPI.PathItem {
463499
return "trace"
464500
case .query:
465501
return "query"
502+
case .additionalOperations:
503+
return "additionalOperations"
466504
case .extended(let key):
467505
return key
468506
}

0 commit comments

Comments
 (0)