-
Notifications
You must be signed in to change notification settings - Fork 90
/
OperationDescription.swift
321 lines (282 loc) · 13.6 KB
/
OperationDescription.swift
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
//===----------------------------------------------------------------------===//
//
// This source file is part of the SwiftOpenAPIGenerator open source project
//
// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors
// Licensed under Apache License v2.0
//
// See LICENSE.txt for license information
// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors
//
// SPDX-License-Identifier: Apache-2.0
//
//===----------------------------------------------------------------------===//
import OpenAPIKit
import Foundation
/// A wrapper of an OpenAPI operation that includes the information
/// about the parent containers of the operation, such as its path
/// item.
struct OperationDescription {
/// The OpenAPI path of the operation.
var path: OpenAPI.Path
/// The OpenAPI endpoint of the operation.
var endpoint: OpenAPI.PathItem.Endpoint
/// The path parameters at the operation level.
var pathParameters: OpenAPI.Parameter.Array
/// The OpenAPI components, used to resolve JSON references.
var components: OpenAPI.Components
/// A converted function from user-provided strings to strings
/// safe to be used as a Swift identifier.
var asSwiftSafeName: (String) -> String
/// The OpenAPI operation object.
var operation: OpenAPI.Operation { endpoint.operation }
/// The HTTP method of the operation.
var httpMethod: OpenAPI.HttpMethod { endpoint.method }
/// Returns a lowercased string for the HTTP method.
var httpMethodLowercased: String { httpMethod.rawValue.lowercased() }
}
extension OperationDescription {
/// Returns operation descriptions for all the operations discovered
/// in the specified paths dictionary.
/// - Parameters:
/// - map: The paths from the OpenAPI document.
/// - components: The components from the OpenAPI document.
/// - asSwiftSafeName: A converted function from user-provided strings
/// to strings safe to be used as a Swift identifier.
/// - Returns: An array of `OperationDescription` instances, each representing
/// an operation discovered in the provided paths.
/// - Throws: if `map` contains any references; see discussion for details.
///
/// This function will throw an error if `map` contains any references, because:
/// 1. OpenAPI 3.0.3 only supports external path references (cf. 3.1, which supports internal references too)
/// 2. Swift OpenAPI Generator currently only supports OpenAPI 3.0.x.
/// 3. Swift OpenAPI Generator currently doesn't support external references.
static func all(
from map: OpenAPI.PathItem.Map,
in components: OpenAPI.Components,
asSwiftSafeName: @escaping (String) -> String
) throws -> [OperationDescription] {
try map.flatMap { path, value in
let value = try value.resolve(in: components)
return value.endpoints.map { endpoint in
OperationDescription(
path: path,
endpoint: endpoint,
pathParameters: value.parameters,
components: components,
asSwiftSafeName: asSwiftSafeName
)
}
}
}
/// Returns a Swift-safe function name for the operation.
///
/// Uses the `operationID` value in the OpenAPI operation, if one was
/// specified. Otherwise, computes a unique name from the operation's
/// path and HTTP method.
var methodName: String { asSwiftSafeName(operationID) }
/// Returns the identifier for the operation.
///
/// If none was provided in the OpenAPI document, synthesizes one from
/// the path and HTTP method.
var operationID: String {
if let operationID = operation.operationId { return operationID }
return "\(httpMethod.rawValue.lowercased())\(path.rawValue)"
}
/// Returns a documentation comment for the method implementing
/// the OpenAPI operation.
var comment: Comment { .init(from: self) }
/// Returns the type name of the namespace unique to the operation.
var operationNamespace: TypeName {
.init(
components: [.root, .init(swift: Constants.Operations.namespace, json: "paths")]
+ path.components.map { .init(swift: nil, json: $0) } + [
.init(swift: methodName, json: httpMethod.rawValue)
]
)
}
/// Returns the name of the Input type.
var inputTypeName: TypeName {
operationNamespace.appending(
swiftComponent: Constants.Operation.Input.typeName,
// intentionally nil, we'll append the specific params etc
// with their valid JSON key path when nested inside Input
jsonComponent: nil
)
}
/// Returns the name of the Output type.
var outputTypeName: TypeName {
operationNamespace.appending(swiftComponent: Constants.Operation.Output.typeName, jsonComponent: "responses")
}
/// Returns the name of the AcceptableContentType type.
var acceptableContentTypeName: TypeName {
operationNamespace.appending(
swiftComponent: Constants.Operation.AcceptableContentType.typeName,
// intentionally nil, we'll append the specific params etc
// with their valid JSON key path if nested further
jsonComponent: nil
)
}
/// Returns the name of the array of wrapped AcceptableContentType type.
var acceptableArrayName: TypeUsage {
acceptableContentTypeName.asUsage
.asWrapped(in: .runtime(Constants.Operation.AcceptableContentType.headerTypeName)).asArray
}
/// Merged parameters from both the path item level and the operation level.
/// If duplicate parameters exist, only the parameters from the operation level are preserved.
///
/// - Returns: An array of merged path item and operation level parameters without duplicates.
/// - Throws: When an invalid JSON reference is found.
var allParameters: [UnresolvedParameter] {
get throws {
var mergedParameters: [UnresolvedParameter] = []
var uniqueIdentifiers: Set<String> = []
let allParameters = pathParameters + operation.parameters
for parameter in allParameters.reversed() {
let resolvedParameter = try parameter.resolve(in: components)
let identifier = resolvedParameter.location.rawValue + ":" + resolvedParameter.name
guard !uniqueIdentifiers.contains(identifier) else { continue }
mergedParameters.append(parameter)
uniqueIdentifiers.insert(identifier)
}
return mergedParameters.reversed()
}
}
/// Returns all parameters by resolving any parameter references first.
///
/// - Throws: When an invalid JSON reference is found.
var allResolvedParameters: [OpenAPI.Parameter] {
get throws { try allParameters.map { try $0.resolve(in: components) } }
}
/// Returns the path parameters from both the path item level and the
/// operation level.
var allPathParameters: [UnresolvedParameter] {
get throws { try allParameters.filter { (try $0.resolve(in: components).location) == .path } }
}
/// Returns the query parameters from both the path item level and the
/// operation level.
var allQueryParameters: [UnresolvedParameter] {
get throws { try allParameters.filter { (try $0.resolve(in: components).location) == .query } }
}
/// Returns the header parameters from both the path item level and the
/// operation level.
var allHeaderParameters: [UnresolvedParameter] {
get throws { try allParameters.filter { (try $0.resolve(in: components).location) == .header } }
}
/// Returns the cookie parameters from both the path item level and the
/// operation level.
var allCookieParameters: [UnresolvedParameter] {
get throws { try allParameters.filter { (try $0.resolve(in: components).location) == .cookie } }
}
/// Returns a string representing the JSON path to the operation object.
var jsonPathComponent: String {
[
"#", "paths", path.rawValue,
endpoint.method.rawValue.lowercased() + (operation.operationId.flatMap { "(\($0))" } ?? ""),
]
.joined(separator: "/")
}
/// Returns the type name of the response struct for the specified kind.
func responseStructTypeName(for responseKind: ResponseKind) -> TypeName {
responseKind.typeName(in: outputTypeName)
}
/// Returns the signature of the function representing the OpenAPI operation
/// in the API protocol.
var protocolSignatureDescription: FunctionSignatureDescription {
.init(
// Do not respect the access modifier here, as this is a protocol
// declaration, so we don't put `public` on methods, only on the
// protocol itself.
accessModifier: nil,
kind: .function(name: methodName),
parameters: [.init(name: Constants.Operation.Input.variableName, type: .init(inputTypeName))],
keywords: [.async, .throws],
returnType: .identifierType(outputTypeName)
)
}
/// Returns the signature of the function representing the OpenAPI operation
/// in the generated server stubs.
var serverImplSignatureDescription: FunctionSignatureDescription {
.init(
accessModifier: nil,
kind: .function(name: methodName),
parameters: [
.init(label: "request", type: .init(TypeName.request)),
.init(label: "body", type: .optional(.init(TypeName.body))),
.init(label: "metadata", type: .init(TypeName.serverRequestMetadata)),
],
keywords: [.async, .throws],
returnType: .tuple([.identifierType(TypeName.response), .identifierType(TypeName.body.asUsage.asOptional)])
)
}
/// The regular expression for parsing subcomponents of path components.
///
/// Either a parameter `{foo}` or a constant value `foo`.
private static let pathParameterRegex = try! NSRegularExpression(pattern: #"(\{[a-zA-Z0-9_]+\})|([^{}]+)"#)
/// Returns a string that contains the template to be generated for
/// the client that fills in path parameters, and an array expression
/// with the parameter values.
///
/// For example, `/cats/{}` and `[input.catId]`.
var templatedPathForClient: (String, Expression) {
get throws {
let pathParameterNames = try Set(allResolvedParameters.filter { $0.location == .path }.map(\.name))
var orderedPathParameters: [String] = []
// Replace "{foo}" with "{}" for each parameter and record the order
// in which the parameters are used.
var newComponents: [String] = []
for component in path.components {
let matches = Self.pathParameterRegex.matches(
in: component,
options: [],
range: NSRange(location: 0, length: component.utf16.count)
)
var subcomponents: [String] = []
for match in matches {
for i in 1..<match.numberOfRanges {
let range = match.range(at: i)
guard range.location != NSNotFound, let swiftRange = Range(range, in: component) else {
continue
}
let value = component[swiftRange]
if value.hasPrefix("{") && value.hasSuffix("}") {
let componentName = String(value.dropFirst().dropLast())
guard pathParameterNames.contains(componentName) else {
throw GenericError(
message:
"Parameter '\(componentName)' used in the path '\(self.path.rawValue)', but not found in the defined list of path parameters."
)
}
orderedPathParameters.append(componentName)
subcomponents.append("{}")
} else {
subcomponents.append(String(value))
}
}
}
newComponents.append(subcomponents.joined())
}
let newPath = OpenAPI.Path(newComponents, trailingSlash: path.trailingSlash)
let names: [Expression] = orderedPathParameters.map { param in
.identifierPattern("input").dot("path").dot(asSwiftSafeName(param))
}
let arrayExpr: Expression = .literal(.array(names))
return (newPath.rawValue, arrayExpr)
}
}
/// A Boolean value that indicates whether the operation defines
/// a default response.
var containsDefaultResponse: Bool { operation.responses.contains(key: .default) }
/// Returns the operation.responseOutcomes while ensuring if a `.default`
/// responseOutcome is present, then it is the last element in the returned array
var responseOutcomes: [OpenAPI.Operation.ResponseOutcome] {
var outcomes = operation.responseOutcomes
// if .default is present and not already last
if let index = outcomes.firstIndex(where: { $0.status == .default }), index != (outcomes.count - 1) {
// then we move it to be last
let defaultResp = outcomes.remove(at: index)
outcomes.append(defaultResp)
}
return outcomes
}
}