Skip to content

Commit b4c4ada

Browse files
authored
[Runtime] Support unexploded query items (#35)
[Runtime] Support unexploded query items ### Motivation Fixes apple/swift-openapi-generator#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 Add support for unexploded query items by expanding the helper functions and allow passing in `style` and `explode` parameters. The `style` is in preparation of also supporting alternative styles, but for now we just validate that only the supported style is provided. ### Result Generated code can use these improved helper functions to encode/decode unexploded query items. ### Test Plan Updated/added unit tests for unexploded query items. Reviewed by: glbrntt Builds: ✔︎ pull request validation (5.8) - Build finished. ✔︎ pull request validation (5.9) - Build finished. ✔︎ pull request validation (api breakage) - 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. #35
1 parent d79dbc9 commit b4c4ada

10 files changed

+531
-13
lines changed

Sources/OpenAPIRuntime/Conversion/Converter+Client.swift

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,15 @@ extension Converter {
3636
// | client | set | request query | text | string-convertible | both | setQueryItemAsText |
3737
public func setQueryItemAsText<T: _StringConvertible>(
3838
in request: inout Request,
39+
style: ParameterStyle?,
40+
explode: Bool?,
3941
name: String,
4042
value: T?
4143
) throws {
4244
try setQueryItem(
4345
in: &request,
46+
style: style,
47+
explode: explode,
4448
name: name,
4549
value: value,
4650
convert: convertStringConvertibleToText
@@ -50,11 +54,15 @@ extension Converter {
5054
// | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText |
5155
public func setQueryItemAsText<T: _StringConvertible>(
5256
in request: inout Request,
57+
style: ParameterStyle?,
58+
explode: Bool?,
5359
name: String,
5460
value: [T]?
5561
) throws {
5662
try setQueryItems(
5763
in: &request,
64+
style: style,
65+
explode: explode,
5866
name: name,
5967
values: value,
6068
convert: convertStringConvertibleToText
@@ -64,11 +72,15 @@ extension Converter {
6472
// | client | set | request query | text | date | both | setQueryItemAsText |
6573
public func setQueryItemAsText(
6674
in request: inout Request,
75+
style: ParameterStyle?,
76+
explode: Bool?,
6777
name: String,
6878
value: Date?
6979
) throws {
7080
try setQueryItem(
7181
in: &request,
82+
style: style,
83+
explode: explode,
7284
name: name,
7385
value: value,
7486
convert: convertDateToText
@@ -78,11 +90,15 @@ extension Converter {
7890
// | client | set | request query | text | array of dates | both | setQueryItemAsText |
7991
public func setQueryItemAsText(
8092
in request: inout Request,
93+
style: ParameterStyle?,
94+
explode: Bool?,
8195
name: String,
8296
value: [Date]?
8397
) throws {
8498
try setQueryItems(
8599
in: &request,
100+
style: style,
101+
explode: explode,
86102
name: name,
87103
values: value,
88104
convert: convertDateToText

Sources/OpenAPIRuntime/Conversion/Converter+Server.swift

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,11 +74,15 @@ public extension Converter {
7474
// | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText |
7575
func getOptionalQueryItemAsText<T: _StringConvertible>(
7676
in queryParameters: [URLQueryItem],
77+
style: ParameterStyle?,
78+
explode: Bool?,
7779
name: String,
7880
as type: T.Type
7981
) throws -> T? {
8082
try getOptionalQueryItem(
8183
in: queryParameters,
84+
style: style,
85+
explode: explode,
8286
name: name,
8387
as: type,
8488
convert: convertTextToStringConvertible
@@ -88,11 +92,15 @@ public extension Converter {
8892
// | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText |
8993
func getRequiredQueryItemAsText<T: _StringConvertible>(
9094
in queryParameters: [URLQueryItem],
95+
style: ParameterStyle?,
96+
explode: Bool?,
9197
name: String,
9298
as type: T.Type
9399
) throws -> T {
94100
try getRequiredQueryItem(
95101
in: queryParameters,
102+
style: style,
103+
explode: explode,
96104
name: name,
97105
as: type,
98106
convert: convertTextToStringConvertible
@@ -102,11 +110,15 @@ public extension Converter {
102110
// | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText |
103111
func getOptionalQueryItemAsText<T: _StringConvertible>(
104112
in queryParameters: [URLQueryItem],
113+
style: ParameterStyle?,
114+
explode: Bool?,
105115
name: String,
106116
as type: [T].Type
107117
) throws -> [T]? {
108118
try getOptionalQueryItems(
109119
in: queryParameters,
120+
style: style,
121+
explode: explode,
110122
name: name,
111123
as: type,
112124
convert: convertTextToStringConvertible
@@ -116,11 +128,15 @@ public extension Converter {
116128
// | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText |
117129
func getRequiredQueryItemAsText<T: _StringConvertible>(
118130
in queryParameters: [URLQueryItem],
131+
style: ParameterStyle?,
132+
explode: Bool?,
119133
name: String,
120134
as type: [T].Type
121135
) throws -> [T] {
122136
try getRequiredQueryItems(
123137
in: queryParameters,
138+
style: style,
139+
explode: explode,
124140
name: name,
125141
as: type,
126142
convert: convertTextToStringConvertible
@@ -130,11 +146,15 @@ public extension Converter {
130146
// | server | get | request query | text | date | optional | getOptionalQueryItemAsText |
131147
func getOptionalQueryItemAsText(
132148
in queryParameters: [URLQueryItem],
149+
style: ParameterStyle?,
150+
explode: Bool?,
133151
name: String,
134152
as type: Date.Type
135153
) throws -> Date? {
136154
try getOptionalQueryItem(
137155
in: queryParameters,
156+
style: style,
157+
explode: explode,
138158
name: name,
139159
as: type,
140160
convert: convertTextToDate
@@ -144,11 +164,15 @@ public extension Converter {
144164
// | server | get | request query | text | date | required | getRequiredQueryItemAsText |
145165
func getRequiredQueryItemAsText(
146166
in queryParameters: [URLQueryItem],
167+
style: ParameterStyle?,
168+
explode: Bool?,
147169
name: String,
148170
as type: Date.Type
149171
) throws -> Date {
150172
try getRequiredQueryItem(
151173
in: queryParameters,
174+
style: style,
175+
explode: explode,
152176
name: name,
153177
as: type,
154178
convert: convertTextToDate
@@ -158,11 +182,15 @@ public extension Converter {
158182
// | server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText |
159183
func getOptionalQueryItemAsText(
160184
in queryParameters: [URLQueryItem],
185+
style: ParameterStyle?,
186+
explode: Bool?,
161187
name: String,
162188
as type: [Date].Type
163189
) throws -> [Date]? {
164190
try getOptionalQueryItems(
165191
in: queryParameters,
192+
style: style,
193+
explode: explode,
166194
name: name,
167195
as: type,
168196
convert: convertTextToDate
@@ -172,11 +200,15 @@ public extension Converter {
172200
// | server | get | request query | text | array of dates | required | getRequiredQueryItemAsText |
173201
func getRequiredQueryItemAsText(
174202
in queryParameters: [URLQueryItem],
203+
style: ParameterStyle?,
204+
explode: Bool?,
175205
name: String,
176206
as type: [Date].Type
177207
) throws -> [Date] {
178208
try getRequiredQueryItems(
179209
in: queryParameters,
210+
style: style,
211+
explode: explode,
180212
name: name,
181213
as: type,
182214
convert: convertTextToDate

Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift

Lines changed: 93 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,33 @@ extension Array where Element == HeaderField {
9393
}
9494
}
9595

96+
extension ParameterStyle {
97+
98+
/// Returns the parameter style and explode parameter that should be used
99+
/// based on the provided inputs, taking defaults into considerations.
100+
/// - Parameters:
101+
/// - style: The provided parameter style, if any.
102+
/// - explode: The provided explode value, if any.
103+
/// - Throws: For an unsupported input combination.
104+
static func resolvedQueryStyleAndExplode(
105+
name: String,
106+
style: ParameterStyle?,
107+
explode: Bool?
108+
) throws -> (ParameterStyle, Bool) {
109+
let resolvedStyle = style ?? .defaultForQueryItems
110+
let resolvedExplode = explode ?? ParameterStyle.defaultExplodeFor(forStyle: resolvedStyle)
111+
guard resolvedStyle == .form else {
112+
throw RuntimeError.unsupportedParameterStyle(
113+
name: name,
114+
location: .query,
115+
style: resolvedStyle,
116+
explode: resolvedExplode
117+
)
118+
}
119+
return (resolvedStyle, resolvedExplode)
120+
}
121+
}
122+
96123
extension Converter {
97124

98125
// MARK: Common functions for Converter's SPI helper methods
@@ -259,36 +286,68 @@ extension Converter {
259286

260287
func setQueryItem<T>(
261288
in request: inout Request,
289+
style: ParameterStyle?,
290+
explode: Bool?,
262291
name: String,
263292
value: T?,
264293
convert: (T) throws -> String
265294
) throws {
266295
guard let value else {
267296
return
268297
}
269-
request.addQueryItem(name: name, value: try convert(value))
298+
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
299+
name: name,
300+
style: style,
301+
explode: explode
302+
)
303+
request.addQueryItem(
304+
name: name,
305+
value: try convert(value),
306+
explode: resolvedExplode
307+
)
270308
}
271309

272310
func setQueryItems<T>(
273311
in request: inout Request,
312+
style: ParameterStyle?,
313+
explode: Bool?,
274314
name: String,
275315
values: [T]?,
276316
convert: (T) throws -> String
277317
) throws {
278318
guard let values else {
279319
return
280320
}
321+
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
322+
name: name,
323+
style: style,
324+
explode: explode
325+
)
281326
for value in values {
282-
request.addQueryItem(name: name, value: try convert(value))
327+
request.addQueryItem(
328+
name: name,
329+
value: try convert(value),
330+
explode: resolvedExplode
331+
)
283332
}
284333
}
285334

286335
func getOptionalQueryItem<T>(
287336
in queryParameters: [URLQueryItem],
337+
style: ParameterStyle?,
338+
explode: Bool?,
288339
name: String,
289340
as type: T.Type,
290341
convert: (String) throws -> T
291342
) throws -> T? {
343+
// Even though the return value isn't used, the validation
344+
// in the method is important for consistently handling
345+
// style+explode combinations in all the helper functions.
346+
let (_, _) = try ParameterStyle.resolvedQueryStyleAndExplode(
347+
name: name,
348+
style: style,
349+
explode: explode
350+
)
292351
guard
293352
let untypedValue =
294353
queryParameters
@@ -301,13 +360,17 @@ extension Converter {
301360

302361
func getRequiredQueryItem<T>(
303362
in queryParameters: [URLQueryItem],
363+
style: ParameterStyle?,
364+
explode: Bool?,
304365
name: String,
305366
as type: T.Type,
306367
convert: (String) throws -> T
307368
) throws -> T {
308369
guard
309370
let value = try getOptionalQueryItem(
310371
in: queryParameters,
372+
style: style,
373+
explode: explode,
311374
name: name,
312375
as: type,
313376
convert: convert
@@ -320,23 +383,49 @@ extension Converter {
320383

321384
func getOptionalQueryItems<T>(
322385
in queryParameters: [URLQueryItem],
386+
style: ParameterStyle?,
387+
explode: Bool?,
323388
name: String,
324389
as type: [T].Type,
325390
convert: (String) throws -> T
326391
) throws -> [T]? {
327-
let untypedValues = queryParameters.filter { $0.name == name }
328-
return try untypedValues.map { try convert($0.value ?? "") }
392+
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
393+
name: name,
394+
style: style,
395+
explode: explode
396+
)
397+
let untypedValues =
398+
queryParameters
399+
.filter { $0.name == name }
400+
.map { $0.value ?? "" }
401+
// If explode is false, some of the items might have multiple
402+
// comma-separate values, so we need to split them here.
403+
let processedValues: [String]
404+
if resolvedExplode {
405+
processedValues = untypedValues
406+
} else {
407+
processedValues = untypedValues.flatMap { multiValue in
408+
multiValue
409+
.split(separator: ",", omittingEmptySubsequences: false)
410+
.map(String.init)
411+
}
412+
}
413+
return try processedValues.map(convert)
329414
}
330415

331416
func getRequiredQueryItems<T>(
332417
in queryParameters: [URLQueryItem],
418+
style: ParameterStyle?,
419+
explode: Bool?,
333420
name: String,
334421
as type: [T].Type,
335422
convert: (String) throws -> T
336423
) throws -> [T] {
337424
guard
338425
let values = try getOptionalQueryItems(
339426
in: queryParameters,
427+
style: style,
428+
explode: explode,
340429
name: name,
341430
as: type,
342431
convert: convert

0 commit comments

Comments
 (0)