Skip to content

Commit 0ae57b9

Browse files
authored
[Generator] Support unexploded query items (#171)
[Generator] Support unexploded query items ### Motivation Depends on apple/swift-openapi-runtime#35. Fixes #52. By default, query items are encoded as exploded (`key=value1&key=value2`), but OpenAPI allows explicitly requesting them unexploded (`key=value1,value2`). This feature missing has shown up in a few OpenAPI documents recently. ### Modifications Adapt the generator to provide the two new `style` and `explode` parameters to query item encoding/decoding functions. ### Result We now support unexploded query items. ### Test Plan Expanded snippet-based tests to allow generating not just types, but also parts of the client/server. This has allowed us to not have to expand file-based reference tests here. Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (docc test) - Build finished. ✔︎ pull request validation (integration test) - Build finished. ✔︎ pull request validation (nightly) - Build finished. ✔︎ pull request validation (soundness) - Build finished. #171
1 parent fd64063 commit 0ae57b9

File tree

14 files changed

+340
-20
lines changed

14 files changed

+340
-20
lines changed

Package.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,7 @@ let package = Package(
7878
),
7979

8080
// Tests-only: Runtime library linked by generated code
81-
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.7")),
81+
.package(url: "https://github.com/apple/swift-openapi-runtime", .upToNextMinor(from: "0.1.8")),
8282

8383
// Build and preview docs
8484
.package(url: "https://github.com/apple/swift-docc-plugin", from: "1.0.0"),

Sources/_OpenAPIGeneratorCore/Layers/StructuredSwiftRepresentation.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,11 @@ enum LiteralDescription: Equatable, Codable {
9797
/// For example `42`.
9898
case int(Int)
9999

100+
/// A Boolean literal.
101+
///
102+
/// For example `true`.
103+
case bool(Bool)
104+
100105
/// The nil literal: `nil`.
101106
case `nil`
102107

Sources/_OpenAPIGeneratorCore/Renderer/TextBasedRenderer.swift

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,8 @@ struct TextBasedRenderer: RendererProtocol {
316316
return "\"\(string)\""
317317
case let .int(int):
318318
return "\(int)"
319+
case let .bool(bool):
320+
return bool ? "true" : "false"
319321
case .nil:
320322
return "nil"
321323
case .array(let items):

Sources/_OpenAPIGeneratorCore/Translator/CommonTypes/Constants.swift

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -299,6 +299,13 @@ enum Constants {
299299

300300
/// The name of the namespace.
301301
static let namespace: String = "Parameters"
302+
303+
/// Maps to `OpenAPIRuntime.ParameterStyle`.
304+
enum Style {
305+
306+
/// The form style.
307+
static let form = "form"
308+
}
302309
}
303310

304311
/// Constants related to the Headers namespace.

Sources/_OpenAPIGeneratorCore/Translator/Parameters/TypedParameter.swift

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,12 @@ struct TypedParameter {
2222
/// The underlying schema.
2323
var schema: UnresolvedSchema
2424

25+
/// The parameter serialization style.
26+
var style: OpenAPI.Parameter.SchemaContext.Style
27+
28+
/// The parameter explode value.
29+
var explode: Bool
30+
2531
/// The computed type usage.
2632
var typeUsage: TypeUsage
2733

@@ -134,9 +140,13 @@ extension FileTranslator {
134140

135141
let schema: UnresolvedSchema
136142
let codingStrategy: CodingStrategy
143+
let style: OpenAPI.Parameter.SchemaContext.Style
144+
let explode: Bool
137145
switch parameter.schemaOrContent {
138146
case let .a(schemaContext):
139147
schema = schemaContext.schema
148+
style = schemaContext.style
149+
explode = schemaContext.explode
140150
codingStrategy = .text
141151

142152
// Check supported exploded/style types
@@ -150,13 +160,6 @@ extension FileTranslator {
150160
)
151161
return nil
152162
}
153-
guard schemaContext.explode else {
154-
diagnostics.emitUnsupported(
155-
"Unexploded query params",
156-
foundIn: foundIn
157-
)
158-
return nil
159-
}
160163
case .header, .path:
161164
guard case .simple = schemaContext.style else {
162165
diagnostics.emitUnsupported(
@@ -189,6 +192,17 @@ extension FileTranslator {
189192
.content
190193
.contentType
191194
.codingStrategy
195+
196+
// Defaults are defined by the OpenAPI specification:
197+
// https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10
198+
switch parameter.location {
199+
case .query, .cookie:
200+
style = .form
201+
explode = true
202+
case .path, .header:
203+
style = .simple
204+
explode = false
205+
}
192206
}
193207

194208
// Check if the underlying schema is supported
@@ -221,6 +235,8 @@ extension FileTranslator {
221235
return .init(
222236
parameter: parameter,
223237
schema: schema,
238+
style: style,
239+
explode: explode,
224240
typeUsage: usage,
225241
codingStrategy: codingStrategy,
226242
asSwiftSafeName: swiftSafeName
@@ -271,3 +287,16 @@ extension OpenAPI.Parameter.Context.Location {
271287
}
272288
}
273289
}
290+
291+
extension OpenAPI.Parameter.SchemaContext.Style {
292+
293+
/// The runtime name for the style.
294+
var runtimeName: String {
295+
switch self {
296+
case .form:
297+
return Constants.Components.Parameters.Style.form
298+
default:
299+
preconditionFailure("Unsupported style")
300+
}
301+
}
302+
}

Sources/_OpenAPIGeneratorCore/Translator/Parameters/translateParameter.swift

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -116,20 +116,32 @@ extension ClientFileTranslator {
116116
) throws -> Expression? {
117117
let methodPrefix: String
118118
let containerExpr: Expression
119+
let supportsStyleAndExplode: Bool
119120
switch parameter.location {
120121
case .header:
121122
methodPrefix = "HeaderField"
122123
containerExpr = .identifier(requestVariableName).dot("headerFields")
124+
supportsStyleAndExplode = false
123125
case .query:
124126
methodPrefix = "QueryItem"
125127
containerExpr = .identifier(requestVariableName)
128+
supportsStyleAndExplode = true
126129
default:
127130
diagnostics.emitUnsupported(
128131
"Parameter of type \(parameter.location.rawValue)",
129132
foundIn: parameter.description
130133
)
131134
return nil
132135
}
136+
let styleAndExplodeArgs: [FunctionArgumentDescription]
137+
if supportsStyleAndExplode {
138+
styleAndExplodeArgs = [
139+
.init(label: "style", expression: .dot(parameter.style.runtimeName)),
140+
.init(label: "explode", expression: .literal(.bool(parameter.explode))),
141+
]
142+
} else {
143+
styleAndExplodeArgs = []
144+
}
133145
return .try(
134146
.identifier("converter")
135147
.dot("set\(methodPrefix)As\(parameter.codingStrategy.runtimeName)")
@@ -138,7 +150,8 @@ extension ClientFileTranslator {
138150
.init(
139151
label: "in",
140152
expression: .inOut(containerExpr)
141-
),
153+
)
154+
] + styleAndExplodeArgs + [
142155
.init(label: "name", expression: .literal(parameter.name)),
143156
.init(
144157
label: "value",
@@ -194,6 +207,8 @@ extension ServerFileTranslator {
194207
.identifier("converter").dot(methodName("QueryItem"))
195208
.call([
196209
.init(label: "in", expression: .identifier("metadata").dot("queryParameters")),
210+
.init(label: "style", expression: .dot(typedParameter.style.runtimeName)),
211+
.init(label: "explode", expression: .literal(.bool(typedParameter.explode))),
197212
.init(label: "name", expression: .literal(parameter.name)),
198213
.init(
199214
label: "as",

Sources/_OpenAPIGeneratorCore/Translator/TypesTranslator/TypesFileTranslator.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ struct TypesFileTranslator: FileTranslator {
4848
let components = try translateComponents(doc.components)
4949

5050
let operationDescriptions = try OperationDescription.all(
51-
from: parsedOpenAPI.paths,
51+
from: doc.paths,
5252
in: doc.components,
5353
asSwiftSafeName: swiftSafeName
5454
)

Sources/swift-openapi-generator/Documentation.docc/Articles/Supported-OpenAPI-features.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,7 +81,7 @@ Supported features are always provided on _both_ client and server.
8181
- [x] requestBody
8282
- [x] responses
8383
- [ ] callbacks
84-
- [ ] deprecated
84+
- [x] deprecated
8585
- [ ] security
8686
- [ ] servers
8787

@@ -162,7 +162,7 @@ Supported features are always provided on _both_ client and server.
162162
- [ ] xml
163163
- [ ] externalDocs
164164
- [ ] example
165-
- [ ] deprecated
165+
- [x] deprecated
166166

167167
#### External Documentation Object
168168

@@ -196,15 +196,15 @@ Supported features are always provided on _both_ client and server.
196196
- [x] in
197197
- [x] description
198198
- [x] required
199-
- [ ] deprecated
199+
- [x] deprecated
200200
- [ ] allowEmptyValue
201-
- [x] style (not all)
202-
- [x] explode (only explode: `true`)
201+
- [x] style (only defaults)
202+
- [x] explode (non default only for query items)
203203
- [ ] allowReserved
204204
- [x] schema
205205
- [ ] example
206206
- [ ] examples
207-
- [ ] content
207+
- [x] content (chooses one from the map)
208208

209209
#### Style Values
210210

@@ -223,7 +223,7 @@ Supported features are always provided on _both_ client and server.
223223

224224
Supported location + styles + exploded combinations:
225225
- path + simple + false
226-
- query + form + true
226+
- query + form + true/false
227227
- header + simple + false
228228

229229
#### Reference Object

Tests/OpenAPIGeneratorCoreTests/StructureHelpers.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -156,13 +156,15 @@ extension Declaration {
156156
extension LiteralDescription {
157157
var name: String {
158158
switch self {
159-
case .string(_):
159+
case .string:
160160
return "string"
161-
case .int(_):
161+
case .int:
162162
return "int"
163+
case .bool:
164+
return "bool"
163165
case .nil:
164166
return "nil"
165-
case .array(_):
167+
case .array:
166168
return "array"
167169
}
168170
}

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Client.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,22 @@ public struct Client: APIProtocol {
5252
suppressMutabilityWarning(&request)
5353
try converter.setQueryItemAsText(
5454
in: &request,
55+
style: .form,
56+
explode: true,
5557
name: "limit",
5658
value: input.query.limit
5759
)
5860
try converter.setQueryItemAsText(
5961
in: &request,
62+
style: .form,
63+
explode: true,
6064
name: "habitat",
6165
value: input.query.habitat
6266
)
6367
try converter.setQueryItemAsText(
6468
in: &request,
69+
style: .form,
70+
explode: true,
6571
name: "feeds",
6672
value: input.query.feeds
6773
)
@@ -72,6 +78,8 @@ public struct Client: APIProtocol {
7278
)
7379
try converter.setQueryItemAsText(
7480
in: &request,
81+
style: .form,
82+
explode: true,
7583
name: "since",
7684
value: input.query.since
7785
)

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore/Server.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,29 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
8787
let query: Operations.listPets.Input.Query = .init(
8888
limit: try converter.getOptionalQueryItemAsText(
8989
in: metadata.queryParameters,
90+
style: .form,
91+
explode: true,
9092
name: "limit",
9193
as: Swift.Int32.self
9294
),
9395
habitat: try converter.getOptionalQueryItemAsText(
9496
in: metadata.queryParameters,
97+
style: .form,
98+
explode: true,
9599
name: "habitat",
96100
as: Operations.listPets.Input.Query.habitatPayload.self
97101
),
98102
feeds: try converter.getOptionalQueryItemAsText(
99103
in: metadata.queryParameters,
104+
style: .form,
105+
explode: true,
100106
name: "feeds",
101107
as: Operations.listPets.Input.Query.feedsPayload.self
102108
),
103109
since: try converter.getOptionalQueryItemAsText(
104110
in: metadata.queryParameters,
111+
style: .form,
112+
explode: true,
105113
name: "since",
106114
as: Components.Parameters.query_born_since.self
107115
)

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Client.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,16 +52,22 @@ public struct Client: APIProtocol {
5252
suppressMutabilityWarning(&request)
5353
try converter.setQueryItemAsText(
5454
in: &request,
55+
style: .form,
56+
explode: true,
5557
name: "limit",
5658
value: input.query.limit
5759
)
5860
try converter.setQueryItemAsText(
5961
in: &request,
62+
style: .form,
63+
explode: true,
6064
name: "habitat",
6165
value: input.query.habitat
6266
)
6367
try converter.setQueryItemAsText(
6468
in: &request,
69+
style: .form,
70+
explode: true,
6571
name: "feeds",
6672
value: input.query.feeds
6773
)
@@ -72,6 +78,8 @@ public struct Client: APIProtocol {
7278
)
7379
try converter.setQueryItemAsText(
7480
in: &request,
81+
style: .form,
82+
explode: true,
7583
name: "since",
7684
value: input.query.since
7785
)

Tests/OpenAPIGeneratorReferenceTests/Resources/ReferenceSources/Petstore_FF_MultipleContentTypes/Server.swift

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,21 +87,29 @@ fileprivate extension UniversalServer where APIHandler: APIProtocol {
8787
let query: Operations.listPets.Input.Query = .init(
8888
limit: try converter.getOptionalQueryItemAsText(
8989
in: metadata.queryParameters,
90+
style: .form,
91+
explode: true,
9092
name: "limit",
9193
as: Swift.Int32.self
9294
),
9395
habitat: try converter.getOptionalQueryItemAsText(
9496
in: metadata.queryParameters,
97+
style: .form,
98+
explode: true,
9599
name: "habitat",
96100
as: Operations.listPets.Input.Query.habitatPayload.self
97101
),
98102
feeds: try converter.getOptionalQueryItemAsText(
99103
in: metadata.queryParameters,
104+
style: .form,
105+
explode: true,
100106
name: "feeds",
101107
as: Operations.listPets.Input.Query.feedsPayload.self
102108
),
103109
since: try converter.getOptionalQueryItemAsText(
104110
in: metadata.queryParameters,
111+
style: .form,
112+
explode: true,
105113
name: "since",
106114
as: Components.Parameters.query_born_since.self
107115
)

0 commit comments

Comments
 (0)