Skip to content

Commit

Permalink
[Runtime] Support unexploded query items (#35)
Browse files Browse the repository at this point in the history
[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
  • Loading branch information
czechboy0 authored Aug 8, 2023
1 parent d79dbc9 commit b4c4ada
Show file tree
Hide file tree
Showing 10 changed files with 531 additions and 13 deletions.
16 changes: 16 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Client.swift
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,15 @@ extension Converter {
// | client | set | request query | text | string-convertible | both | setQueryItemAsText |
public func setQueryItemAsText<T: _StringConvertible>(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
value: T?
) throws {
try setQueryItem(
in: &request,
style: style,
explode: explode,
name: name,
value: value,
convert: convertStringConvertibleToText
Expand All @@ -50,11 +54,15 @@ extension Converter {
// | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText |
public func setQueryItemAsText<T: _StringConvertible>(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
value: [T]?
) throws {
try setQueryItems(
in: &request,
style: style,
explode: explode,
name: name,
values: value,
convert: convertStringConvertibleToText
Expand All @@ -64,11 +72,15 @@ extension Converter {
// | client | set | request query | text | date | both | setQueryItemAsText |
public func setQueryItemAsText(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
value: Date?
) throws {
try setQueryItem(
in: &request,
style: style,
explode: explode,
name: name,
value: value,
convert: convertDateToText
Expand All @@ -78,11 +90,15 @@ extension Converter {
// | client | set | request query | text | array of dates | both | setQueryItemAsText |
public func setQueryItemAsText(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
value: [Date]?
) throws {
try setQueryItems(
in: &request,
style: style,
explode: explode,
name: name,
values: value,
convert: convertDateToText
Expand Down
32 changes: 32 additions & 0 deletions Sources/OpenAPIRuntime/Conversion/Converter+Server.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,11 +74,15 @@ public extension Converter {
// | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText |
func getOptionalQueryItemAsText<T: _StringConvertible>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: T.Type
) throws -> T? {
try getOptionalQueryItem(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToStringConvertible
Expand All @@ -88,11 +92,15 @@ public extension Converter {
// | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText |
func getRequiredQueryItemAsText<T: _StringConvertible>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: T.Type
) throws -> T {
try getRequiredQueryItem(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToStringConvertible
Expand All @@ -102,11 +110,15 @@ public extension Converter {
// | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText |
func getOptionalQueryItemAsText<T: _StringConvertible>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [T].Type
) throws -> [T]? {
try getOptionalQueryItems(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToStringConvertible
Expand All @@ -116,11 +128,15 @@ public extension Converter {
// | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText |
func getRequiredQueryItemAsText<T: _StringConvertible>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [T].Type
) throws -> [T] {
try getRequiredQueryItems(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToStringConvertible
Expand All @@ -130,11 +146,15 @@ public extension Converter {
// | server | get | request query | text | date | optional | getOptionalQueryItemAsText |
func getOptionalQueryItemAsText(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: Date.Type
) throws -> Date? {
try getOptionalQueryItem(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToDate
Expand All @@ -144,11 +164,15 @@ public extension Converter {
// | server | get | request query | text | date | required | getRequiredQueryItemAsText |
func getRequiredQueryItemAsText(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: Date.Type
) throws -> Date {
try getRequiredQueryItem(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToDate
Expand All @@ -158,11 +182,15 @@ public extension Converter {
// | server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText |
func getOptionalQueryItemAsText(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [Date].Type
) throws -> [Date]? {
try getOptionalQueryItems(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToDate
Expand All @@ -172,11 +200,15 @@ public extension Converter {
// | server | get | request query | text | array of dates | required | getRequiredQueryItemAsText |
func getRequiredQueryItemAsText(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [Date].Type
) throws -> [Date] {
try getRequiredQueryItems(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convertTextToDate
Expand Down
97 changes: 93 additions & 4 deletions Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,33 @@ extension Array where Element == HeaderField {
}
}

extension ParameterStyle {

/// Returns the parameter style and explode parameter that should be used
/// based on the provided inputs, taking defaults into considerations.
/// - Parameters:
/// - style: The provided parameter style, if any.
/// - explode: The provided explode value, if any.
/// - Throws: For an unsupported input combination.
static func resolvedQueryStyleAndExplode(
name: String,
style: ParameterStyle?,
explode: Bool?
) throws -> (ParameterStyle, Bool) {
let resolvedStyle = style ?? .defaultForQueryItems
let resolvedExplode = explode ?? ParameterStyle.defaultExplodeFor(forStyle: resolvedStyle)
guard resolvedStyle == .form else {
throw RuntimeError.unsupportedParameterStyle(
name: name,
location: .query,
style: resolvedStyle,
explode: resolvedExplode
)
}
return (resolvedStyle, resolvedExplode)
}
}

extension Converter {

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

func setQueryItem<T>(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
value: T?,
convert: (T) throws -> String
) throws {
guard let value else {
return
}
request.addQueryItem(name: name, value: try convert(value))
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
name: name,
style: style,
explode: explode
)
request.addQueryItem(
name: name,
value: try convert(value),
explode: resolvedExplode
)
}

func setQueryItems<T>(
in request: inout Request,
style: ParameterStyle?,
explode: Bool?,
name: String,
values: [T]?,
convert: (T) throws -> String
) throws {
guard let values else {
return
}
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
name: name,
style: style,
explode: explode
)
for value in values {
request.addQueryItem(name: name, value: try convert(value))
request.addQueryItem(
name: name,
value: try convert(value),
explode: resolvedExplode
)
}
}

func getOptionalQueryItem<T>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: T.Type,
convert: (String) throws -> T
) throws -> T? {
// Even though the return value isn't used, the validation
// in the method is important for consistently handling
// style+explode combinations in all the helper functions.
let (_, _) = try ParameterStyle.resolvedQueryStyleAndExplode(
name: name,
style: style,
explode: explode
)
guard
let untypedValue =
queryParameters
Expand All @@ -301,13 +360,17 @@ extension Converter {

func getRequiredQueryItem<T>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: T.Type,
convert: (String) throws -> T
) throws -> T {
guard
let value = try getOptionalQueryItem(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convert
Expand All @@ -320,23 +383,49 @@ extension Converter {

func getOptionalQueryItems<T>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [T].Type,
convert: (String) throws -> T
) throws -> [T]? {
let untypedValues = queryParameters.filter { $0.name == name }
return try untypedValues.map { try convert($0.value ?? "") }
let (_, resolvedExplode) = try ParameterStyle.resolvedQueryStyleAndExplode(
name: name,
style: style,
explode: explode
)
let untypedValues =
queryParameters
.filter { $0.name == name }
.map { $0.value ?? "" }
// If explode is false, some of the items might have multiple
// comma-separate values, so we need to split them here.
let processedValues: [String]
if resolvedExplode {
processedValues = untypedValues
} else {
processedValues = untypedValues.flatMap { multiValue in
multiValue
.split(separator: ",", omittingEmptySubsequences: false)
.map(String.init)
}
}
return try processedValues.map(convert)
}

func getRequiredQueryItems<T>(
in queryParameters: [URLQueryItem],
style: ParameterStyle?,
explode: Bool?,
name: String,
as type: [T].Type,
convert: (String) throws -> T
) throws -> [T] {
guard
let values = try getOptionalQueryItems(
in: queryParameters,
style: style,
explode: explode,
name: name,
as: type,
convert: convert
Expand Down
Loading

0 comments on commit b4c4ada

Please sign in to comment.