diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 2dd4d89c..0e6ed7a2 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -36,11 +36,15 @@ extension Converter { // | client | set | request query | text | string-convertible | both | setQueryItemAsText | public func setQueryItemAsText( 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 @@ -50,11 +54,15 @@ extension Converter { // | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | public func setQueryItemAsText( 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 @@ -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 @@ -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 diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index d709382d..0a0ac42b 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -74,11 +74,15 @@ public extension Converter { // | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText | func getOptionalQueryItemAsText( 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 @@ -88,11 +92,15 @@ public extension Converter { // | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText | func getRequiredQueryItemAsText( 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 @@ -102,11 +110,15 @@ public extension Converter { // | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText | func getOptionalQueryItemAsText( 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 @@ -116,11 +128,15 @@ public extension Converter { // | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | func getRequiredQueryItemAsText( 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 @@ -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 @@ -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 @@ -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 @@ -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 diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index f306d0f7..881aca62 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -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 @@ -259,6 +286,8 @@ extension Converter { func setQueryItem( in request: inout Request, + style: ParameterStyle?, + explode: Bool?, name: String, value: T?, convert: (T) throws -> String @@ -266,11 +295,22 @@ extension Converter { 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( in request: inout Request, + style: ParameterStyle?, + explode: Bool?, name: String, values: [T]?, convert: (T) throws -> String @@ -278,17 +318,36 @@ extension Converter { 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( 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 @@ -301,6 +360,8 @@ extension Converter { func getRequiredQueryItem( in queryParameters: [URLQueryItem], + style: ParameterStyle?, + explode: Bool?, name: String, as type: T.Type, convert: (String) throws -> T @@ -308,6 +369,8 @@ extension Converter { guard let value = try getOptionalQueryItem( in: queryParameters, + style: style, + explode: explode, name: name, as: type, convert: convert @@ -320,16 +383,40 @@ extension Converter { func getOptionalQueryItems( 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( in queryParameters: [URLQueryItem], + style: ParameterStyle?, + explode: Bool?, name: String, as type: [T].Type, convert: (String) throws -> T @@ -337,6 +424,8 @@ extension Converter { guard let values = try getOptionalQueryItems( in: queryParameters, + style: style, + explode: explode, name: name, as: type, convert: convert diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index 3d75380e..3590c853 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -36,10 +36,21 @@ extension Request { query = urlComponents.percentEncodedQuery } - /// Allows modifying the parsed query parameters of the request. - mutating func addQueryItem(name: String, value: String) { + /// Adds the provided name and value to the URL's query. + /// - Parameters: + /// - name: The name of the query item. + /// - value: The value of the query item. + /// - explode: A Boolean value indicating whether query items with the + /// same name should be provided as separate key-value pairs (`true`) or + /// if all the values for one key should be concatenated with a comma + /// and provided as a single key-value pair (`false`). + mutating func addQueryItem(name: String, value: String, explode: Bool) { mutateQuery { urlComponents in - urlComponents.addStringQueryItem(name: name, value: value) + urlComponents.addStringQueryItem( + name: name, + value: value, + explode: explode + ) } } } @@ -47,13 +58,46 @@ extension Request { extension URLComponents { /// Adds the provided name and value to the URL's query. + /// - Parameters: + /// - name: The name of the query item. + /// - value: The value of the query item. + /// - explode: A Boolean value indicating whether query items with the + /// same name should be provided as separate key-value pairs (`true`) or + /// if all the values for one key should be concatenated with a comma + /// and provided as a single key-value pair (`false`). mutating func addStringQueryItem( name: String, - value: String + value: String, + explode: Bool ) { - queryItems = - (queryItems ?? []) + [ - .init(name: name, value: value) - ] + if explode { + queryItems = + (queryItems ?? []) + [ + .init(name: name, value: value) + ] + return + } + // When explode is false, we need to collect all the potential existing + // values from the array with the same name, add the new one, and + // concatenate them with a comma. + let originalQueryItems = queryItems ?? [] + struct GroupedQueryItems { + var matchingValues: [String] = [] + var otherItems: [URLQueryItem] = [] + } + let groups = + originalQueryItems + .reduce(into: GroupedQueryItems()) { partialResult, item in + if item.name == name { + partialResult.matchingValues.append(item.value ?? "") + } else { + partialResult.otherItems.append(item) + } + } + let newItem = URLQueryItem( + name: name, + value: (groups.matchingValues + [value]).joined(separator: ",") + ) + queryItems = groups.otherItems + [newItem] } } diff --git a/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift new file mode 100644 index 00000000..13f3e8a7 --- /dev/null +++ b/Sources/OpenAPIRuntime/Conversion/ParameterStyles.swift @@ -0,0 +1,51 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// The serialization style used by a parameter. +/// +/// Details: https://github.com/OAI/OpenAPI-Specification/blob/main/versions/3.0.3.md#fixed-fields-10 +@_spi(Generated) +public enum ParameterStyle: Sendable { + + /// The form style. + /// + /// Details: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.8 + case form + + /// The simple style. + /// + /// Details: https://datatracker.ietf.org/doc/html/rfc6570#section-3.2.2 + case simple +} + +extension ParameterStyle { + + /// The default style for path parameters. + static let defaultForPathParameters: Self = .simple + + /// The default style for query items. + static let defaultForQueryItems: Self = .form + + /// The default style for query items. + static let defaultForHeaderFields: Self = .simple + + /// The default style for cookies. + static let defaultForCookies: Self = .form + + /// Returns the default value of the explode field for the given style + /// - Parameter style: The parameter style. + static func defaultExplodeFor(forStyle style: ParameterStyle) -> Bool { + style == .form + } +} diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 6ddef107..351347c8 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -975,4 +975,208 @@ extension Converter { let convertibleValue = body.value return try convert(convertibleValue) } + + // | client | set | request query | text | string-convertible | both | setQueryItemAsText | + @available(*, deprecated) + public func setQueryItemAsText( + in request: inout Request, + name: String, + value: T? + ) throws { + try setQueryItem( + in: &request, + style: nil, + explode: nil, + name: name, + value: value, + convert: convertStringConvertibleToText + ) + } + + // | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | + @available(*, deprecated) + public func setQueryItemAsText( + in request: inout Request, + name: String, + value: [T]? + ) throws { + try setQueryItems( + in: &request, + style: nil, + explode: nil, + name: name, + values: value, + convert: convertStringConvertibleToText + ) + } + + // | client | set | request query | text | date | both | setQueryItemAsText | + @available(*, deprecated) + public func setQueryItemAsText( + in request: inout Request, + name: String, + value: Date? + ) throws { + try setQueryItem( + in: &request, + style: nil, + explode: nil, + name: name, + value: value, + convert: convertDateToText + ) + } + + // | client | set | request query | text | array of dates | both | setQueryItemAsText | + @available(*, deprecated) + public func setQueryItemAsText( + in request: inout Request, + name: String, + value: [Date]? + ) throws { + try setQueryItems( + in: &request, + style: nil, + explode: nil, + name: name, + values: value, + convert: convertDateToText + ) + } + + // | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText | + @available(*, deprecated) + public func getOptionalQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: T.Type + ) throws -> T? { + try getOptionalQueryItem( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText | + @available(*, deprecated) + public func getRequiredQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: T.Type + ) throws -> T { + try getRequiredQueryItem( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText | + @available(*, deprecated) + public func getOptionalQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [T].Type + ) throws -> [T]? { + try getOptionalQueryItems( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | + @available(*, deprecated) + public func getRequiredQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [T].Type + ) throws -> [T] { + try getRequiredQueryItems( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | server | get | request query | text | date | optional | getOptionalQueryItemAsText | + @available(*, deprecated) + public func getOptionalQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: Date.Type + ) throws -> Date? { + try getOptionalQueryItem( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToDate + ) + } + + // | server | get | request query | text | date | required | getRequiredQueryItemAsText | + @available(*, deprecated) + public func getRequiredQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: Date.Type + ) throws -> Date { + try getRequiredQueryItem( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToDate + ) + } + + // | server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText | + @available(*, deprecated) + public func getOptionalQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [Date].Type + ) throws -> [Date]? { + try getOptionalQueryItems( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToDate + ) + } + + // | server | get | request query | text | array of dates | required | getRequiredQueryItemAsText | + @available(*, deprecated) + public func getRequiredQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [Date].Type + ) throws -> [Date] { + try getRequiredQueryItems( + in: queryParameters, + style: nil, + explode: nil, + name: name, + as: type, + convert: convertTextToDate + ) + } } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index d8a72841..46185e04 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -24,6 +24,15 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Data conversion case failedToDecodeStringConvertibleValue(type: String) + enum ParameterLocation: String, CustomStringConvertible { + case query + + var description: String { + rawValue + } + } + case unsupportedParameterStyle(name: String, location: ParameterLocation, style: ParameterStyle, explode: Bool) + // Headers case missingRequiredHeaderField(String) case unexpectedContentTypeHeader(String) @@ -56,6 +65,9 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Invalid expected content type: '\(string)'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." + case .unsupportedParameterStyle(name: let name, location: let location, style: let style, explode: let explode): + return + "Unsupported parameter style, parameter name: '\(name)', kind: \(location), style: \(style), explode: \(explode)" case .missingRequiredHeaderField(let name): return "The required header field named '\(name)' is missing." case .unexpectedContentTypeHeader(let contentType): diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index aefd7b30..8dd24f23 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -35,6 +35,8 @@ final class Test_ClientConverterExtensions: Test_Runtime { var request = testRequest try converter.setQueryItemAsText( in: &request, + style: nil, + explode: nil, name: "search", value: "foo" ) @@ -45,6 +47,8 @@ final class Test_ClientConverterExtensions: Test_Runtime { var request = testRequest try converter.setQueryItemAsText( in: &request, + style: nil, + explode: nil, name: "search", value: "h%llo" ) @@ -56,17 +60,34 @@ final class Test_ClientConverterExtensions: Test_Runtime { var request = testRequest try converter.setQueryItemAsText( in: &request, + style: nil, + explode: nil, name: "search", value: ["foo", "bar"] ) XCTAssertEqual(request.query, "search=foo&search=bar") } + // | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | + func test_setQueryItemAsText_arrayOfStringConvertibles_unexploded() throws { + var request = testRequest + try converter.setQueryItemAsText( + in: &request, + style: nil, + explode: false, + name: "search", + value: ["foo", "bar"] + ) + XCTAssertEqual(request.query, "search=foo,bar") + } + // | client | set | request query | text | date | both | setQueryItemAsText | func test_setQueryItemAsText_date() throws { var request = testRequest try converter.setQueryItemAsText( in: &request, + style: nil, + explode: nil, name: "search", value: testDate ) @@ -78,6 +99,8 @@ final class Test_ClientConverterExtensions: Test_Runtime { var request = testRequest try converter.setQueryItemAsText( in: &request, + style: nil, + explode: nil, name: "search", value: [testDate, testDate] ) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 03f80d8a..5436d9dc 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -123,6 +123,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: String? = try converter.getOptionalQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: String.self ) @@ -136,6 +138,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: String = try converter.getRequiredQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: String.self ) @@ -150,6 +154,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: [String]? = try converter.getOptionalQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: [String].self ) @@ -164,6 +170,23 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: [String] = try converter.getRequiredQueryItemAsText( in: query, + style: nil, + explode: nil, + name: "search", + as: [String].self + ) + XCTAssertEqual(value, ["foo", "bar"]) + } + + // | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | + func test_getRequiredQueryItemAsText_arrayOfStringConvertibles_unexploded() throws { + let query: [URLQueryItem] = [ + .init(name: "search", value: "foo,bar") + ] + let value: [String] = try converter.getRequiredQueryItemAsText( + in: query, + style: nil, + explode: false, name: "search", as: [String].self ) @@ -177,6 +200,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: Date? = try converter.getOptionalQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: Date.self ) @@ -190,6 +215,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: Date = try converter.getRequiredQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: Date.self ) @@ -204,6 +231,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: [Date]? = try converter.getOptionalQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: [Date].self ) @@ -218,6 +247,8 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] let value: [Date] = try converter.getRequiredQueryItemAsText( in: query, + style: nil, + explode: nil, name: "search", as: [Date].self ) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift index 4a6790fb..c5c4e68b 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift @@ -18,7 +18,23 @@ final class Test_FoundationExtensions: Test_Runtime { func testURLComponents_addStringQueryItem() throws { var components = testComponents - components.addStringQueryItem(name: "key", value: "value") + components.addStringQueryItem(name: "key", value: "value", explode: true) XCTAssertEqualURLString(components.url, "/api?key=value") } + + func testURLComponents_addStringQueryItems() throws { + var components = testComponents + components.addStringQueryItem(name: "key2", value: "value3", explode: true) + components.addStringQueryItem(name: "key", value: "value1", explode: true) + components.addStringQueryItem(name: "key", value: "value2", explode: true) + XCTAssertEqualURLString(components.url, "/api?key2=value3&key=value1&key=value2") + } + + func testURLComponents_addStringQueryItems_unexploded() throws { + var components = testComponents + components.addStringQueryItem(name: "key2", value: "value3", explode: false) + components.addStringQueryItem(name: "key", value: "value1", explode: false) + components.addStringQueryItem(name: "key", value: "value2", explode: false) + XCTAssertEqualURLString(components.url, "/api?key2=value3&key=value1,value2") + } }