From f9661b992d11631cb6c61492d46a19111170d715 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Wed, 31 May 2023 10:04:14 +0200 Subject: [PATCH 1/9] [Runtime] Choose the serialization method based on content type --- .../Base/EncodableBodyContent.swift | 16 +- .../Conversion/CodingStrategy.swift | 50 ++++ .../Conversion/Converter+Client.swift | 19 +- .../Conversion/Converter+Common.swift | 39 ++- .../Conversion/Converter+Server.swift | 35 ++- .../Deprecated/Deprecated.swift | 262 ++++++++++++++++++ .../OpenAPIRuntime/Errors/RuntimeError.swift | 12 +- .../Conversion/Test_Converter+Client.swift | 205 +++++++++++++- .../Conversion/Test_Converter+Common.swift | 109 ++++++-- .../Conversion/Test_Converter+Server.swift | 145 +++++++++- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 10 +- 11 files changed, 836 insertions(+), 66 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift index 37717c8b..c1b50159 100644 --- a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift +++ b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift @@ -22,9 +22,21 @@ public struct EncodableBodyContent: Equatable { /// The header value of the content type, for example `application/json`. public var contentType: String - /// Creates a new content wrapper around the specified value and content type. - public init(value: T, contentType: String) { + /// A hint about which coding strategy to use. + public var strategy: CodingStrategy + + /// Creates a new content wrapper. + /// - Parameters: + /// - value: An encodable body value. + /// - contentType: The header value of the content type. + /// - strategy: A hint about which coding strategy to use. + public init( + value: T, + contentType: String, + strategy: CodingStrategy + ) { self.value = value self.contentType = contentType + self.strategy = strategy } } diff --git a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift new file mode 100644 index 00000000..7da60c5f --- /dev/null +++ b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift @@ -0,0 +1,50 @@ +//===----------------------------------------------------------------------===// +// +// 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 +// +//===----------------------------------------------------------------------===// + +/// A hint to the data encoding and decoding logic. +/// +/// Derived from the content type. +@_spi(Generated) +public struct CodingStrategy: Equatable, Hashable, Sendable { + + /// Describes the underlying coding strategy. + private enum _Strategy: String, Equatable, Hashable, Sendable { + + /// A strategy using JSONEncoder/JSONDecoder. + case codable + + /// A strategy using LosslessStringConvertible. + case string + + /// A strategy for letting the type choose the appropriate option. + case deferredToType + } + + private let strategy: _Strategy + + /// A strategy using JSONEncoder/JSONDecoder. + public static var codable: Self { + .init(strategy: .codable) + } + + /// A strategy using LosslessStringConvertible. + public static var string: Self { + .init(strategy: .string) + } + + /// A strategy for letting the type choose the appropriate option. + public static var deferredToType: Self { + .init(strategy: .deferredToType) + } +} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b9dc0667..8e7a481d 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -83,20 +83,25 @@ extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. - /// - transform: Closure for transforming the Decodable type into a final type. + /// - strategy: A hint about which coding strategy to use. + /// - transform: Closure for transforming the Decodable type into a final + /// type. /// - Returns: Deserialized body value. public func bodyGet( _ type: T.Type, from data: Data, + strategy: CodingStrategy, transforming transform: (T) -> C ) throws -> C { let decoded: T - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringParameterConvertible.Type, + strategy == .string + { guard let stringValue = String(data: data, encoding: .utf8), let decodedValue = myType.init(stringValue) else { - throw RuntimeError.failedToDecodePrimitiveBodyFromData + throw RuntimeError.failedToDecodeBody(type: T.self) } decoded = decodedValue as! T } else { @@ -139,9 +144,11 @@ extension Converter { ) throws -> Data { let body = transform(value) headerFields.add(name: "content-type", value: body.contentType) - if let value = value as? _StringParameterConvertible { + if let value = value as? _StringParameterConvertible, + body.strategy == .string + { guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodePrimitiveBodyIntoData + throw RuntimeError.failedToEncodeBody(type: T.self) } return data } @@ -154,11 +161,13 @@ extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. + /// - strategy: A hint about which coding strategy to use. /// - transform: Closure for transforming the Decodable type into a final type. /// - Returns: Deserialized body value. public func bodyGet( _ type: Data.Type, from data: Data, + strategy: CodingStrategy, transforming transform: (Data) -> C ) throws -> C { return transform(data) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 2df14252..bf5b1d42 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -55,7 +55,7 @@ public extension Array where Element == HeaderField { } } -public extension Converter { +extension Converter { // MARK: Miscs @@ -68,13 +68,14 @@ public extension Converter { /// - headerFields: Header fields to inspect for a content type. /// - substring: Expected content type. /// - Throws: If the response's Content-Type value is not compatible with the provided substring. - func validateContentTypeIfPresent( + public func validateContentTypeIfPresent( in headerFields: [HeaderField], substring: String ) throws { guard let contentType = try headerFieldGetOptional( in: headerFields, + strategy: .string, name: "content-type", as: String.self ) @@ -91,10 +92,12 @@ public extension Converter { /// Adds a header field with the provided name and Date value. /// - Parameters: /// - headerFields: Collection of header fields to add to. + /// - strategy: A hint about which coding strategy to use. /// - name: The name of the header field. /// - value: Date value. If nil, header is not added. - func headerFieldAdd( + public func headerFieldAdd( in headerFields: inout [HeaderField], + strategy: CodingStrategy, name: String, value: Date? ) throws { @@ -108,11 +111,13 @@ public extension Converter { /// Returns the value for the first header field with given name. /// - Parameters: /// - headerFields: Collection of header fields to retrieve the field from. + /// - strategy: A hint about which coding strategy to use. /// - name: The name of the header field (case-insensitive). /// - type: Date type. /// - Returns: First value for the given name, if one exists. - func headerFieldGetOptional( + public func headerFieldGetOptional( in headerFields: [HeaderField], + strategy: CodingStrategy, name: String, as type: Date.Type ) throws -> Date? { @@ -125,17 +130,20 @@ public extension Converter { /// Returns the value for the first header field with the given name. /// - Parameters: /// - headerFields: Collection of header fields to retrieve the field from. + /// - strategy: A hint about which coding strategy to use. /// - name: Header name (case-insensitive). /// - type: Date type. /// - Returns: First value for the given name. - func headerFieldGetRequired( + public func headerFieldGetRequired( in headerFields: [HeaderField], + strategy: CodingStrategy, name: String, as type: Date.Type ) throws -> Date { guard let value = try headerFieldGetOptional( in: headerFields, + strategy: strategy, name: name, as: type ) @@ -152,17 +160,21 @@ public extension Converter { /// Encodes the value into minimized JSON. /// - Parameters: /// - headerFields: Collection of header fields to add to. + /// - strategy: A hint about which coding strategy to use. /// - name: Header name. /// - value: Encodable header value. - func headerFieldAdd( + public func headerFieldAdd( in headerFields: inout [HeaderField], + strategy: CodingStrategy, name: String, value: T? ) throws { guard let value else { return } - if let value = value as? _StringParameterConvertible { + if let value = value as? _StringParameterConvertible, + strategy != .codable + { headerFields.add(name: name, value: value.description) return } @@ -178,18 +190,22 @@ public extension Converter { /// Decodes the value from JSON. /// - Parameters: /// - headerFields: Collection of header fields to retrieve the field from. + /// - strategy: A hint about which coding strategy to use. /// - name: Header name (case-insensitive). /// - type: Date type. /// - Returns: First value for the given name, if one exists. - func headerFieldGetOptional( + public func headerFieldGetOptional( in headerFields: [HeaderField], + strategy: CodingStrategy, name: String, as type: T.Type ) throws -> T? { guard let stringValue = headerFields.firstValue(name: name) else { return nil } - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringParameterConvertible.Type, + strategy != .codable + { return myType.init(stringValue).map { $0 as! T } } let data = Data(stringValue.utf8) @@ -201,17 +217,20 @@ public extension Converter { /// Decodes the value from JSON. /// - Parameters: /// - headerFields: Collection of header fields to retrieve the field from. + /// - strategy: A hint about which coding strategy to use. /// - name: Header name (case-insensitive). /// - type: Date type. /// - Returns: First value for the given name. - func headerFieldGetRequired( + public func headerFieldGetRequired( in headerFields: [HeaderField], + strategy: CodingStrategy, name: String, as type: T.Type ) throws -> T { guard let value = try headerFieldGetOptional( in: headerFields, + strategy: strategy, name: name, as: type ) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index d2467fde..d55eedbd 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -252,23 +252,27 @@ public extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. + /// - strategy: A hint about which coding strategy to use. /// - transform: Closure for transforming the Decodable type into a final type. /// - Returns: Deserialized body value, if present. func bodyGetOptional( _ type: T.Type, from data: Data?, + strategy: CodingStrategy, transforming transform: (T) -> C ) throws -> C? { guard let data else { return nil } let decoded: T - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringParameterConvertible.Type, + strategy == .string + { guard let stringValue = String(data: data, encoding: .utf8), let decodedValue = myType.init(stringValue) else { - throw RuntimeError.failedToDecodePrimitiveBodyFromData + throw RuntimeError.failedToDecodeBody(type: T.self) } decoded = decodedValue as! T } else { @@ -281,17 +285,32 @@ public extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. + /// - strategy: A hint about which coding strategy to use. /// - transform: Closure for transforming the Decodable type into a final type. /// - Returns: Deserialized body value. func bodyGetRequired( _ type: T.Type, from data: Data?, + strategy: CodingStrategy, transforming transform: (T) -> C ) throws -> C { guard let data else { throw RuntimeError.missingRequiredRequestBody } - let decoded = try decoder.decode(type, from: data) + let decoded: T + if let myType = T.self as? _StringParameterConvertible.Type, + strategy == .string + { + guard + let stringValue = String(data: data, encoding: .utf8), + let decodedValue = myType.init(stringValue) + else { + throw RuntimeError.failedToDecodeBody(type: T.self) + } + decoded = decodedValue as! T + } else { + decoded = try decoder.decode(type, from: data) + } return transform(decoded) } @@ -309,9 +328,11 @@ public extension Converter { let body = transform(value) headerFields.add(name: "content-type", value: body.contentType) let bodyValue = body.value - if let value = bodyValue as? _StringParameterConvertible { + if let value = bodyValue as? _StringParameterConvertible, + body.strategy == .string + { guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodePrimitiveBodyIntoData + throw RuntimeError.failedToEncodeBody(type: T.self) } return data } @@ -324,11 +345,13 @@ public extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. + /// - strategy: A hint about which coding strategy to use. /// - transform: Closure for transforming the Decodable type into a final type. /// - Returns: Deserialized body value, if present. func bodyGetOptional( _ type: Data.Type, from data: Data?, + strategy: CodingStrategy, transforming transform: (Data) -> C ) throws -> C? { guard let data else { @@ -341,11 +364,13 @@ public extension Converter { /// - Parameters: /// - type: Type used to decode the data. /// - data: Encoded body data. + /// - strategy: A hint about which coding strategy to use. /// - transform: Closure for transforming the Decodable type into a final type. /// - Returns: Deserialized body value. func bodyGetRequired( _ type: Data.Type, from data: Data?, + strategy: CodingStrategy, transforming transform: (Data) -> C ) throws -> C { guard let data else { diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index cb20c7e6..08831807 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -14,3 +14,265 @@ import Foundation // MARK: - Functionality to be removed in the future + +extension Converter { + /// Gets a deserialized value from body data. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value. + @available(*, deprecated, renamed: "bodyGet(_:from:strategy:transforming:)") + public func bodyGet( + _ type: T.Type, + from data: Data, + transforming transform: (T) -> C + ) throws -> C { + let decoded = try decoder.decode(type, from: data) + return transform(decoded) + } + + /// Gets a deserialized value from body data. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value. + @available(*, deprecated, renamed: "bodyGet(_:from:strategy:transforming:)") + public func bodyGet( + _ type: Data.Type, + from data: Data, + transforming transform: (Data) -> C + ) throws -> C { + return transform(data) + } + + /// Gets a deserialized value from body data, if present. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value, if present. + @available(*, deprecated, renamed: "bodyGetOptional(_:from:strategy:transforming:)") + public func bodyGetOptional( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C + ) throws -> C? { + guard let data else { + return nil + } + let decoded = try decoder.decode(type, from: data) + return transform(decoded) + } + + /// Gets a deserialized value from body data. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value. + @available(*, deprecated, renamed: "bodyGetRequired(_:from:strategy:transforming:)") + public func bodyGetRequired( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C + ) throws -> C { + guard let data else { + throw RuntimeError.missingRequiredRequestBody + } + let decoded = try decoder.decode(type, from: data) + return transform(decoded) + } + + /// Gets a deserialized value from body data, if present. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value, if present. + @available(*, deprecated, renamed: "bodyGetOptional(_:from:strategy:transforming:)") + public func bodyGetOptional( + _ type: Data.Type, + from data: Data?, + transforming transform: (Data) -> C + ) throws -> C? { + guard let data else { + return nil + } + return transform(data) + } + + /// Gets a deserialized value from body data. + /// - Parameters: + /// - type: Type used to decode the data. + /// - data: Encoded body data. + /// - transform: Closure for transforming the Decodable type into a final type. + /// - Returns: Deserialized body value. + @available(*, deprecated, renamed: "bodyGetRequired(_:from:strategy:transforming:)") + public func bodyGetRequired( + _ type: Data.Type, + from data: Data?, + transforming transform: (Data) -> C + ) throws -> C { + guard let data else { + throw RuntimeError.missingRequiredRequestBody + } + return transform(data) + } + + /// Adds a header field with the provided name and Date value. + /// - Parameters: + /// - headerFields: Collection of header fields to add to. + /// - name: The name of the header field. + /// - value: Date value. If nil, header is not added. + @available(*, deprecated, renamed: "headerFieldAdd(in:strategy:name:value:)") + public func headerFieldAdd( + in headerFields: inout [HeaderField], + name: String, + value: Date? + ) throws { + guard let value = value else { + return + } + let stringValue = try self.configuration.dateTranscoder.encode(value) + headerFields.add(name: name, value: stringValue) + } + + /// Returns the value for the first header field with given name. + /// - Parameters: + /// - headerFields: Collection of header fields to retrieve the field from. + /// - name: The name of the header field (case-insensitive). + /// - type: Date type. + /// - Returns: First value for the given name, if one exists. + @available(*, deprecated, renamed: "headerFieldGetOptional(in:strategy:name:as:)") + public func headerFieldGetOptional( + in headerFields: [HeaderField], + name: String, + as type: Date.Type + ) throws -> Date? { + guard let dateString = headerFields.firstValue(name: name) else { + return nil + } + return try self.configuration.dateTranscoder.decode(dateString) + } + + /// Returns the value for the first header field with the given name. + /// - Parameters: + /// - headerFields: Collection of header fields to retrieve the field from. + /// - name: Header name (case-insensitive). + /// - type: Date type. + /// - Returns: First value for the given name. + @available(*, deprecated, renamed: "headerFieldGetRequired(in:strategy:name:as:)") + public func headerFieldGetRequired( + in headerFields: [HeaderField], + name: String, + as type: Date.Type + ) throws -> Date { + guard + let value = try headerFieldGetOptional( + in: headerFields, + name: name, + as: type + ) + else { + throw RuntimeError.missingRequiredHeader(name) + } + return value + } + + /// Adds a header field with the provided name and encodable value. + /// + /// Encodes the value into minimized JSON. + /// - Parameters: + /// - headerFields: Collection of header fields to add to. + /// - name: Header name. + /// - value: Encodable header value. + @available(*, deprecated, renamed: "headerFieldAdd(in:strategy:name:value:)") + public func headerFieldAdd( + in headerFields: inout [HeaderField], + name: String, + value: T? + ) throws { + guard let value else { + return + } + if let value = value as? _StringParameterConvertible { + headerFields.add(name: name, value: value.description) + return + } + let data = try headerFieldEncoder.encode(value) + guard let stringValue = String(data: data, encoding: .utf8) else { + throw RuntimeError.failedToEncodeJSONHeaderIntoString(name: name) + } + headerFields.add(name: name, value: stringValue) + } + + /// Returns the value of the first header field for the given name. + /// + /// Decodes the value from JSON. + /// - Parameters: + /// - headerFields: Collection of header fields to retrieve the field from. + /// - name: Header name (case-insensitive). + /// - type: Date type. + /// - Returns: First value for the given name, if one exists. + @available(*, deprecated, renamed: "headerFieldGetOptional(in:strategy:name:as:)") + public func headerFieldGetOptional( + in headerFields: [HeaderField], + name: String, + as type: T.Type + ) throws -> T? { + guard let stringValue = headerFields.firstValue(name: name) else { + return nil + } + if let myType = T.self as? _StringParameterConvertible.Type { + return myType.init(stringValue).map { $0 as! T } + } + let data = Data(stringValue.utf8) + return try decoder.decode(T.self, from: data) + } + + /// Returns the first header value for the given (case-insensitive) name. + /// + /// Decodes the value from JSON. + /// - Parameters: + /// - headerFields: Collection of header fields to retrieve the field from. + /// - name: Header name (case-insensitive). + /// - type: Date type. + /// - Returns: First value for the given name. + @available(*, deprecated, renamed: "headerFieldGetRequired(in:strategy:name:as:)") + public func headerFieldGetRequired( + in headerFields: [HeaderField], + name: String, + as type: T.Type + ) throws -> T { + guard + let value = try headerFieldGetOptional( + in: headerFields, + name: name, + as: type + ) + else { + throw RuntimeError.missingRequiredHeader(name) + } + return value + } +} + +extension EncodableBodyContent { + /// Creates a new content wrapper. + /// - Parameters: + /// - value: An encodable body value. + /// - contentType: The header value of the content type. + @available(*, deprecated, renamed: "init(value:contentType:strategy:)") + public init( + value: T, + contentType: String + ) { + self.init( + value: value, + contentType: contentType, + strategy: .deferredToType + ) + } +} diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 67b90ed7..6cb7ddde 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -36,8 +36,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Body case missingRequiredRequestBody - case failedToEncodePrimitiveBodyIntoData - case failedToDecodePrimitiveBodyFromData + case failedToEncodeBody(type: Any.Type) + case failedToDecodeBody(type: Any.Type) // Transport/Handler case transportFailed(Error) @@ -71,10 +71,10 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret return "Failed to decode query parameter named '\(name)' to type \(type)." case .missingRequiredRequestBody: return "Missing required request body" - case .failedToEncodePrimitiveBodyIntoData: - return "Failed to encode a primitive body into data" - case .failedToDecodePrimitiveBodyFromData: - return "Failed to decode a primitive body from data" + case .failedToEncodeBody(type: let type): + return "Failed to encode a body of type \(type) into data" + case .failedToDecodeBody(type: let type): + return "Failed to decode a body of type \(type) from data" case .transportFailed(let underlyingError): return "Transport failed with error: \(underlyingError.localizedDescription)" case .handlerFailed(let underlyingError): diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index d0696ef2..a4d33dd8 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -12,7 +12,7 @@ // //===----------------------------------------------------------------------===// import XCTest -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated)@testable import OpenAPIRuntime final class Test_ClientConverterExtensions: Test_Runtime { @@ -64,39 +64,90 @@ final class Test_ClientConverterExtensions: Test_Runtime { // MARK: Body - func testBodyGetComplex_success() throws { + func testBodyGetStruct_strategyCodable_success() throws { let body = try converter.bodyGet( TestPet.self, from: testStructData, + strategy: .codable, transforming: { $0 } ) XCTAssertEqual(body, testStruct) } - func testBodyGetData_success() throws { + func testBodyGetData_strategyDeferredToType_success() throws { let body = try converter.bodyGet( Data.self, from: testStructData, + strategy: .deferredToType, transforming: { $0 } ) XCTAssertEqual(body, testStructData) } - func testBodyGetString_success() throws { + func testBodyGetString_strategyDeferredToType_success() throws { + let body = try converter.bodyGet( + String.self, + from: testQuotedStringData, + strategy: .deferredToType, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetString_strategyCodable_success() throws { + let body = try converter.bodyGet( + String.self, + from: testQuotedStringData, + strategy: .codable, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetString_strategyString_success() throws { let body = try converter.bodyGet( String.self, from: testStringData, + strategy: .string, transforming: { $0 } ) XCTAssertEqual(body, testString) } + func testBodyGetString_strategyInt_failure() throws { + XCTAssertThrowsError( + try converter.bodyGet( + Int.self, + from: testStringData, + strategy: .string, + transforming: { $0 } + ), + "Was expected to throw error on invalid int", + { error in + guard + let err = error as? RuntimeError, + case .failedToDecodeBody(let type) = err + else { + XCTFail("Unexpected kind of error thrown") + return + } + XCTAssertEqual("\(type)", "\(Int.self)") + } + ) + } + func testBodyAddComplexOptional_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddOptional( testStruct, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/json") } + transforming: { + .init( + value: $0, + contentType: "application/json", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -124,7 +175,13 @@ final class Test_ClientConverterExtensions: Test_Runtime { let data = try converter.bodyAddRequired( testStruct, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/json") } + transforming: { + .init( + value: $0, + contentType: "application/json", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -140,7 +197,13 @@ final class Test_ClientConverterExtensions: Test_Runtime { let data = try converter.bodyAddOptional( testStructPrettyData, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { + .init( + value: $0, + contentType: "application/octet-stream", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -151,12 +214,30 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } + func testBodyAddDataOptional_nil() throws { + let value: Data? = nil + var headerFields: [HeaderField] = [] + let data = try converter.bodyAddOptional( + value, + headerFields: &headerFields, + transforming: { _ -> EncodableBodyContent in fatalError("Unreachable") } + ) + XCTAssertNil(data) + XCTAssertEqual(headerFields, []) + } + func testBodyAddDataRequired_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddRequired( testStructPrettyData, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { + .init( + value: $0, + contentType: "application/octet-stream", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -167,12 +248,40 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - func testBodyAddStringOptional_success() throws { + func testBodyAddStringOptional_strategyDeferredToType_success() throws { + var headerFields: [HeaderField] = [] + let data = try converter.bodyAddOptional( + testString, + headerFields: &headerFields, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .deferredToType + ) + } + ) + XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + func testBodyAddStringOptional_strategyString_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddOptional( testString, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "text/plain") } + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .string + ) + } ) XCTAssertEqual(data, testStringData) XCTAssertEqual( @@ -183,12 +292,62 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - func testBodyAddStringRequired_success() throws { + func testBodyAddStringOptional_strategyCodable_success() throws { + var headerFields: [HeaderField] = [] + let data = try converter.bodyAddOptional( + testString, + headerFields: &headerFields, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .codable + ) + } + ) + XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + func testBodyAddStringRequired_strategyDeferredToType_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddRequired( testString, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "text/plain") } + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .deferredToType + ) + } + ) + XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + func testBodyAddStringRequired_strategyString_success() throws { + var headerFields: [HeaderField] = [] + let data = try converter.bodyAddRequired( + testString, + headerFields: &headerFields, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .string + ) + } ) XCTAssertEqual(data, testStringData) XCTAssertEqual( @@ -198,4 +357,26 @@ final class Test_ClientConverterExtensions: Test_Runtime { ] ) } + + func testBodyAddStringRequired_strategyCodable_success() throws { + var headerFields: [HeaderField] = [] + let data = try converter.bodyAddRequired( + testString, + headerFields: &headerFields, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .codable + ) + } + ) + XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 9f60f4e4..ced9bf5e 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -145,12 +145,27 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - // MARK: [HeaderField] - _StringParameterConvertible + func testHeaderFieldsAdd_string_strategyDeferredToType() throws { + var headerFields: [HeaderField] = [] + try converter.headerFieldAdd( + in: &headerFields, + strategy: .deferredToType, + name: "foo", + value: "bar" + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "bar") + ] + ) + } - func testHeaderAdd_string() throws { + func testHeaderFieldsAdd_string_strategyString() throws { var headerFields: [HeaderField] = [] try converter.headerFieldAdd( in: &headerFields, + strategy: .string, name: "foo", value: "bar" ) @@ -162,45 +177,91 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderGetOptional_string() throws { + func testHeaderFieldsAdd_string_strategyCodable() throws { + var headerFields: [HeaderField] = [] + try converter.headerFieldAdd( + in: &headerFields, + strategy: .codable, + name: "foo", + value: "bar" + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "\"bar\"") + ] + ) + } + + func testHeaderFieldsGetOptional_string_strategyDeferredToType() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: "bar") + ] + let value = try converter.headerFieldGetOptional( + in: headerFields, + strategy: .deferredToType, + name: "foo", + as: String.self + ) + XCTAssertEqual(value, "bar") + } + + func testHeaderFieldsGetOptional_string_strategyString() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: "bar") + ] + let value = try converter.headerFieldGetOptional( + in: headerFields, + strategy: .deferredToType, + name: "foo", + as: String.self + ) + XCTAssertEqual(value, "bar") + } + + func testHeaderFieldsGetOptional_string_strategyCodable() throws { let headerFields: [HeaderField] = [ .init(name: "foo", value: "bar") ] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "foo", as: String.self ) XCTAssertEqual(value, "bar") } - func testHeaderGetOptional_missing() throws { + func testHeaderFieldsGetOptional_missing() throws { let headerFields: [HeaderField] = [] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "foo", as: String.self ) XCTAssertNil(value) } - func testHeaderGetRequired_string() throws { + func testHeaderFieldsGetRequired_string() throws { let headerFields: [HeaderField] = [ .init(name: "foo", value: "bar") ] let value = try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "foo", as: String.self ) XCTAssertEqual(value, "bar") } - func testHeaderGetRequired_missing() throws { + func testHeaderFieldsGetRequired_missing() throws { let headerFields: [HeaderField] = [] XCTAssertThrowsError( try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "foo", as: String.self ), @@ -220,10 +281,11 @@ final class Test_CommonConverterExtensions: Test_Runtime { // MARK: [HeaderField] - (Date) - func testHeaderAdd_date() throws { + func testHeaderFieldsAdd_date() throws { var headerFields: [HeaderField] = [] try converter.headerFieldAdd( in: &headerFields, + strategy: .deferredToType, name: "since", value: testDate ) @@ -235,56 +297,61 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderAdd_date_nil() throws { + func testHeaderFieldsAdd_date_nil() throws { var headerFields: [HeaderField] = [] let date: Date? = nil try converter.headerFieldAdd( in: &headerFields, + strategy: .deferredToType, name: "since", value: date ) XCTAssertEqual(headerFields, []) } - func testHeaderGetOptional_date() throws { + func testHeaderFieldsGetOptional_date() throws { let headerFields: [HeaderField] = [ .init(name: "since", value: testDateString) ] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "since", as: Date.self ) XCTAssertEqual(value, testDate) } - func testHeaderGetOptional_date_missing() throws { + func testHeaderFieldsGetOptional_date_missing() throws { let headerFields: [HeaderField] = [] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "since", as: Date.self ) XCTAssertNil(value) } - func testHeaderGetRequired_date() throws { + func testHeaderFieldsGetRequired_date() throws { let headerFields: [HeaderField] = [ .init(name: "since", value: testDateString) ] let value = try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "since", as: Date.self ) XCTAssertEqual(value, testDate) } - func testHeaderGetRequired_date_missing() throws { + func testHeaderFieldsGetRequired_date_missing() throws { let headerFields: [HeaderField] = [] XCTAssertThrowsError( try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "since", as: Date.self ), @@ -304,10 +371,11 @@ final class Test_CommonConverterExtensions: Test_Runtime { // MARK: [HeaderField] - Complex - func testHeaderAddComplex_struct() throws { + func testHeaderFieldsAddComplex_struct() throws { var headerFields: [HeaderField] = [] try converter.headerFieldAdd( in: &headerFields, + strategy: .deferredToType, name: "foo", value: testStruct ) @@ -319,56 +387,61 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderAddComplex_nil() throws { + func testHeaderFieldsAddComplex_nil() throws { var headerFields: [HeaderField] = [] let value: TestPet? = nil try converter.headerFieldAdd( in: &headerFields, + strategy: .deferredToType, name: "foo", value: value ) XCTAssertEqual(headerFields, []) } - func testHeaderGetComplexOptional_struct() throws { + func testHeaderFieldsGetComplexOptional_struct() throws { let headerFields: [HeaderField] = [ .init(name: "pet", value: testStructString) ] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "pet", as: TestPet.self ) XCTAssertEqual(value, testStruct) } - func testHeaderGetComplexOptional_missing() throws { + func testHeaderFieldsGetComplexOptional_missing() throws { let headerFields: [HeaderField] = [] let value = try converter.headerFieldGetOptional( in: headerFields, + strategy: .deferredToType, name: "pet", as: TestPet.self ) XCTAssertNil(value) } - func testHeaderGetComplexRequired_struct() throws { + func testHeaderFieldsGetComplexRequired_struct() throws { let headerFields: [HeaderField] = [ .init(name: "pet", value: testStructString) ] let value = try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "pet", as: TestPet.self ) XCTAssertEqual(value, testStruct) } - func testHeaderGetComplexRequired_missing() throws { + func testHeaderFieldsGetComplexRequired_missing() throws { let headerFields: [HeaderField] = [] XCTAssertThrowsError( try converter.headerFieldGetRequired( in: headerFields, + strategy: .deferredToType, name: "pet", as: TestPet.self ), diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index d8855615..ca75b9db 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -527,7 +527,13 @@ final class Test_ServerConverterExtensions: Test_Runtime { let data = try converter.bodyAdd( testStruct, headerFields: &headers, - transforming: { .init(value: $0, contentType: "application/json") } + transforming: { + .init( + value: $0, + contentType: "application/json", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -538,12 +544,40 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } - func testBodyAddString() throws { + func testBodyAddString_strategyDeferredToType() throws { + var headers: [HeaderField] = [] + let data = try converter.bodyAdd( + testString, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .deferredToType + ) + } + ) + XCTAssertEqual(String(data: data, encoding: .utf8)!, testQuotedString) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + func testBodyAddString_strategyString() throws { var headers: [HeaderField] = [] let data = try converter.bodyAdd( testString, headerFields: &headers, - transforming: { .init(value: $0, contentType: "text/plain") } + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .string + ) + } ) XCTAssertEqual(String(data: data, encoding: .utf8)!, testString) XCTAssertEqual( @@ -554,12 +588,40 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } + func testBodyAddString_strategyCodable() throws { + var headers: [HeaderField] = [] + let data = try converter.bodyAdd( + testString, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "text/plain", + strategy: .codable + ) + } + ) + XCTAssertEqual(String(data: data, encoding: .utf8)!, testQuotedString) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + func testBodyAddData() throws { var headers: [HeaderField] = [] let data = try converter.bodyAdd( testStructPrettyData, headerFields: &headers, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { + .init( + value: $0, + contentType: "application/octet-stream", + strategy: .deferredToType + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -574,6 +636,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: testStructData, + strategy: .deferredToType, transforming: { $0 } ) XCTAssertEqual(body, testStruct) @@ -583,6 +646,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: nil, + strategy: .deferredToType, transforming: { _ -> TestPet in fatalError("Unreachable") } ) XCTAssertNil(body) @@ -592,6 +656,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: testStructData, + strategy: .deferredToType, transforming: { $0 } ) XCTAssertEqual(body, testStruct) @@ -602,6 +667,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { try converter.bodyGetRequired( TestPet.self, from: nil, + strategy: .deferredToType, transforming: { _ -> TestPet in fatalError("Unreachable") } ), "Was expected to throw error on missing required body", @@ -621,33 +687,98 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( Data.self, from: testStructPrettyData, + strategy: .deferredToType, transforming: { $0 } ) XCTAssertEqual(body, testStructPrettyData) } func testBodyGetDataRequired_success() throws { - let body = try converter.bodyGetOptional( + let body = try converter.bodyGetRequired( Data.self, from: testStructPrettyData, + strategy: .deferredToType, transforming: { $0 } ) XCTAssertEqual(body, testStructPrettyData) } - func testBodyGetStringOptional_success() throws { + func testBodyGetDataRequired_missing() throws { + XCTAssertThrowsError( + try converter.bodyGetRequired( + Data.self, + from: nil, + strategy: .deferredToType, + transforming: { $0 } + ), + "Was expected to throw error on missing required body", + { error in + guard + let err = error as? RuntimeError, + case .missingRequiredRequestBody = err + else { + XCTFail("Unexpected kind of error thrown") + return + } + } + ) + } + + func testBodyGetStringOptional_strategyDeferredToType_success() throws { + let body = try converter.bodyGetOptional( + String.self, + from: testQuotedStringData, + strategy: .deferredToType, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetStringOptional_strategyString_success() throws { let body = try converter.bodyGetOptional( String.self, from: testStringData, + strategy: .string, transforming: { $0 } ) XCTAssertEqual(body, testString) } - func testBodyGetStringRequired_success() throws { + func testBodyGetStringOptional_strategyCodable_success() throws { let body = try converter.bodyGetOptional( + String.self, + from: testQuotedStringData, + strategy: .codable, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetStringRequired_strategyDeferredToType_success() throws { + let body = try converter.bodyGetRequired( + String.self, + from: testQuotedStringData, + strategy: .deferredToType, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetStringRequired_strategyString_success() throws { + let body = try converter.bodyGetRequired( String.self, from: testStringData, + strategy: .string, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } + + func testBodyGetStringRequired_strategyCodable_success() throws { + let body = try converter.bodyGetRequired( + String.self, + from: testQuotedStringData, + strategy: .codable, transforming: { $0 } ) XCTAssertEqual(body, testString) diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 451b07ec..3d942970 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -59,7 +59,15 @@ class Test_Runtime: XCTestCase { } var testStringData: Data { - "hello".data(using: .utf8)! + testString.data(using: .utf8)! + } + + var testQuotedString: String { + "\"hello\"" + } + + var testQuotedStringData: Data { + testQuotedString.data(using: .utf8)! } var testStruct: TestPet { From 2ef047d99cea21b7a9ba9d84a4005f4c8c11abf1 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Sat, 3 Jun 2023 09:49:59 +0200 Subject: [PATCH 2/9] PR feedback - split the coding strategy into Parameter and Body --- .../Base/EncodableBodyContent.swift | 4 +- .../Conversion/CodingStrategy.swift | 59 +++++++- .../Conversion/Converter+Client.swift | 4 +- .../Conversion/Converter+Common.swift | 12 +- .../Conversion/Converter+Server.swift | 8 +- .../Deprecated/Deprecated.swift | 132 +++++++++++++++++- .../Conversion/Test_Converter+Client.swift | 60 ++------ .../Conversion/Test_Converter+Server.swift | 54 ++----- 8 files changed, 216 insertions(+), 117 deletions(-) diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift index c1b50159..bddca705 100644 --- a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift +++ b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift @@ -23,7 +23,7 @@ public struct EncodableBodyContent: Equatable { public var contentType: String /// A hint about which coding strategy to use. - public var strategy: CodingStrategy + public var strategy: BodyCodingStrategy /// Creates a new content wrapper. /// - Parameters: @@ -33,7 +33,7 @@ public struct EncodableBodyContent: Equatable { public init( value: T, contentType: String, - strategy: CodingStrategy + strategy: BodyCodingStrategy ) { self.value = value self.contentType = contentType diff --git a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift index 7da60c5f..b5da24c3 100644 --- a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift +++ b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift @@ -12,11 +12,23 @@ // //===----------------------------------------------------------------------===// -/// A hint to the data encoding and decoding logic. +/// A hint to the data encoding and decoding logic for parameters. /// /// Derived from the content type. +/// +/// Parameters can optionally provide an explicit content type using +/// the `content` mapping. Otherwise, their `schema` parameter is used +/// to decide the Swift type. +/// +/// A parameter can be either explicitly specified to use a stringly type +/// (`.string`) or a codable type (`.codable`), when instructed by +/// the `content` mapping. +/// +/// If no `content` is provided, only `schema`, the case `.deferredToType` is +/// used to let the compiler choose the best converter method based on the +/// Swift type of the parameter (for example: `Int`, `Date`, and so on). @_spi(Generated) -public struct CodingStrategy: Equatable, Hashable, Sendable { +public struct ParameterCodingStrategy: Equatable, Hashable, Sendable { /// Describes the underlying coding strategy. private enum _Strategy: String, Equatable, Hashable, Sendable { @@ -48,3 +60,46 @@ public struct CodingStrategy: Equatable, Hashable, Sendable { .init(strategy: .deferredToType) } } + +/// A hint to the data encoding and decoding logic for request and response +/// bodies. +/// +/// Derived from the content type. +/// +/// Request and response bodies always specify a content type, so unlike +/// in the case of ``ParameterCodingStrategy``, there is no `.deferredToType` +/// case for bodies, only explicit strategies for the three fundamental ways +/// bodies can be treated: as a stringly type, a codable type, or raw data. +@_spi(Generated) +public struct BodyCodingStrategy: Equatable, Hashable, Sendable { + + /// Describes the underlying coding strategy. + private enum _Strategy: String, Equatable, Hashable, Sendable { + + /// A strategy using JSONEncoder/JSONDecoder. + case codable + + /// A strategy using LosslessStringConvertible. + case string + + /// A strategy passing through data unmodified. + case data + } + + private let strategy: _Strategy + + /// A strategy using JSONEncoder/JSONDecoder. + public static var codable: Self { + .init(strategy: .codable) + } + + /// A strategy using LosslessStringConvertible. + public static var string: Self { + .init(strategy: .string) + } + + /// A strategy passing through data unmodified. + public static var data: Self { + .init(strategy: .data) + } +} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 8e7a481d..20709ee9 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -90,7 +90,7 @@ extension Converter { public func bodyGet( _ type: T.Type, from data: Data, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (T) -> C ) throws -> C { let decoded: T @@ -167,7 +167,7 @@ extension Converter { public func bodyGet( _ type: Data.Type, from data: Data, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (Data) -> C ) throws -> C { return transform(data) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index bf5b1d42..facbce11 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -97,7 +97,7 @@ extension Converter { /// - value: Date value. If nil, header is not added. public func headerFieldAdd( in headerFields: inout [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, value: Date? ) throws { @@ -117,7 +117,7 @@ extension Converter { /// - Returns: First value for the given name, if one exists. public func headerFieldGetOptional( in headerFields: [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, as type: Date.Type ) throws -> Date? { @@ -136,7 +136,7 @@ extension Converter { /// - Returns: First value for the given name. public func headerFieldGetRequired( in headerFields: [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, as type: Date.Type ) throws -> Date { @@ -165,7 +165,7 @@ extension Converter { /// - value: Encodable header value. public func headerFieldAdd( in headerFields: inout [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, value: T? ) throws { @@ -196,7 +196,7 @@ extension Converter { /// - Returns: First value for the given name, if one exists. public func headerFieldGetOptional( in headerFields: [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, as type: T.Type ) throws -> T? { @@ -223,7 +223,7 @@ extension Converter { /// - Returns: First value for the given name. public func headerFieldGetRequired( in headerFields: [HeaderField], - strategy: CodingStrategy, + strategy: ParameterCodingStrategy, name: String, as type: T.Type ) throws -> T { diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index d55eedbd..d8a97a29 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -258,7 +258,7 @@ public extension Converter { func bodyGetOptional( _ type: T.Type, from data: Data?, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (T) -> C ) throws -> C? { guard let data else { @@ -291,7 +291,7 @@ public extension Converter { func bodyGetRequired( _ type: T.Type, from data: Data?, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (T) -> C ) throws -> C { guard let data else { @@ -351,7 +351,7 @@ public extension Converter { func bodyGetOptional( _ type: Data.Type, from data: Data?, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (Data) -> C ) throws -> C? { guard let data else { @@ -370,7 +370,7 @@ public extension Converter { func bodyGetRequired( _ type: Data.Type, from data: Data?, - strategy: CodingStrategy, + strategy: BodyCodingStrategy, transforming transform: (Data) -> C ) throws -> C { guard let data else { diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 08831807..098a088a 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -259,20 +259,140 @@ extension Converter { } } -extension EncodableBodyContent { +/// A wrapper of a body value with its content type. +@_spi(Generated) +@available(*, deprecated, renamed: "EncodableBodyContent") +public struct LegacyEncodableBodyContent: Equatable { + + /// An encodable body value. + public var value: T + + /// The header value of the content type, for example `application/json`. + public var contentType: String + /// Creates a new content wrapper. /// - Parameters: /// - value: An encodable body value. /// - contentType: The header value of the content type. - @available(*, deprecated, renamed: "init(value:contentType:strategy:)") public init( value: T, contentType: String ) { - self.init( - value: value, - contentType: contentType, - strategy: .deferredToType + self.value = value + self.contentType = contentType + } +} + +extension Converter { + /// Provides an optional serialized value for the body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headerFields: Headers container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value, or nil if `value` was nil. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + public func bodyAddOptional( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data? { + guard let value else { + return nil + } + return try bodyAddRequired( + value, + headerFields: &headerFields, + transforming: transform ) } + + /// Provides a required serialized value for the body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headerFields: Headers container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + public func bodyAddRequired( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + return try encoder.encode(body.value) + } + + /// Provides an optional serialized value for the body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headerFields: Headers container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value, or nil if `value` was nil. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + public func bodyAddOptional( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data? { + guard let value else { + return nil + } + return try bodyAddRequired( + value, + headerFields: &headerFields, + transforming: transform + ) + } + + /// Provides a required serialized value for the body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headerFields: Headers container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + public func bodyAddRequired( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + return body.value + } + + /// Provides a serialized value for the provided body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headerFields: Header fields container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + func bodyAdd( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + return try encoder.encode(body.value) + } + + /// Provides a serialized value for the provided body value. + /// - Parameters: + /// - value: Encodable value to turn into data. + /// - headers: Headers container where to add the Content-Type header. + /// - transform: Closure for transforming the Encodable value into body content. + /// - Returns: Data for the serialized body value. + @available(*, deprecated, message: "Use the variant with EncodableBodyContent") + func bodyAdd( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> LegacyEncodableBodyContent + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + return body.value + } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index a4d33dd8..8e45fa2d 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -74,21 +74,21 @@ final class Test_ClientConverterExtensions: Test_Runtime { XCTAssertEqual(body, testStruct) } - func testBodyGetData_strategyDeferredToType_success() throws { + func testBodyGetData_strategyData_success() throws { let body = try converter.bodyGet( Data.self, from: testStructData, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ) XCTAssertEqual(body, testStructData) } - func testBodyGetString_strategyDeferredToType_success() throws { + func testBodyGetString_strategyData_success() throws { let body = try converter.bodyGet( String.self, from: testQuotedStringData, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ) XCTAssertEqual(body, testString) @@ -145,7 +145,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/json", - strategy: .deferredToType + strategy: .codable ) } ) @@ -179,7 +179,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/json", - strategy: .deferredToType + strategy: .codable ) } ) @@ -201,7 +201,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/octet-stream", - strategy: .deferredToType + strategy: .data ) } ) @@ -235,7 +235,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/octet-stream", - strategy: .deferredToType + strategy: .data ) } ) @@ -248,28 +248,6 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - func testBodyAddStringOptional_strategyDeferredToType_success() throws { - var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testString, - headerFields: &headerFields, - transforming: { - .init( - value: $0, - contentType: "text/plain", - strategy: .deferredToType - ) - } - ) - XCTAssertEqual(data, testQuotedStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - func testBodyAddStringOptional_strategyString_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddOptional( @@ -314,28 +292,6 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - func testBodyAddStringRequired_strategyDeferredToType_success() throws { - var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testString, - headerFields: &headerFields, - transforming: { - .init( - value: $0, - contentType: "text/plain", - strategy: .deferredToType - ) - } - ) - XCTAssertEqual(data, testQuotedStringData) - XCTAssertEqual( - headerFields, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - func testBodyAddStringRequired_strategyString_success() throws { var headerFields: [HeaderField] = [] let data = try converter.bodyAddRequired( diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index ca75b9db..fac45447 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -531,7 +531,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/json", - strategy: .deferredToType + strategy: .codable ) } ) @@ -544,28 +544,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } - func testBodyAddString_strategyDeferredToType() throws { - var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testString, - headerFields: &headers, - transforming: { - .init( - value: $0, - contentType: "text/plain", - strategy: .deferredToType - ) - } - ) - XCTAssertEqual(String(data: data, encoding: .utf8)!, testQuotedString) - XCTAssertEqual( - headers, - [ - .init(name: "content-type", value: "text/plain") - ] - ) - } - func testBodyAddString_strategyString() throws { var headers: [HeaderField] = [] let data = try converter.bodyAdd( @@ -619,7 +597,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { .init( value: $0, contentType: "application/octet-stream", - strategy: .deferredToType + strategy: .data ) } ) @@ -636,7 +614,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: testStructData, - strategy: .deferredToType, + strategy: .codable, transforming: { $0 } ) XCTAssertEqual(body, testStruct) @@ -646,7 +624,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: nil, - strategy: .deferredToType, + strategy: .codable, transforming: { _ -> TestPet in fatalError("Unreachable") } ) XCTAssertNil(body) @@ -656,7 +634,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( TestPet.self, from: testStructData, - strategy: .deferredToType, + strategy: .codable, transforming: { $0 } ) XCTAssertEqual(body, testStruct) @@ -667,7 +645,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { try converter.bodyGetRequired( TestPet.self, from: nil, - strategy: .deferredToType, + strategy: .codable, transforming: { _ -> TestPet in fatalError("Unreachable") } ), "Was expected to throw error on missing required body", @@ -687,7 +665,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetOptional( Data.self, from: testStructPrettyData, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ) XCTAssertEqual(body, testStructPrettyData) @@ -697,7 +675,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { let body = try converter.bodyGetRequired( Data.self, from: testStructPrettyData, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ) XCTAssertEqual(body, testStructPrettyData) @@ -708,7 +686,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { try converter.bodyGetRequired( Data.self, from: nil, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ), "Was expected to throw error on missing required body", @@ -724,11 +702,11 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } - func testBodyGetStringOptional_strategyDeferredToType_success() throws { + func testBodyGetStringOptional_strategyData_success() throws { let body = try converter.bodyGetOptional( String.self, from: testQuotedStringData, - strategy: .deferredToType, + strategy: .data, transforming: { $0 } ) XCTAssertEqual(body, testString) @@ -754,16 +732,6 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(body, testString) } - func testBodyGetStringRequired_strategyDeferredToType_success() throws { - let body = try converter.bodyGetRequired( - String.self, - from: testQuotedStringData, - strategy: .deferredToType, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - func testBodyGetStringRequired_strategyString_success() throws { let body = try converter.bodyGetRequired( String.self, From 56a6ba537274b3f88aaca6328dd71ccf7a4aaece Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Sat, 3 Jun 2023 10:26:04 +0200 Subject: [PATCH 3/9] Formatting fixes --- Sources/OpenAPIRuntime/Deprecated/Deprecated.swift | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 098a088a..dfa2ee0b 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -263,13 +263,13 @@ extension Converter { @_spi(Generated) @available(*, deprecated, renamed: "EncodableBodyContent") public struct LegacyEncodableBodyContent: Equatable { - + /// An encodable body value. public var value: T - + /// The header value of the content type, for example `application/json`. public var contentType: String - + /// Creates a new content wrapper. /// - Parameters: /// - value: An encodable body value. From 8b8b4817d73b26505f1fbe79eddd108c5aebcf56 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 14:08:15 +0200 Subject: [PATCH 4/9] Rename _StringParameterConvertible to _StringConvertible --- .../Base/_AutoLosslessStringConvertible.swift | 2 +- ...ertible.swift => _StringConvertible.swift} | 20 +++++++++---------- .../Conversion/Converter+Client.swift | 12 +++++------ .../Conversion/Converter+Common.swift | 4 ++-- .../Conversion/Converter+Server.swift | 18 ++++++++--------- .../Conversion/FoundationExtensions.swift | 4 ++-- .../Conversion/Test_Converter+Common.swift | 2 +- 7 files changed, 31 insertions(+), 31 deletions(-) rename Sources/OpenAPIRuntime/Base/{_StringParameterConvertible.swift => _StringConvertible.swift} (57%) diff --git a/Sources/OpenAPIRuntime/Base/_AutoLosslessStringConvertible.swift b/Sources/OpenAPIRuntime/Base/_AutoLosslessStringConvertible.swift index 95d640b0..38116c73 100644 --- a/Sources/OpenAPIRuntime/Base/_AutoLosslessStringConvertible.swift +++ b/Sources/OpenAPIRuntime/Base/_AutoLosslessStringConvertible.swift @@ -20,7 +20,7 @@ /// Cannot be marked as SPI, as it's added on public types, but should be /// considered an internal implementation detail of the generator. public protocol _AutoLosslessStringConvertible: - RawRepresentable, LosslessStringConvertible, _StringParameterConvertible + RawRepresentable, LosslessStringConvertible, _StringConvertible where RawValue == String {} extension _AutoLosslessStringConvertible { diff --git a/Sources/OpenAPIRuntime/Base/_StringParameterConvertible.swift b/Sources/OpenAPIRuntime/Base/_StringConvertible.swift similarity index 57% rename from Sources/OpenAPIRuntime/Base/_StringParameterConvertible.swift rename to Sources/OpenAPIRuntime/Base/_StringConvertible.swift index 20152201..e680e822 100644 --- a/Sources/OpenAPIRuntime/Base/_StringParameterConvertible.swift +++ b/Sources/OpenAPIRuntime/Base/_StringConvertible.swift @@ -13,17 +13,17 @@ //===----------------------------------------------------------------------===// import Foundation -/// This marker protocol represents types used in parameters -/// (headers, path parameters, query items, ...). +/// This marker protocol represents types that are representable as a string, +/// usable in headers, path parameters, query items, and text bodies. /// /// Cannot be marked as SPI, as it's added on public types, but should be /// considered an internal implementation detail of the generator. -public protocol _StringParameterConvertible: LosslessStringConvertible {} +public protocol _StringConvertible: LosslessStringConvertible {} -extension String: _StringParameterConvertible {} -extension Bool: _StringParameterConvertible {} -extension Int: _StringParameterConvertible {} -extension Int64: _StringParameterConvertible {} -extension Int32: _StringParameterConvertible {} -extension Float: _StringParameterConvertible {} -extension Double: _StringParameterConvertible {} +extension String: _StringConvertible {} +extension Bool: _StringConvertible {} +extension Int: _StringConvertible {} +extension Int64: _StringConvertible {} +extension Int32: _StringConvertible {} +extension Float: _StringConvertible {} +extension Double: _StringConvertible {} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b9dc0667..da1444c8 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -15,14 +15,14 @@ import Foundation extension Converter { - // MARK: Query - _StringParameterConvertible + // MARK: Query - _StringConvertible /// Adds a query item with a string-convertible value to the request. /// - Parameters: /// - request: Request to add the query item. /// - name: Query item name. /// - value: Query item string-convertible value. - public func queryAdd( + public func queryAdd( in request: inout Request, name: String, value: T? @@ -57,14 +57,14 @@ extension Converter { } } - // MARK: Query - Array of _StringParameterConvertible + // MARK: Query - Array of _StringConvertible /// Adds a query item with a list of string-convertible values to the request. /// - Parameters: /// - request: Request to add the query item. /// - name: Query item name. /// - value: Query item string-convertible values. - public func queryAdd( + public func queryAdd( in request: inout Request, name: String, value: [T]? @@ -91,7 +91,7 @@ extension Converter { transforming transform: (T) -> C ) throws -> C { let decoded: T - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringConvertible.Type { guard let stringValue = String(data: data, encoding: .utf8), let decodedValue = myType.init(stringValue) @@ -139,7 +139,7 @@ extension Converter { ) throws -> Data { let body = transform(value) headerFields.add(name: "content-type", value: body.contentType) - if let value = value as? _StringParameterConvertible { + if let value = value as? _StringConvertible { guard let data = value.description.data(using: .utf8) else { throw RuntimeError.failedToEncodePrimitiveBodyIntoData } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 2df14252..bcbb3597 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -162,7 +162,7 @@ public extension Converter { guard let value else { return } - if let value = value as? _StringParameterConvertible { + if let value = value as? _StringConvertible { headerFields.add(name: name, value: value.description) return } @@ -189,7 +189,7 @@ public extension Converter { guard let stringValue = headerFields.firstValue(name: name) else { return nil } - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringConvertible.Type { return myType.init(stringValue).map { $0 as! T } } let data = Data(stringValue.utf8) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index d2467fde..e0172490 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -65,7 +65,7 @@ public extension Converter { /// - name: Path variable name. /// - type: Path variable type. /// - Returns: Deserialized path variable value, if present. - func pathGetOptional( + func pathGetOptional( in pathParameters: [String: String], name: String, as type: T.Type @@ -85,7 +85,7 @@ public extension Converter { /// - name: Path variable name. /// - type: Path variable type. /// - Returns: Deserialized path variable value. - func pathGetRequired( + func pathGetRequired( in pathParameters: [String: String], name: String, as type: T.Type @@ -111,7 +111,7 @@ public extension Converter { /// - name: Query item name. /// - type: Query item value type. /// - Returns: Deserialized query item value, if present. - func queryGetOptional( + func queryGetOptional( in queryParameters: [URLQueryItem], name: String, as type: T.Type @@ -135,7 +135,7 @@ public extension Converter { /// - name: Query item name. /// - type: Query item value type. /// - Returns: Deserialized query item value. - func queryGetRequired( + func queryGetRequired( in queryParameters: [URLQueryItem], name: String, as type: T.Type @@ -187,7 +187,7 @@ public extension Converter { return try self.configuration.dateTranscoder.decode(dateString) } - // MARK: Query - Array of _StringParameterConvertible + // MARK: Query - Array of _StringConvertible /// Returns an array of deserialized values for all the query items /// found under the provided name. @@ -196,7 +196,7 @@ public extension Converter { /// - name: Query item name. /// - type: Query item value type. /// - Returns: Deserialized query item value, if present. - func queryGetOptional( + func queryGetOptional( in queryParameters: [URLQueryItem], name: String, as type: [T].Type @@ -226,7 +226,7 @@ public extension Converter { /// - name: Query item name. /// - type: Query item value type. /// - Returns: Deserialized query item value. - func queryGetRequired( + func queryGetRequired( in queryParameters: [URLQueryItem], name: String, as type: [T].Type @@ -263,7 +263,7 @@ public extension Converter { return nil } let decoded: T - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringConvertible.Type { guard let stringValue = String(data: data, encoding: .utf8), let decodedValue = myType.init(stringValue) @@ -309,7 +309,7 @@ public extension Converter { let body = transform(value) headerFields.add(name: "content-type", value: body.contentType) let bodyValue = body.value - if let value = bodyValue as? _StringParameterConvertible { + if let value = bodyValue as? _StringConvertible { guard let data = value.description.data(using: .utf8) else { throw RuntimeError.failedToEncodePrimitiveBodyIntoData } diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index 19e6656f..d7f4302f 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -45,7 +45,7 @@ extension URLComponents { /// - Parameters: /// - name: Query name. /// - value: Typed value. - mutating func addQueryItem( + mutating func addQueryItem( name: String, value: T? ) { @@ -62,7 +62,7 @@ extension URLComponents { /// - Parameters: /// - name: Query name. /// - value: Array of typed values. - mutating func addQueryItem( + mutating func addQueryItem( name: String, value: [T]? ) { diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 9f60f4e4..d3a0c1c4 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -145,7 +145,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - // MARK: [HeaderField] - _StringParameterConvertible + // MARK: [HeaderField] - _StringConvertible func testHeaderAdd_string() throws { var headerFields: [HeaderField] = [] From 7ff7e638fe11261ada1a8d5298f44f847a26bc60 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 14:16:49 +0200 Subject: [PATCH 5/9] Fix a bad merge --- Sources/OpenAPIRuntime/Conversion/Converter+Server.swift | 2 +- Sources/OpenAPIRuntime/Deprecated/Deprecated.swift | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index f9c9b4d9..0ebdcca2 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -298,7 +298,7 @@ public extension Converter { throw RuntimeError.missingRequiredRequestBody } let decoded: T - if let myType = T.self as? _StringParameterConvertible.Type, + if let myType = T.self as? _StringConvertible.Type, strategy == .string { guard diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index dfa2ee0b..fc8a1cec 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -197,7 +197,7 @@ extension Converter { guard let value else { return } - if let value = value as? _StringParameterConvertible { + if let value = value as? _StringConvertible { headerFields.add(name: name, value: value.description) return } @@ -225,7 +225,7 @@ extension Converter { guard let stringValue = headerFields.firstValue(name: name) else { return nil } - if let myType = T.self as? _StringParameterConvertible.Type { + if let myType = T.self as? _StringConvertible.Type { return myType.init(stringValue).map { $0 as! T } } let data = Data(stringValue.utf8) From a9c634b268a589eeb23016f16813ba923ae976ee Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 15:07:23 +0200 Subject: [PATCH 6/9] Putting together the full list --- .../Conversion/CodingStrategy.swift | 2 + .../Conversion/Converter+Client.swift | 57 +++++++++++++++++++ 2 files changed, 59 insertions(+) diff --git a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift index b5da24c3..578f37c1 100644 --- a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift +++ b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift @@ -27,6 +27,7 @@ /// If no `content` is provided, only `schema`, the case `.deferredToType` is /// used to let the compiler choose the best converter method based on the /// Swift type of the parameter (for example: `Int`, `Date`, and so on). +@available(*, deprecated, message: "stop using") @_spi(Generated) public struct ParameterCodingStrategy: Equatable, Hashable, Sendable { @@ -70,6 +71,7 @@ public struct ParameterCodingStrategy: Equatable, Hashable, Sendable { /// in the case of ``ParameterCodingStrategy``, there is no `.deferredToType` /// case for bodies, only explicit strategies for the three fundamental ways /// bodies can be treated: as a stringly type, a codable type, or raw data. +@available(*, deprecated, message: "stop using") @_spi(Generated) public struct BodyCodingStrategy: Equatable, Hashable, Sendable { diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b22cf3eb..806853c9 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -13,6 +13,63 @@ //===----------------------------------------------------------------------===// import Foundation +/* + TODO: Optional value getters need a separate override. + + Client: + + - request + - set request path + - text + - string-convertible + - optional/required + - set request query + - text + - string-convertible + - optional/required + - array of string-convertibles + - optional/required + - set request headers + - text + - string-convertible + - optional/required + - array of string-convertibles + - optional/required + - structured + - codable + - optional/required + - set request body + - text + - string-convertible + - optional + - required + - structured + - codable + - optional + - required + - data + - data + - optional + - required + - response + - get response headers + - text + - string-convertible + - optional + - required + - array of string-convertibles + - optional + - required + - structured + - codable + - optional + - required + - get response body + + + + */ + extension Converter { // MARK: Query - _StringConvertible From 581b69ba998b4c4549ae79f4b544f65144994efa Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Tue, 6 Jun 2023 17:19:29 +0200 Subject: [PATCH 7/9] Moved to generator --- .../Conversion/Converter+Client.swift | 57 ------------------- 1 file changed, 57 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 806853c9..b22cf3eb 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -13,63 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -/* - TODO: Optional value getters need a separate override. - - Client: - - - request - - set request path - - text - - string-convertible - - optional/required - - set request query - - text - - string-convertible - - optional/required - - array of string-convertibles - - optional/required - - set request headers - - text - - string-convertible - - optional/required - - array of string-convertibles - - optional/required - - structured - - codable - - optional/required - - set request body - - text - - string-convertible - - optional - - required - - structured - - codable - - optional - - required - - data - - data - - optional - - required - - response - - get response headers - - text - - string-convertible - - optional - - required - - array of string-convertibles - - optional - - required - - structured - - codable - - optional - - required - - get response body - - - - */ - extension Converter { // MARK: Query - _StringConvertible From 4c9d1edfcaf8f17dc4900a969383bb65b6e3fe85 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 00:01:22 +0200 Subject: [PATCH 8/9] Work in progress --- .../Conversion/Converter+Common.swift | 84 +++++++++---------- .../Conversion/CurrencyExtensions.swift | 50 +++++++++++ 2 files changed, 90 insertions(+), 44 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 1a253172..7ff69c5d 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -13,48 +13,6 @@ //===----------------------------------------------------------------------===// import Foundation -@_spi(Generated) -public extension Array where Element == HeaderField { - - /// Adds a header for the provided name and value. - /// - Parameters: - /// - name: Header name. - /// - value: Header value. If nil, the header is not added. - @_spi(Generated) - mutating func add(name: String, value: String?) { - guard let value = value else { - return - } - append(.init(name: name, value: value)) - } - - /// Removes all headers matching the provided (case-insensitive) name. - /// - Parameters: - /// - name: Header name. - @_spi(Generated) - mutating func removeAll(named name: String) { - removeAll { - $0.name.caseInsensitiveCompare(name) == .orderedSame - } - } - - /// Returns the first header value for the provided (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: First value for the given name. Nil if one does not exist. - @_spi(Generated) - func firstValue(name: String) -> String? { - first { $0.name.caseInsensitiveCompare(name) == .orderedSame }?.value - } - - /// Returns all header values for the given (case-insensitive) name. - /// - Parameter name: Header name. - /// - Returns: All values for the given name, might be empty if none are found. - @_spi(Generated) - func values(name: String) -> [String] { - filter { $0.name.caseInsensitiveCompare(name) == .orderedSame }.map { $0.value } - } -} - extension Converter { // MARK: Miscs @@ -87,8 +45,46 @@ extension Converter { } } - // MARK: Headers - Date - + // method name: {set,get}{location}As{strategy}{required/optional/omit if both} + // method parameters: value or type of value + + + +// | common | set | header field | text | string-convertible | both | TODO | + public func setHeaderFieldAsText( + in headerFields: inout [HeaderField], + name: String, + value: T? + ) throws { + guard let value else { + return + } + headerFields.add(name: name, value: value.description) + } + +// | common | set | header field | text | array of string-convertibles | both | TODO | + public func setHeaderFieldAsText( + in headerFields: inout [HeaderField], + name: String, + value values: [T]? + ) throws { + guard let values else { + return + } + headerFields.add(name: name, values: values.map(\.description)) + } + +// | common | set | header field | text | date | both | TODO | +// | common | set | header field | text | array of dates | both | TODO | +// | common | set | header field | JSON | codable | both | TODO | + + + + + + + + /// Adds a header field with the provided name and Date value. /// - Parameters: /// - headerFields: Collection of header fields to add to. diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 5b4e89af..4bb5d3c8 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -42,3 +42,53 @@ extension ServerRequestMetadata: CustomStringConvertible { "path parameters: \(pathParameters.description), query parameters: \(queryParameters.description)" } } + +extension Array where Element == HeaderField { + + /// Adds a header for the provided name and value. + /// - Parameters: + /// - name: Header name. + /// - value: Header value. If nil, the header is not added. + mutating func add(name: String, value: String?) { + guard let value = value else { + return + } + append(.init(name: name, value: value)) + } + + /// Adds headers for the provided name and values. + /// - Parameters: + /// - name: Header name. + /// - value: Header values. + mutating func add(name: String, values: [String]?) { + guard let values = values else { + return + } + for value in values { + append(.init(name: name, value: value)) + } + } + + /// Removes all headers matching the provided (case-insensitive) name. + /// - Parameters: + /// - name: Header name. + mutating func removeAll(named name: String) { + removeAll { + $0.name.caseInsensitiveCompare(name) == .orderedSame + } + } + + /// Returns the first header value for the provided (case-insensitive) name. + /// - Parameter name: Header name. + /// - Returns: First value for the given name. Nil if one does not exist. + func firstValue(name: String) -> String? { + first { $0.name.caseInsensitiveCompare(name) == .orderedSame }?.value + } + + /// Returns all header values for the given (case-insensitive) name. + /// - Parameter name: Header name. + /// - Returns: All values for the given name, might be empty if none are found. + func values(name: String) -> [String] { + filter { $0.name.caseInsensitiveCompare(name) == .orderedSame }.map { $0.value } + } +} From 98659fe44b530f292131db40782b71ef3e4db46f Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 8 Jun 2023 16:59:02 +0200 Subject: [PATCH 9/9] The new approach now working --- .../Base/EncodableBodyContent.swift | 10 +- .../Conversion/CodingStrategy.swift | 107 --- .../Conversion/Converter+Client.swift | 331 +++++---- .../Conversion/Converter+Common.swift | 318 +++++---- .../Conversion/Converter+Server.swift | 492 ++++++------- .../Conversion/CurrencyExtensions.swift | 365 +++++++++- .../Conversion/FoundationExtensions.swift | 45 +- .../Deprecated/Deprecated.swift | 8 +- .../OpenAPIRuntime/Errors/RuntimeError.swift | 26 +- .../Base/Test_StringConvertible.swift | 33 + .../Conversion/Test_CodableExtensions.swift | 44 +- .../Conversion/Test_Converter+Client.swift | 345 +++++----- .../Conversion/Test_Converter+Common.swift | 395 ++++------- .../Conversion/Test_Converter+Server.swift | 647 ++++-------------- .../Conversion/Test_CurrencyExtensions.swift | 86 +++ .../Test_FoundationExtensions.swift | 24 +- Tests/OpenAPIRuntimeTests/Test_Runtime.swift | 12 +- 17 files changed, 1564 insertions(+), 1724 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift create mode 100644 Tests/OpenAPIRuntimeTests/Base/Test_StringConvertible.swift create mode 100644 Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift index bddca705..f4995314 100644 --- a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift +++ b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift @@ -14,7 +14,7 @@ /// A wrapper of a body value with its content type. @_spi(Generated) -public struct EncodableBodyContent: Equatable { +public struct EncodableBodyContent: Equatable { /// An encodable body value. public var value: T @@ -22,21 +22,15 @@ public struct EncodableBodyContent: Equatable { /// The header value of the content type, for example `application/json`. public var contentType: String - /// A hint about which coding strategy to use. - public var strategy: BodyCodingStrategy - /// Creates a new content wrapper. /// - Parameters: /// - value: An encodable body value. /// - contentType: The header value of the content type. - /// - strategy: A hint about which coding strategy to use. public init( value: T, - contentType: String, - strategy: BodyCodingStrategy + contentType: String ) { self.value = value self.contentType = contentType - self.strategy = strategy } } diff --git a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift b/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift deleted file mode 100644 index 578f37c1..00000000 --- a/Sources/OpenAPIRuntime/Conversion/CodingStrategy.swift +++ /dev/null @@ -1,107 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// 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 -// -//===----------------------------------------------------------------------===// - -/// A hint to the data encoding and decoding logic for parameters. -/// -/// Derived from the content type. -/// -/// Parameters can optionally provide an explicit content type using -/// the `content` mapping. Otherwise, their `schema` parameter is used -/// to decide the Swift type. -/// -/// A parameter can be either explicitly specified to use a stringly type -/// (`.string`) or a codable type (`.codable`), when instructed by -/// the `content` mapping. -/// -/// If no `content` is provided, only `schema`, the case `.deferredToType` is -/// used to let the compiler choose the best converter method based on the -/// Swift type of the parameter (for example: `Int`, `Date`, and so on). -@available(*, deprecated, message: "stop using") -@_spi(Generated) -public struct ParameterCodingStrategy: Equatable, Hashable, Sendable { - - /// Describes the underlying coding strategy. - private enum _Strategy: String, Equatable, Hashable, Sendable { - - /// A strategy using JSONEncoder/JSONDecoder. - case codable - - /// A strategy using LosslessStringConvertible. - case string - - /// A strategy for letting the type choose the appropriate option. - case deferredToType - } - - private let strategy: _Strategy - - /// A strategy using JSONEncoder/JSONDecoder. - public static var codable: Self { - .init(strategy: .codable) - } - - /// A strategy using LosslessStringConvertible. - public static var string: Self { - .init(strategy: .string) - } - - /// A strategy for letting the type choose the appropriate option. - public static var deferredToType: Self { - .init(strategy: .deferredToType) - } -} - -/// A hint to the data encoding and decoding logic for request and response -/// bodies. -/// -/// Derived from the content type. -/// -/// Request and response bodies always specify a content type, so unlike -/// in the case of ``ParameterCodingStrategy``, there is no `.deferredToType` -/// case for bodies, only explicit strategies for the three fundamental ways -/// bodies can be treated: as a stringly type, a codable type, or raw data. -@available(*, deprecated, message: "stop using") -@_spi(Generated) -public struct BodyCodingStrategy: Equatable, Hashable, Sendable { - - /// Describes the underlying coding strategy. - private enum _Strategy: String, Equatable, Hashable, Sendable { - - /// A strategy using JSONEncoder/JSONDecoder. - case codable - - /// A strategy using LosslessStringConvertible. - case string - - /// A strategy passing through data unmodified. - case data - } - - private let strategy: _Strategy - - /// A strategy using JSONEncoder/JSONDecoder. - public static var codable: Self { - .init(strategy: .codable) - } - - /// A strategy using LosslessStringConvertible. - public static var string: Self { - .init(strategy: .string) - } - - /// A strategy passing through data unmodified. - public static var data: Self { - .init(strategy: .data) - } -} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index b22cf3eb..561febc2 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -15,198 +15,243 @@ import Foundation extension Converter { - // MARK: Query - _StringConvertible - - /// Adds a query item with a string-convertible value to the request. - /// - Parameters: - /// - request: Request to add the query item. - /// - name: Query item name. - /// - value: Query item string-convertible value. - public func queryAdd( + // | client | set | request path | text | string-convertible | required | renderedRequestPath | + public func renderedRequestPath( + template: String, + parameters: [any _StringConvertible] + ) throws -> String { + var renderedString = template + for parameter in parameters { + renderedString.replace( + "{}", + with: parameter.description, + maxReplacements: 1 + ) + } + return renderedString + } + + // | client | set | request query | text | string-convertible | both | setQueryItemAsText | + public func setQueryItemAsText( in request: inout Request, name: String, value: T? ) throws { - request.mutatingQuery { components in - components.addQueryItem( - name: name, - value: value - ) - } + try setQueryItem( + in: &request, + name: name, + value: value, + convert: convertStringConvertibleToText + ) } - // MARK: Query - Date + // | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | + public func setQueryItemAsText( + in request: inout Request, + name: String, + value: [T]? + ) throws { + try setQueryItems( + in: &request, + name: name, + values: value, + convert: convertStringConvertibleToText + ) + } - /// Adds a query item with a Date value to the request. - /// - Parameters: - /// - request: Request to add the query item. - /// - name: Query item name. - /// - value: Query item Date value. - public func queryAdd( + // | client | set | request query | text | date | both | setQueryItemAsText | + public func setQueryItemAsText( in request: inout Request, name: String, value: Date? ) throws { - try request.mutatingQuery { components in - try components.addQueryItem( - name: name, - value: value.flatMap { value in - try self.configuration.dateTranscoder.encode(value) - } - ) - } + try setQueryItem( + in: &request, + name: name, + value: value, + convert: convertDateToText + ) } - // MARK: Query - Array of _StringConvertible - - /// Adds a query item with a list of string-convertible values to the request. - /// - Parameters: - /// - request: Request to add the query item. - /// - name: Query item name. - /// - value: Query item string-convertible values. - public func queryAdd( + // | client | set | request query | text | array of dates | both | setQueryItemAsText | + public func setQueryItemAsText( in request: inout Request, name: String, - value: [T]? + value: [Date]? ) throws { - request.mutatingQuery { components in - components.addQueryItem( - name: name, - value: value - ) - } + try setQueryItems( + in: &request, + name: name, + values: value, + convert: convertDateToText + ) } - // MARK: Body - Complex + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + public func setOptionalRequestBodyAsText( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertStringConvertibleToTextData + ) + } - /// Gets a deserialized value from body data. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final - /// type. - /// - Returns: Deserialized body value. - public func bodyGet( - _ type: T.Type, - from data: Data, - strategy: BodyCodingStrategy, - transforming transform: (T) -> C - ) throws -> C { - let decoded: T - if let myType = T.self as? _StringConvertible.Type, - strategy == .string - { - guard - let stringValue = String(data: data, encoding: .utf8), - let decodedValue = myType.init(stringValue) - else { - throw RuntimeError.failedToDecodeBody(type: T.self) - } - decoded = decodedValue as! T - } else { - decoded = try decoder.decode(type, from: data) - } - return transform(decoded) + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + public func setRequiredRequestBodyAsText( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertStringConvertibleToTextData + ) } - /// Provides an optional serialized value for the body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headerFields: Headers container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value, or nil if `value` was nil. - public func bodyAddOptional( + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + public func setOptionalRequestBodyAsText( _ value: C?, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + transforming transform: (C) -> EncodableBodyContent ) throws -> Data? { - guard let value else { - return nil - } - return try bodyAddRequired( + try setOptionalRequestBody( value, headerFields: &headerFields, - transforming: transform + transforming: transform, + convert: convertDateToTextData ) } - /// Provides a required serialized value for the body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headerFields: Headers container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value. - public func bodyAddRequired( + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + public func setRequiredRequestBodyAsText( _ value: C, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + transforming transform: (C) -> EncodableBodyContent ) throws -> Data { - let body = transform(value) - headerFields.add(name: "content-type", value: body.contentType) - if let value = value as? _StringConvertible, - body.strategy == .string - { - guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodeBody(type: T.self) - } - return data - } - return try encoder.encode(body.value) + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDateToTextData + ) } - // MARK: Body - Primivite - Data + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + public func setOptionalRequestBodyAsJSON( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertBodyCodableToJSON + ) + } - /// Gets a deserialized value from body data. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value. - public func bodyGet( - _ type: Data.Type, - from data: Data, - strategy: BodyCodingStrategy, - transforming transform: (Data) -> C - ) throws -> C { - return transform(data) + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + public func setRequiredRequestBodyAsJSON( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertBodyCodableToJSON + ) } - /// Provides an optional serialized value for the body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headerFields: Headers container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value, or nil if `value` was nil. - public func bodyAddOptional( + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + public func setOptionalRequestBodyAsBinary( _ value: C?, headerFields: inout [HeaderField], transforming transform: (C) -> EncodableBodyContent ) throws -> Data? { - guard let value else { - return nil - } - return try bodyAddRequired( + try setOptionalRequestBody( value, headerFields: &headerFields, - transforming: transform + transforming: transform, + convert: convertDataToBinary ) } - /// Provides a required serialized value for the body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headerFields: Headers container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value. - public func bodyAddRequired( + // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + public func setRequiredRequestBodyAsBinary( _ value: C, headerFields: inout [HeaderField], transforming transform: (C) -> EncodableBodyContent ) throws -> Data { - let body = transform(value) - headerFields.add(name: "content-type", value: body.contentType) - return body.value + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDataToBinary + ) + } + + // | client | get | response body | text | string-convertible | required | getResponseBodyAsText | + public func getResponseBodyAsText( + _ type: T.Type, + from data: Data, + transforming transform: (T) -> C + ) throws -> C { + try getResponseBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToStringConvertible + ) + } + + // | client | get | response body | text | date | required | getResponseBodyAsText | + public func getResponseBodyAsText( + _ type: Date.Type, + from data: Data, + transforming transform: (Date) -> C + ) throws -> C { + try getResponseBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToDate + ) + } + + // | client | get | response body | JSON | codable | required | getResponseBodyAsJSON | + public func getResponseBodyAsJSON( + _ type: T.Type, + from data: Data, + transforming transform: (T) -> C + ) throws -> C { + try getResponseBody( + type, + from: data, + transforming: transform, + convert: convertJSONToCodable + ) + } + + // | client | get | response body | binary | data | required | getResponseBodyAsBinary | + public func getResponseBodyAsBinary( + _ type: Data.Type, + from data: Data, + transforming transform: (Data) -> C + ) throws -> C { + try getResponseBody( + type, + from: data, + transforming: transform, + convert: convertBinaryToData + ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 7ff69c5d..46243e26 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -31,9 +31,8 @@ extension Converter { substring: String ) throws { guard - let contentType = try headerFieldGetOptional( + let contentType = try getOptionalHeaderFieldAsText( in: headerFields, - strategy: .string, name: "content-type", as: String.self ) @@ -45,194 +44,215 @@ extension Converter { } } - // method name: {set,get}{location}As{strategy}{required/optional/omit if both} - // method parameters: value or type of value - - - -// | common | set | header field | text | string-convertible | both | TODO | + // MARK: - Converter helper methods + + // | common | set | header field | text | string-convertible | both | setHeaderFieldAsText | public func setHeaderFieldAsText( in headerFields: inout [HeaderField], name: String, value: T? ) throws { - guard let value else { - return - } - headerFields.add(name: name, value: value.description) + try setHeaderField( + in: &headerFields, + name: name, + value: value, + convert: convertStringConvertibleToText + ) } - -// | common | set | header field | text | array of string-convertibles | both | TODO | + + // | common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | public func setHeaderFieldAsText( in headerFields: inout [HeaderField], name: String, value values: [T]? ) throws { - guard let values else { - return - } - headerFields.add(name: name, values: values.map(\.description)) - } - -// | common | set | header field | text | date | both | TODO | -// | common | set | header field | text | array of dates | both | TODO | -// | common | set | header field | JSON | codable | both | TODO | - - - - - - - - - /// Adds a header field with the provided name and Date value. - /// - Parameters: - /// - headerFields: Collection of header fields to add to. - /// - strategy: A hint about which coding strategy to use. - /// - name: The name of the header field. - /// - value: Date value. If nil, header is not added. - public func headerFieldAdd( + try setHeaderFields( + in: &headerFields, + name: name, + values: values, + convert: convertStringConvertibleToText + ) + } + + // | common | set | header field | text | date | both | setHeaderFieldAsText | + public func setHeaderFieldAsText( in headerFields: inout [HeaderField], - strategy: ParameterCodingStrategy, name: String, value: Date? ) throws { - guard let value = value else { - return - } - let stringValue = try self.configuration.dateTranscoder.encode(value) - headerFields.add(name: name, value: stringValue) + try setHeaderField( + in: &headerFields, + name: name, + value: value, + convert: convertDateToText + ) } - /// Returns the value for the first header field with given name. - /// - Parameters: - /// - headerFields: Collection of header fields to retrieve the field from. - /// - strategy: A hint about which coding strategy to use. - /// - name: The name of the header field (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name, if one exists. - public func headerFieldGetOptional( + // | common | set | header field | text | array of dates | both | setHeaderFieldAsText | + public func setHeaderFieldAsText( + in headerFields: inout [HeaderField], + name: String, + value values: [Date]? + ) throws { + try setHeaderFields( + in: &headerFields, + name: name, + values: values, + convert: convertDateToText + ) + } + + // | common | set | header field | JSON | codable | both | setHeaderFieldAsJSON | + public func setHeaderFieldAsJSON( + in headerFields: inout [HeaderField], + name: String, + value: T? + ) throws { + try setHeaderField( + in: &headerFields, + name: name, + value: value, + convert: convertHeaderFieldCodableToJSON + ) + } + + // | common | get | header field | text | string-convertible | optional | getOptionalHeaderFieldAsText | + public func getOptionalHeaderFieldAsText( + in headerFields: [HeaderField], + name: String, + as type: T.Type + ) throws -> T? { + try getOptionalHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | common | get | header field | text | string-convertible | required | getRequiredHeaderFieldAsText | + public func getRequiredHeaderFieldAsText( + in headerFields: [HeaderField], + name: String, + as type: T.Type + ) throws -> T { + try getRequiredHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | common | get | header field | text | array of string-convertibles | optional | getOptionalHeaderFieldAsText | + public func getOptionalHeaderFieldAsText( + in headerFields: [HeaderField], + name: String, + as type: [T].Type + ) throws -> [T]? { + try getOptionalHeaderFields( + in: headerFields, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | common | get | header field | text | array of string-convertibles | required | getRequiredHeaderFieldAsText | + public func getRequiredHeaderFieldAsText( + in headerFields: [HeaderField], + name: String, + as type: [T].Type + ) throws -> [T]? { + try getRequiredHeaderFields( + in: headerFields, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | common | get | header field | text | date | optional | getOptionalHeaderFieldAsText | + public func getOptionalHeaderFieldAsText( in headerFields: [HeaderField], - strategy: ParameterCodingStrategy, name: String, as type: Date.Type ) throws -> Date? { - guard let dateString = headerFields.firstValue(name: name) else { - return nil - } - return try self.configuration.dateTranscoder.decode(dateString) + try getOptionalHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldTextToDate + ) } - /// Returns the value for the first header field with the given name. - /// - Parameters: - /// - headerFields: Collection of header fields to retrieve the field from. - /// - strategy: A hint about which coding strategy to use. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name. - public func headerFieldGetRequired( + // | common | get | header field | text | date | required | getRequiredHeaderFieldAsText | + public func getRequiredHeaderFieldAsText( in headerFields: [HeaderField], - strategy: ParameterCodingStrategy, name: String, as type: Date.Type ) throws -> Date { - guard - let value = try headerFieldGetOptional( - in: headerFields, - strategy: strategy, - name: name, - as: type - ) - else { - throw RuntimeError.missingRequiredHeader(name) - } - return value + try getRequiredHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldTextToDate + ) } - // MARK: Headers - Complex + // | common | get | header field | text | array of dates | optional | getOptionalHeaderFieldAsText | + public func getOptionalHeaderFieldAsText( + in headerFields: [HeaderField], + name: String, + as type: [Date].Type + ) throws -> [Date]? { + try getOptionalHeaderFields( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldTextToDate + ) + } - /// Adds a header field with the provided name and encodable value. - /// - /// Encodes the value into minimized JSON. - /// - Parameters: - /// - headerFields: Collection of header fields to add to. - /// - strategy: A hint about which coding strategy to use. - /// - name: Header name. - /// - value: Encodable header value. - public func headerFieldAdd( - in headerFields: inout [HeaderField], - strategy: ParameterCodingStrategy, + // | common | get | header field | text | array of dates | required | getRequiredHeaderFieldAsText | + public func getRequiredHeaderFieldAsText( + in headerFields: [HeaderField], name: String, - value: T? - ) throws { - guard let value else { - return - } - if let value = value as? _StringConvertible, - strategy != .codable - { - headerFields.add(name: name, value: value.description) - return - } - let data = try headerFieldEncoder.encode(value) - guard let stringValue = String(data: data, encoding: .utf8) else { - throw RuntimeError.failedToEncodeJSONHeaderIntoString(name: name) - } - headerFields.add(name: name, value: stringValue) + as type: [Date].Type + ) throws -> [Date]? { + try getRequiredHeaderFields( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldTextToDate + ) } - /// Returns the value of the first header field for the given name. - /// - /// Decodes the value from JSON. - /// - Parameters: - /// - headerFields: Collection of header fields to retrieve the field from. - /// - strategy: A hint about which coding strategy to use. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name, if one exists. - public func headerFieldGetOptional( + // | common | get | header field | JSON | codable | optional | getOptionalHeaderFieldAsJSON | + public func getOptionalHeaderFieldAsJSON( in headerFields: [HeaderField], - strategy: ParameterCodingStrategy, name: String, as type: T.Type ) throws -> T? { - guard let stringValue = headerFields.firstValue(name: name) else { - return nil - } - if let myType = T.self as? _StringConvertible.Type, - strategy != .codable - { - return myType.init(stringValue).map { $0 as! T } - } - let data = Data(stringValue.utf8) - return try decoder.decode(T.self, from: data) + try getOptionalHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldJSONToCodable + ) } - /// Returns the first header value for the given (case-insensitive) name. - /// - /// Decodes the value from JSON. - /// - Parameters: - /// - headerFields: Collection of header fields to retrieve the field from. - /// - strategy: A hint about which coding strategy to use. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name. - public func headerFieldGetRequired( + // | common | get | header field | JSON | codable | required | getRequiredHeaderFieldAsJSON | + public func getRequiredHeaderFieldAsJSON( in headerFields: [HeaderField], - strategy: ParameterCodingStrategy, name: String, as type: T.Type - ) throws -> T { - guard - let value = try headerFieldGetOptional( - in: headerFields, - strategy: strategy, - name: name, - as: type - ) - else { - throw RuntimeError.missingRequiredHeader(name) - } - return value + ) throws -> T? { + try getRequiredHeaderField( + in: headerFields, + name: name, + as: type, + convert: convertHeaderFieldJSONToCodable + ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index 0ebdcca2..765a2ad5 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -57,341 +57,297 @@ public extension Converter { throw RuntimeError.unexpectedAcceptHeader(acceptHeader) } - // MARK: Path - - /// Returns a deserialized value for the optional path variable name. - /// - Parameters: - /// - pathParameters: Path parameters where the value might exist. - /// - name: Path variable name. - /// - type: Path variable type. - /// - Returns: Deserialized path variable value, if present. - func pathGetOptional( - in pathParameters: [String: String], - name: String, - as type: T.Type - ) throws -> T? { - guard let untypedValue = pathParameters[name] else { - return nil - } - guard let typedValue = T(untypedValue) else { - throw RuntimeError.failedToDecodePathParameter(name: name, type: String(describing: T.self)) - } - return typedValue - } - - /// Returns a deserialized value for the required path variable name. - /// - Parameters: - /// - pathParameters: Path parameters where the value must exist. - /// - name: Path variable name. - /// - type: Path variable type. - /// - Returns: Deserialized path variable value. - func pathGetRequired( + // | server | get | request path | text | string-convertible | required | getPathParameterAsText | + func getPathParameterAsText( in pathParameters: [String: String], name: String, as type: T.Type ) throws -> T { - guard - let value = try pathGetOptional( - in: pathParameters, - name: name, - as: type - ) - else { - throw RuntimeError.missingRequiredPathParameter(name) - } - return value + try getRequiredRequestPath( + in: pathParameters, + name: name, + as: type, + convert: convertTextToStringConvertible + ) } - // MARK: Query - LosslessStringConvertible - - /// Returns a deserialized value for the the first query item - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value might exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value, if present. - func queryGetOptional( + // | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText | + func getOptionalQueryItemAsText( in queryParameters: [URLQueryItem], name: String, as type: T.Type ) throws -> T? { - guard let untypedValue = queryParameters.first(where: { $0.name == name })?.value else { - return nil - } - guard let typedValue = T(untypedValue) else { - throw RuntimeError.failedToDecodeQueryParameter( - name: name, - type: String(describing: T.self) - ) - } - return typedValue + try getOptionalQueryItem( + in: queryParameters, + name: name, + as: type, + convert: convertTextToStringConvertible + ) } - /// Returns a deserialized value for the the first query item - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value must exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value. - func queryGetRequired( + // | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText | + func getRequiredQueryItemAsText( in queryParameters: [URLQueryItem], name: String, as type: T.Type ) throws -> T { - guard let untypedValue = queryParameters.first(where: { $0.name == name })?.value else { - throw RuntimeError.missingRequiredQueryParameter(name) - } - guard let typedValue = T(untypedValue) else { - throw RuntimeError.failedToDecodeQueryParameter(name: name, type: String(describing: T.self)) - } - return typedValue + try getRequiredQueryItem( + in: queryParameters, + name: name, + as: type, + convert: convertTextToStringConvertible + ) } - // MARK: Query - Date + // | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText | + func getOptionalQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [T].Type + ) throws -> [T]? { + try getOptionalQueryItems( + in: queryParameters, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } - /// Returns a deserialized value for the the first query item - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value might exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value, if present. - func queryGetOptional( + // | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | + func getRequiredQueryItemAsText( + in queryParameters: [URLQueryItem], + name: String, + as type: [T].Type + ) throws -> [T] { + try getRequiredQueryItems( + in: queryParameters, + name: name, + as: type, + convert: convertTextToStringConvertible + ) + } + + // | server | get | request query | text | date | optional | getOptionalQueryItemAsText | + func getOptionalQueryItemAsText( in queryParameters: [URLQueryItem], name: String, as type: Date.Type ) throws -> Date? { - guard let dateString = queryParameters.first(where: { $0.name == name })?.value else { - return nil - } - return try self.configuration.dateTranscoder.decode(dateString) + try getOptionalQueryItem( + in: queryParameters, + name: name, + as: type, + convert: convertTextToDate + ) } - /// Returns a deserialized value for the the first query item - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value must exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value. - func queryGetRequired( + // | server | get | request query | text | date | required | getRequiredQueryItemAsText | + func getRequiredQueryItemAsText( in queryParameters: [URLQueryItem], name: String, as type: Date.Type ) throws -> Date { - guard let dateString = queryParameters.first(where: { $0.name == name })?.value else { - throw RuntimeError.missingRequiredQueryParameter(name) - } - return try self.configuration.dateTranscoder.decode(dateString) + try getRequiredQueryItem( + in: queryParameters, + name: name, + as: type, + convert: convertTextToDate + ) } - // MARK: Query - Array of _StringConvertible - - /// Returns an array of deserialized values for all the query items - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value might exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value, if present. - func queryGetOptional( + // | server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText | + func getOptionalQueryItemAsText( in queryParameters: [URLQueryItem], name: String, - as type: [T].Type - ) throws -> [T]? { - let items: [T] = - try queryParameters - .filter { $0.name == name } - .compactMap { item in - guard let typedValue = T(item.value ?? "") else { - throw RuntimeError.failedToDecodeQueryParameter( - name: name, - type: String(describing: T.self) - ) - } - return typedValue - } - guard !items.isEmpty else { - return nil - } - return items + as type: [Date].Type + ) throws -> [Date]? { + try getOptionalQueryItems( + in: queryParameters, + name: name, + as: type, + convert: convertTextToDate + ) } - /// Returns an array of deserialized values for all the query items - /// found under the provided name. - /// - Parameters: - /// - queryParameters: Query parameters container where the value must exist. - /// - name: Query item name. - /// - type: Query item value type. - /// - Returns: Deserialized query item value. - func queryGetRequired( + // | server | get | request query | text | array of dates | required | getRequiredQueryItemAsText | + func getRequiredQueryItemAsText( in queryParameters: [URLQueryItem], name: String, - as type: [T].Type - ) throws -> [T] { - let items: [T] = - try queryParameters - .filter { $0.name == name } - .map { item in - guard let typedValue = T(item.value ?? "") else { - throw RuntimeError.failedToDecodeQueryParameter(name: name, type: String(describing: T.self)) - } - return typedValue - } - guard !items.isEmpty else { - throw RuntimeError.missingRequiredQueryParameter(name) - } - return items + as type: [Date].Type + ) throws -> [Date] { + try getRequiredQueryItems( + in: queryParameters, + name: name, + as: type, + convert: convertTextToDate + ) } - // MARK: Body - Complex - - /// Gets a deserialized value from body data, if present. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value, if present. - func bodyGetOptional( + // | server | get | request body | text | string-convertible | optional | getOptionalRequestBodyAsText | + func getOptionalRequestBodyAsText( _ type: T.Type, from data: Data?, - strategy: BodyCodingStrategy, transforming transform: (T) -> C ) throws -> C? { - guard let data else { - return nil - } - let decoded: T - if let myType = T.self as? _StringConvertible.Type, - strategy == .string - { - guard - let stringValue = String(data: data, encoding: .utf8), - let decodedValue = myType.init(stringValue) - else { - throw RuntimeError.failedToDecodeBody(type: T.self) - } - decoded = decodedValue as! T - } else { - decoded = try decoder.decode(type, from: data) - } - return transform(decoded) + try getOptionalRequestBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToStringConvertible + ) } - /// Gets a deserialized value from body data. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value. - func bodyGetRequired( + // | server | get | request body | text | string-convertible | required | getRequiredRequestBodyAsText | + func getRequiredRequestBodyAsText( _ type: T.Type, from data: Data?, - strategy: BodyCodingStrategy, transforming transform: (T) -> C + ) throws -> C? { + try getRequiredRequestBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToStringConvertible + ) + } + + // | server | get | request body | text | date | optional | getOptionalRequestBodyAsText | + func getOptionalRequestBodyAsText( + _ type: Date.Type, + from data: Data?, + transforming transform: (Date) -> C + ) throws -> C? { + try getOptionalRequestBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToDate + ) + } + + // | server | get | request body | text | date | required | getRequiredRequestBodyAsText | + func getRequiredRequestBodyAsText( + _ type: Date.Type, + from data: Data?, + transforming transform: (Date) -> C ) throws -> C { - guard let data else { - throw RuntimeError.missingRequiredRequestBody - } - let decoded: T - if let myType = T.self as? _StringConvertible.Type, - strategy == .string - { - guard - let stringValue = String(data: data, encoding: .utf8), - let decodedValue = myType.init(stringValue) - else { - throw RuntimeError.failedToDecodeBody(type: T.self) - } - decoded = decodedValue as! T - } else { - decoded = try decoder.decode(type, from: data) - } - return transform(decoded) + try getRequiredRequestBody( + type, + from: data, + transforming: transform, + convert: convertTextDataToDate + ) } - /// Provides a serialized value for the provided body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headerFields: Header fields container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value. - func bodyAdd( - _ value: C, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data { - let body = transform(value) - headerFields.add(name: "content-type", value: body.contentType) - let bodyValue = body.value - if let value = bodyValue as? _StringConvertible, - body.strategy == .string - { - guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodeBody(type: T.self) - } - return data - } - return try encoder.encode(bodyValue) + // | server | get | request body | JSON | codable | optional | getOptionalRequestBodyAsJSON | + func getOptionalRequestBodyAsJSON( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C + ) throws -> C? { + try getOptionalRequestBody( + type, + from: data, + transforming: transform, + convert: convertJSONToCodable + ) } - // MARK: Body - Data + // | server | get | request body | JSON | codable | required | getRequiredRequestBodyAsJSON | + func getRequiredRequestBodyAsJSON( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C + ) throws -> C { + try getRequiredRequestBody( + type, + from: data, + transforming: transform, + convert: convertJSONToCodable + ) + } - /// Gets a deserialized value from body data, if present. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value, if present. - func bodyGetOptional( + // | server | get | request body | binary | data | optional | getOptionalRequestBodyAsBinary | + func getOptionalRequestBodyAsBinary( _ type: Data.Type, from data: Data?, - strategy: BodyCodingStrategy, - transforming transform: (Data) -> C - ) throws -> C? { - guard let data else { - return nil - } - return transform(data) + transforming transform: (Data) -> Data + ) throws -> Data? { + try getOptionalRequestBody( + type, + from: data, + transforming: transform, + convert: convertBinaryToData + ) } - /// Gets a deserialized value from body data. - /// - Parameters: - /// - type: Type used to decode the data. - /// - data: Encoded body data. - /// - strategy: A hint about which coding strategy to use. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value. - func bodyGetRequired( + // | server | get | request body | binary | data | required | getRequiredRequestBodyAsBinary | + func getRequiredRequestBodyAsBinary( _ type: Data.Type, from data: Data?, - strategy: BodyCodingStrategy, transforming transform: (Data) -> C ) throws -> C { - guard let data else { - throw RuntimeError.missingRequiredRequestBody - } - return transform(data) + try getRequiredRequestBody( + type, + from: data, + transforming: transform, + convert: convertBinaryToData + ) } - /// Provides a serialized value for the provided body value. - /// - Parameters: - /// - value: Encodable value to turn into data. - /// - headers: Headers container where to add the Content-Type header. - /// - transform: Closure for transforming the Encodable value into body content. - /// - Returns: Data for the serialized body value. - func bodyAdd( + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + func setResponseBodyAsText( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setResponseBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertStringConvertibleToTextData + ) + } + + // | server | set | response body | text | date | required | setResponseBodyAsText | + func setResponseBodyAsText( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setResponseBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDateToTextData + ) + } + + // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + func setResponseBodyAsJSON( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setResponseBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertBodyCodableToJSON + ) + } + + // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + func setResponseBodyAsBinary( _ value: C, headerFields: inout [HeaderField], transforming transform: (C) -> EncodableBodyContent ) throws -> Data { - let body = transform(value) - headerFields.add(name: "content-type", value: body.contentType) - return body.value + try setResponseBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDataToBinary + ) } } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 4bb5d3c8..f5daf54f 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -55,7 +55,7 @@ extension Array where Element == HeaderField { } append(.init(name: name, value: value)) } - + /// Adds headers for the provided name and values. /// - Parameters: /// - name: Header name. @@ -92,3 +92,366 @@ extension Array where Element == HeaderField { filter { $0.name.caseInsensitiveCompare(name) == .orderedSame }.map { $0.value } } } + +extension Converter { + + // MARK: Common functions for Converter's SPI helper methods + + func convertStringConvertibleToText( + _ value: T + ) throws -> String { + value.description + } + + func convertStringConvertibleToTextData( + _ value: T + ) throws -> Data { + try Data(convertStringConvertibleToText(value).utf8) + } + + func convertDateToText(_ value: Date) throws -> String { + try configuration.dateTranscoder.encode(value) + } + + func convertDateToTextData(_ value: Date) throws -> Data { + try Data(convertDateToText(value).utf8) + } + + func convertTextToDate(_ stringValue: String) throws -> Date { + try configuration.dateTranscoder.decode(stringValue) + } + + func convertTextDataToDate(_ data: Data) throws -> Date { + let stringValue = String(decoding: data, as: UTF8.self) + return try convertTextToDate(stringValue) + } + + func convertJSONToCodable( + _ data: Data + ) throws -> T { + try decoder.decode(T.self, from: data) + } + + func convertBodyCodableToJSON( + _ value: T + ) throws -> Data { + try encoder.encode(value) + } + + func convertHeaderFieldTextToDate(_ stringValue: String) throws -> Date { + try convertTextToDate(stringValue) + } + + func convertHeaderFieldCodableToJSON( + _ value: T + ) throws -> String { + let data = try headerFieldEncoder.encode(value) + let stringValue = String(decoding: data, as: UTF8.self) + return stringValue + } + + func convertHeaderFieldJSONToCodable( + _ stringValue: String + ) throws -> T { + let data = Data(stringValue.utf8) + return try decoder.decode(T.self, from: data) + } + + func convertTextToStringConvertible( + _ stringValue: String + ) throws -> T { + guard let value = T.init(stringValue) else { + throw RuntimeError.failedToDecodeStringConvertibleValue( + type: String(describing: T.self) + ) + } + return value + } + + func convertTextDataToStringConvertible( + _ data: Data + ) throws -> T { + let stringValue = String(decoding: data, as: UTF8.self) + return try convertTextToStringConvertible(stringValue) + } + + func setHeaderField( + in headerFields: inout [HeaderField], + name: String, + value: T?, + convert: (T) throws -> String + ) throws { + guard let value else { + return + } + headerFields.add( + name: name, + value: try convert(value) + ) + } + + func setHeaderFields( + in headerFields: inout [HeaderField], + name: String, + values: [T]?, + convert: (T) throws -> String + ) throws { + guard let values else { + return + } + for value in values { + headerFields.add( + name: name, + value: try convert(value) + ) + } + } + + func getOptionalHeaderField( + in headerFields: [HeaderField], + name: String, + as type: T.Type, + convert: (String) throws -> T + ) throws -> T? { + guard let stringValue = headerFields.firstValue(name: name) else { + return nil + } + return try convert(stringValue) + } + + func getRequiredHeaderField( + in headerFields: [HeaderField], + name: String, + as type: T.Type, + convert: (String) throws -> T + ) throws -> T { + guard let stringValue = headerFields.firstValue(name: name) else { + throw RuntimeError.missingRequiredHeaderField(name) + } + return try convert(stringValue) + } + + func getOptionalHeaderFields( + in headerFields: [HeaderField], + name: String, + as type: [T].Type, + convert: (String) throws -> T + ) throws -> [T]? { + let values = headerFields.values(name: name) + if values.isEmpty { + return nil + } + return try values.map { value in try convert(value) } + } + + func getRequiredHeaderFields( + in headerFields: [HeaderField], + name: String, + as type: [T].Type, + convert: (String) throws -> T + ) throws -> [T] { + let values = headerFields.values(name: name) + if values.isEmpty { + throw RuntimeError.missingRequiredHeaderField(name) + } + return try values.map { value in try convert(value) } + } + + func setQueryItem( + in request: inout Request, + name: String, + value: T?, + convert: (T) throws -> String + ) throws { + guard let value else { + return + } + request.addQueryItem(name: name, value: try convert(value)) + } + + func setQueryItems( + in request: inout Request, + name: String, + values: [T]?, + convert: (T) throws -> String + ) throws { + guard let values else { + return + } + for value in values { + request.addQueryItem(name: name, value: try convert(value)) + } + } + + func getOptionalQueryItem( + in queryParameters: [URLQueryItem], + name: String, + as type: T.Type, + convert: (String) throws -> T + ) throws -> T? { + guard + let untypedValue = + queryParameters + .first(where: { $0.name == name }) + else { + return nil + } + return try convert(untypedValue.value ?? "") + } + + func getRequiredQueryItem( + in queryParameters: [URLQueryItem], + name: String, + as type: T.Type, + convert: (String) throws -> T + ) throws -> T { + guard + let value = try getOptionalQueryItem( + in: queryParameters, + name: name, + as: type, + convert: convert + ) + else { + throw RuntimeError.missingRequiredQueryParameter(name) + } + return value + } + + func getOptionalQueryItems( + in queryParameters: [URLQueryItem], + 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 ?? "") } + } + + func getRequiredQueryItems( + in queryParameters: [URLQueryItem], + name: String, + as type: [T].Type, + convert: (String) throws -> T + ) throws -> [T] { + guard + let values = try getOptionalQueryItems( + in: queryParameters, + name: name, + as: type, + convert: convert + ), !values.isEmpty + else { + throw RuntimeError.missingRequiredQueryParameter(name) + } + return values + } + + func setRequiredRequestBody( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent, + convert: (T) throws -> Data + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + let convertibleValue = body.value + return try convert(convertibleValue) + } + + func setOptionalRequestBody( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent, + convert: (T) throws -> Data + ) throws -> Data? { + guard let value else { + return nil + } + return try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convert + ) + } + + func getOptionalRequestBody( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C, + convert: (Data) throws -> T + ) throws -> C? { + guard let data else { + return nil + } + let decoded = try convert(data) + return transform(decoded) + } + + func getRequiredRequestBody( + _ type: T.Type, + from data: Data?, + transforming transform: (T) -> C, + convert: (Data) throws -> T + ) throws -> C { + guard + let body = try getOptionalRequestBody( + type, + from: data, + transforming: transform, + convert: convert + ) + else { + throw RuntimeError.missingRequiredRequestBody + } + return body + } + + func getResponseBody( + _ type: T.Type, + from data: Data, + transforming transform: (T) -> C, + convert: (Data) throws -> T + ) throws -> C { + let parsedValue = try convert(data) + let transformedValue = transform(parsedValue) + return transformedValue + } + + func setResponseBody( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent, + convert: (T) throws -> Data + ) throws -> Data { + let body = transform(value) + headerFields.add(name: "content-type", value: body.contentType) + let convertibleValue = body.value + return try convert(convertibleValue) + } + + func convertBinaryToData( + _ binary: Data + ) throws -> Data { + binary + } + + func convertDataToBinary( + _ data: Data + ) throws -> Data { + data + } + + func getRequiredRequestPath( + in pathParameters: [String: String], + name: String, + as type: T.Type, + convert: (String) throws -> T + ) throws -> T { + guard let untypedValue = pathParameters[name] else { + throw RuntimeError.missingRequiredPathParameter(name) + } + return try convert(untypedValue) + } +} diff --git a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift index d7f4302f..10fee7a4 100644 --- a/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/FoundationExtensions.swift @@ -15,11 +15,8 @@ import Foundation extension Data { /// Returns a pretty representation of the Data. - /// - /// First tries to decode it as UTF-8, if that fails, tries ASCII, - /// if that fails too, stringifies the value itself. var pretty: String { - String(data: self, encoding: .utf8) ?? String(data: self, encoding: .ascii) ?? String(describing: self) + String(decoding: self, as: UTF8.self) } /// Returns a prefix of a pretty representation of the Data. @@ -30,7 +27,7 @@ extension Data { extension Request { /// Allows modifying the parsed query parameters of the request. - mutating func mutatingQuery(_ closure: (inout URLComponents) throws -> Void) rethrows { + mutating func mutateQuery(_ closure: (inout URLComponents) throws -> Void) rethrows { var urlComponents: URLComponents = .init() if let query { urlComponents.percentEncodedQuery = query @@ -38,39 +35,25 @@ extension Request { try closure(&urlComponents) query = urlComponents.percentEncodedQuery } + + /// Allows modifying the parsed query parameters of the request. + mutating func addQueryItem(name: String, value: String) { + mutateQuery { urlComponents in + urlComponents.addStringQueryItem(name: name, value: value) + } + } } extension URLComponents { - /// Adds a query item using the provided name and typed value. - /// - Parameters: - /// - name: Query name. - /// - value: Typed value. - mutating func addQueryItem( + + /// Adds the provided name and value to the URL's query. + mutating func addStringQueryItem( name: String, - value: T? + value: String ) { - guard let value = value else { - return - } queryItems = (queryItems ?? []) + [ - .init(name: name, value: value.description) + .init(name: name, value: value) ] } - - /// Adds query items using the provided name and typed values. - /// - Parameters: - /// - name: Query name. - /// - value: Array of typed values. - mutating func addQueryItem( - name: String, - value: [T]? - ) { - guard let items = value else { - return - } - for item in items { - addQueryItem(name: name, value: item) - } - } } diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index fc8a1cec..a1118bfe 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -176,7 +176,7 @@ extension Converter { as: type ) else { - throw RuntimeError.missingRequiredHeader(name) + throw RuntimeError.missingRequiredHeaderField(name) } return value } @@ -202,9 +202,7 @@ extension Converter { return } let data = try headerFieldEncoder.encode(value) - guard let stringValue = String(data: data, encoding: .utf8) else { - throw RuntimeError.failedToEncodeJSONHeaderIntoString(name: name) - } + let stringValue = String(decoding: data, as: UTF8.self) headerFields.add(name: name, value: stringValue) } @@ -253,7 +251,7 @@ extension Converter { as: type ) else { - throw RuntimeError.missingRequiredHeader(name) + throw RuntimeError.missingRequiredHeaderField(name) } return value } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 6cb7ddde..b6407ddd 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -20,24 +20,22 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Miscs case invalidServerURL(String) + // Data conversion + case failedToDecodeStringConvertibleValue(type: String) + // Headers - case missingRequiredHeader(String) + case missingRequiredHeaderField(String) case unexpectedContentTypeHeader(String) case unexpectedAcceptHeader(String) - case failedToEncodeJSONHeaderIntoString(name: String) // Path case missingRequiredPathParameter(String) - case failedToDecodePathParameter(name: String, type: String) // Query case missingRequiredQueryParameter(String) - case failedToDecodeQueryParameter(name: String, type: String) // Body case missingRequiredRequestBody - case failedToEncodeBody(type: Any.Type) - case failedToDecodeBody(type: Any.Type) // Transport/Handler case transportFailed(Error) @@ -53,28 +51,20 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret switch self { case .invalidServerURL(let string): return "Invalid server URL: \(string)" - case .missingRequiredHeader(let name): - return "The required header named '\(name)' is missing." + case .failedToDecodeStringConvertibleValue(let string): + return "Failed to decode a value of type '\(string)'." + case .missingRequiredHeaderField(let name): + return "The required header field named '\(name)' is missing." case .unexpectedContentTypeHeader(let contentType): return "Unexpected Content-Type header: \(contentType)" case .unexpectedAcceptHeader(let accept): return "Unexpected Accept header: \(accept)" - case .failedToEncodeJSONHeaderIntoString(let name): - return "Failed to encode JSON header named '\(name)' into a string" case .missingRequiredPathParameter(let name): return "Missing required path parameter named: \(name)" - case .failedToDecodePathParameter(let name, let type): - return "Failed to decode path parameter named '\(name)' to type \(type)." case .missingRequiredQueryParameter(let name): return "Missing required query parameter named: \(name)" - case .failedToDecodeQueryParameter(let name, let type): - return "Failed to decode query parameter named '\(name)' to type \(type)." case .missingRequiredRequestBody: return "Missing required request body" - case .failedToEncodeBody(type: let type): - return "Failed to encode a body of type \(type) into data" - case .failedToDecodeBody(type: let type): - return "Failed to decode a body of type \(type) from data" case .transportFailed(let underlyingError): return "Transport failed with error: \(underlyingError.localizedDescription)" case .handlerFailed(let underlyingError): diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_StringConvertible.swift b/Tests/OpenAPIRuntimeTests/Base/Test_StringConvertible.swift new file mode 100644 index 00000000..7eee5776 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_StringConvertible.swift @@ -0,0 +1,33 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@_spi(Generated)@testable import OpenAPIRuntime + +final class Test_StringConvertible: XCTestCase { + + func testConformances() throws { + let values: [(any _StringConvertible, String)] = [ + ("hello" as String, "hello"), + (1 as Int, "1"), + (1 as Int32, "1"), + (1 as Int64, "1"), + (0.5 as Float, "0.5"), + (0.5 as Double, "0.5"), + (true as Bool, "true"), + ] + for (value, stringified) in values { + XCTAssertEqual(value.description, stringified) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift index e9f3d686..f6a752b3 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_CodableExtensions.swift @@ -45,23 +45,27 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let data = #""" + let data = Data( + #""" { "bar": "hi" } """# - .data(using: .utf8)! + .utf8 + ) _ = try testDecoder.decode(Foo.self, from: data) } do { - let data = #""" + let data = Data( + #""" { "bar": "hi", "baz": "oh no" } """# - .data(using: .utf8)! + .utf8 + ) XCTAssertThrowsError(try testDecoder.decode(Foo.self, from: data)) { error in let err = try! XCTUnwrap(error as? DecodingError) guard case let .dataCorrupted(context) = err else { @@ -97,25 +101,29 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let data = #""" + let data = Data( + #""" { "bar": "hi" } """# - .data(using: .utf8)! + .utf8 + ) let value = try testDecoder.decode(Foo.self, from: data) XCTAssertEqual(value.bar, "hi") XCTAssertEqual(value.additionalProperties.value.count, 0) } do { - let data = #""" + let data = Data( + #""" { "bar": "hi", "baz": "oh no" } """# - .data(using: .utf8)! + .utf8 + ) let value = try testDecoder.decode(Foo.self, from: data) XCTAssertEqual(value.bar, "hi") let additionalProperties = value.additionalProperties.value @@ -144,25 +152,29 @@ final class Test_CodableExtensions: Test_Runtime { } do { - let data = #""" + let data = Data( + #""" { "bar": "hi" } """# - .data(using: .utf8)! + .utf8 + ) let value = try JSONDecoder().decode(Foo.self, from: data) XCTAssertEqual(value.bar, "hi") XCTAssertEqual(value.additionalProperties.count, 0) } do { - let data = #""" + let data = Data( + #""" { "bar": "hi", "baz": 1 } """# - .data(using: .utf8)! + .utf8 + ) let value = try JSONDecoder().decode(Foo.self, from: data) XCTAssertEqual(value.bar, "hi") let additionalProperties = value.additionalProperties @@ -194,7 +206,7 @@ final class Test_CodableExtensions: Test_Runtime { ) let data = try testEncoder.encode(value) XCTAssertEqual( - String(data: data, encoding: .utf8)!, + String(decoding: data, as: UTF8.self), #""" { "bar" : "hi" @@ -213,7 +225,7 @@ final class Test_CodableExtensions: Test_Runtime { ) let data = try testEncoder.encode(value) XCTAssertEqual( - String(data: data, encoding: .utf8)!, + String(decoding: data, as: UTF8.self), #""" { "bar" : "hi", @@ -248,7 +260,7 @@ final class Test_CodableExtensions: Test_Runtime { ) let data = try testEncoder.encode(value) XCTAssertEqual( - String(data: data, encoding: .utf8)!, + String(decoding: data, as: UTF8.self), #""" { "bar" : "hi" @@ -266,7 +278,7 @@ final class Test_CodableExtensions: Test_Runtime { ) let data = try testEncoder.encode(value) XCTAssertEqual( - String(data: data, encoding: .utf8)!, + String(decoding: data, as: UTF8.self), #""" { "bar" : "hi", diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 8e45fa2d..bcdb201a 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -16,11 +16,24 @@ import XCTest final class Test_ClientConverterExtensions: Test_Runtime { - // MARK: Query - LosslessStringConvertible + // MARK: Converter helper methods - func testQueryAdd_string() throws { + // | client | set | request path | text | string-convertible | required | renderedRequestPath | + func test_renderedRequestPath_stringConvertible() throws { + let renderedPath = try converter.renderedRequestPath( + template: "/items/{}/detail/{}", + parameters: [ + 1 as Int, + "foo" as String, + ] + ) + XCTAssertEqual(renderedPath, "/items/1/detail/foo") + } + + // | client | set | request query | text | string-convertible | both | setQueryItemAsText | + func test_setQueryItemAsText_stringConvertible() throws { var request = testRequest - try converter.queryAdd( + try converter.setQueryItemAsText( in: &request, name: "search", value: "foo" @@ -28,9 +41,9 @@ final class Test_ClientConverterExtensions: Test_Runtime { XCTAssertEqual(request.query, "search=foo") } - func testQueryAdd_string_needsEncoding() throws { + func test_setQueryItemAsText_stringConvertible_needsEncoding() throws { var request = testRequest - try converter.queryAdd( + try converter.setQueryItemAsText( in: &request, name: "search", value: "h%llo" @@ -38,301 +51,273 @@ final class Test_ClientConverterExtensions: Test_Runtime { XCTAssertEqual(request.query, "search=h%25llo") } - // MARK: Query - Date - - func testQueryAdd_date() throws { + // | client | set | request query | text | array of string-convertibles | both | setQueryItemAsText | + func test_setQueryItemAsText_arrayOfStringConvertibles() throws { var request = testRequest - try converter.queryAdd( + try converter.setQueryItemAsText( in: &request, - name: "since", - value: testDate + name: "search", + value: ["foo", "bar"] ) - XCTAssertEqual(request.query, "since=2023-01-18T10:04:11Z") + XCTAssertEqual(request.query, "search=foo&search=bar") } - // MARK: Query - Array of LosslessStringConvertibles - - func testQueryAdd_arrayOfStrings() throws { + // | client | set | request query | text | date | both | setQueryItemAsText | + func test_setQueryItemAsText_date() throws { var request = testRequest - try converter.queryAdd( + try converter.setQueryItemAsText( in: &request, - name: "id", - value: ["1", "2"] - ) - XCTAssertEqual(request.query, "id=1&id=2") - } - - // MARK: Body - - func testBodyGetStruct_strategyCodable_success() throws { - let body = try converter.bodyGet( - TestPet.self, - from: testStructData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func testBodyGetData_strategyData_success() throws { - let body = try converter.bodyGet( - Data.self, - from: testStructData, - strategy: .data, - transforming: { $0 } - ) - XCTAssertEqual(body, testStructData) - } - - func testBodyGetString_strategyData_success() throws { - let body = try converter.bodyGet( - String.self, - from: testQuotedStringData, - strategy: .data, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetString_strategyCodable_success() throws { - let body = try converter.bodyGet( - String.self, - from: testQuotedStringData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetString_strategyString_success() throws { - let body = try converter.bodyGet( - String.self, - from: testStringData, - strategy: .string, - transforming: { $0 } + name: "search", + value: testDate ) - XCTAssertEqual(body, testString) + XCTAssertEqual(request.query, "search=2023-01-18T10:04:11Z") } - func testBodyGetString_strategyInt_failure() throws { - XCTAssertThrowsError( - try converter.bodyGet( - Int.self, - from: testStringData, - strategy: .string, - transforming: { $0 } - ), - "Was expected to throw error on invalid int", - { error in - guard - let err = error as? RuntimeError, - case .failedToDecodeBody(let type) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual("\(type)", "\(Int.self)") - } + // | client | set | request query | text | array of dates | both | setQueryItemAsText | + func test_setQueryItemAsText_arrayOfDates() throws { + var request = testRequest + try converter.setQueryItemAsText( + in: &request, + name: "search", + value: [testDate, testDate] ) + XCTAssertEqual(request.query, "search=2023-01-18T10:04:11Z&search=2023-01-18T10:04:11Z") } - func testBodyAddComplexOptional_success() throws { + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + func test_setOptionalRequestBodyAsText_stringConvertible() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testStruct, + let body = try converter.setOptionalRequestBodyAsText( + testString, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "application/json", - strategy: .codable + value: value, + contentType: "text/plain" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .init(name: "content-type", value: "text/plain") ] ) } - func testBodyAddComplexOptional_nil() throws { - let value: TestPet? = nil - var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - value, - headerFields: &headerFields, - transforming: { _ -> EncodableBodyContent in fatalError("Unreachable") } - ) - XCTAssertNil(data) - XCTAssertEqual(headerFields, []) - } - - func testBodyAddComplexRequired_success() throws { + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + func test_setRequiredRequestBodyAsText_stringConvertible() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testStruct, + let body = try converter.setRequiredRequestBodyAsText( + testString, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "application/json", - strategy: .codable + value: value, + contentType: "text/plain" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .init(name: "content-type", value: "text/plain") ] ) } - func testBodyAddDataOptional_success() throws { + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + func test_setOptionalRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testStructPrettyData, + let body = try converter.setOptionalRequestBodyAsText( + testDate, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "application/octet-stream", - strategy: .data + value: value, + contentType: "text/plain" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testDateStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .init(name: "content-type", value: "text/plain") ] ) } - func testBodyAddDataOptional_nil() throws { - let value: Data? = nil + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + func test_setRequiredRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - value, + let body = try converter.setRequiredRequestBodyAsText( + testDate, headerFields: &headerFields, - transforming: { _ -> EncodableBodyContent in fatalError("Unreachable") } + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(body, testDateStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] ) - XCTAssertNil(data) - XCTAssertEqual(headerFields, []) } - func testBodyAddDataRequired_success() throws { + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + func test_setOptionalRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testStructPrettyData, + let body = try converter.setOptionalRequestBodyAsJSON( + testStruct, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "application/octet-stream", - strategy: .data + value: value, + contentType: "application/json" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testStructPrettyData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyAddStringOptional_strategyString_success() throws { + func test_setOptionalRequestBodyAsJSON_codable_string() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( + let body = try converter.setOptionalRequestBodyAsJSON( testString, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "text/plain", - strategy: .string + value: value, + contentType: "application/json" ) } ) - XCTAssertEqual(data, testStringData) + XCTAssertEqual(body, testQuotedStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "text/plain") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyAddStringOptional_strategyCodable_success() throws { + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + func test_setRequiredRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testString, + let body = try converter.setRequiredRequestBodyAsJSON( + testStruct, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "text/plain", - strategy: .codable + value: value, + contentType: "application/json" ) } ) - XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual(body, testStructPrettyData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "text/plain") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyAddStringRequired_strategyString_success() throws { + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + func test_setOptionalRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testString, + let body = try converter.setOptionalRequestBodyAsBinary( + testStringData, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "text/plain", - strategy: .string + value: value, + contentType: "application/octet-stream" ) } ) - XCTAssertEqual(data, testStringData) + XCTAssertEqual(body, testStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "text/plain") + .init(name: "content-type", value: "application/octet-stream") ] ) } - func testBodyAddStringRequired_strategyCodable_success() throws { + // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + func test_setRequiredRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testString, + let body = try converter.setRequiredRequestBodyAsBinary( + testStringData, headerFields: &headerFields, - transforming: { + transforming: { value in .init( - value: $0, - contentType: "text/plain", - strategy: .codable + value: value, + contentType: "application/octet-stream" ) } ) - XCTAssertEqual(data, testQuotedStringData) + XCTAssertEqual(body, testStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "text/plain") + .init(name: "content-type", value: "application/octet-stream") ] ) } + + // | client | get | response body | text | string-convertible | required | getResponseBodyAsText | + func test_getResponseBodyAsText_stringConvertible() throws { + let value = try converter.getResponseBodyAsText( + String.self, + from: testStringData, + transforming: { $0 } + ) + XCTAssertEqual(value, testString) + } + + // | client | get | response body | text | date | required | getResponseBodyAsText | + func test_getResponseBodyAsText_date() throws { + let value = try converter.getResponseBodyAsText( + Date.self, + from: testDateStringData, + transforming: { $0 } + ) + XCTAssertEqual(value, testDate) + } + + // | client | get | response body | JSON | codable | required | getResponseBodyAsJSON | + func test_getResponseBodyAsJSON_codable() throws { + let value = try converter.getResponseBodyAsJSON( + TestPet.self, + from: testStructData, + transforming: { $0 } + ) + XCTAssertEqual(value, testStruct) + } + + // | client | get | response body | binary | data | required | getResponseBodyAsBinary | + func test_getResponseBodyAsBinary_data() throws { + let value = try converter.getResponseBodyAsBinary( + Data.self, + from: testStringData, + transforming: { $0 } + ) + XCTAssertEqual(value, testStringData) + } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index ced9bf5e..9ba025ec 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -16,76 +16,6 @@ import XCTest final class Test_CommonConverterExtensions: Test_Runtime { - // MARK: [HeaderField] extension - - func testHeaderFields_add_string() throws { - var headerFields: [HeaderField] = [] - headerFields.add(name: "foo", value: "bar") - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: "bar") - ] - ) - } - - func testHeaderFields_add_nil() throws { - var headerFields: [HeaderField] = [] - let value: String? = nil - headerFields.add(name: "foo", value: value) - XCTAssertEqual(headerFields, []) - } - - func testHeaderFields_firstValue_found() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - XCTAssertEqual(headerFields.firstValue(name: "foo"), "bar") - } - - func testHeaderFields_firstValue_nil() throws { - let headerFields: [HeaderField] = [] - XCTAssertNil(headerFields.firstValue(name: "foo")) - } - - func testHeaderFields_values() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar"), - .init(name: "foo", value: "baz"), - ] - XCTAssertEqual(headerFields.values(name: "foo"), ["bar", "baz"]) - } - - func testHeaderFields_removeAll_noMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_oneMatch() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - - func testHeaderFields_removeAll_manyMatches() throws { - var headerFields: [HeaderField] = [ - .init(name: "one", value: "one"), - .init(name: "three", value: "3"), - .init(name: "two", value: "two"), - .init(name: "three", value: "three"), - ] - headerFields.removeAll(named: "three") - XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) - } - // MARK: Miscs func testValidateContentType_match() throws { @@ -145,11 +75,13 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderFieldsAdd_string_strategyDeferredToType() throws { + // MARK: Converter helper methods + + // | common | set | header field | text | string-convertible | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_stringConvertible() throws { var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &headerFields, - strategy: .deferredToType, name: "foo", value: "bar" ) @@ -161,301 +93,218 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderFieldsAdd_string_strategyString() throws { + // | common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_arrayOfStringConvertible() throws { var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &headerFields, - strategy: .string, name: "foo", - value: "bar" + value: ["bar", "baz"] as [String] ) XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "bar") + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), ] ) } - func testHeaderFieldsAdd_string_strategyCodable() throws { + // | common | set | header field | text | date | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_date() throws { var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( + try converter.setHeaderFieldAsText( in: &headerFields, - strategy: .codable, name: "foo", - value: "bar" + value: testDate ) XCTAssertEqual( headerFields, [ - .init(name: "foo", value: "\"bar\"") + .init(name: "foo", value: testDateString) ] ) } - func testHeaderFieldsGetOptional_string_strategyDeferredToType() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - let value = try converter.headerFieldGetOptional( - in: headerFields, - strategy: .deferredToType, + // | common | set | header field | text | array of dates | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_arrayOfDates() throws { + var headerFields: [HeaderField] = [] + try converter.setHeaderFieldAsText( + in: &headerFields, name: "foo", - as: String.self + value: [testDate, testDate] + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: testDateString), + .init(name: "foo", value: testDateString), + ] + ) + } + + // | common | set | header field | JSON | codable | both | setHeaderFieldAsJSON | + func test_setHeaderFieldAsJSON_codable() throws { + var headerFields: [HeaderField] = [] + try converter.setHeaderFieldAsJSON( + in: &headerFields, + name: "foo", + value: testStruct + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: testStructString) + ] + ) + } + + func test_setHeaderFieldAsJSON_codable_string() throws { + var headerFields: [HeaderField] = [] + try converter.setHeaderFieldAsJSON( + in: &headerFields, + name: "foo", + value: "hello" + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "\"hello\"") + ] ) - XCTAssertEqual(value, "bar") } - func testHeaderFieldsGetOptional_string_strategyString() throws { + // | common | get | header field | text | string-convertible | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_stringConvertible() throws { let headerFields: [HeaderField] = [ .init(name: "foo", value: "bar") ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, name: "foo", as: String.self ) XCTAssertEqual(value, "bar") } - func testHeaderFieldsGetOptional_string_strategyCodable() throws { + // | common | get | header field | text | string-convertible | required | getRequiredHeaderFieldAsText | + func test_getRequiredHeaderFieldAsText_stringConvertible() throws { let headerFields: [HeaderField] = [ .init(name: "foo", value: "bar") ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getRequiredHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, name: "foo", as: String.self ) XCTAssertEqual(value, "bar") } - func testHeaderFieldsGetOptional_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( + // | common | get | header field | text | array of string-convertibles | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_arrayOfStringConvertibles() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), + ] + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, name: "foo", - as: String.self + as: [String].self ) - XCTAssertNil(value) + XCTAssertEqual(value, ["bar", "baz"]) } - func testHeaderFieldsGetRequired_string() throws { + // | common | get | header field | text | array of string-convertibles | required | getRequiredHeaderFieldAsText | + func test_getRequiredHeaderFieldAsText_arrayOfStringConvertibles() throws { let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), ] - let value = try converter.headerFieldGetRequired( + let value = try converter.getRequiredHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, name: "foo", - as: String.self + as: [String].self ) - XCTAssertEqual(value, "bar") + XCTAssertEqual(value, ["bar", "baz"]) } - func testHeaderFieldsGetRequired_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - strategy: .deferredToType, - name: "foo", - as: String.self - ), - "Was expected to throw error on missing required header", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredHeader(let name) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "foo") - } - ) - } - - // MARK: [HeaderField] - (Date) - - func testHeaderFieldsAdd_date() throws { - var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( - in: &headerFields, - strategy: .deferredToType, - name: "since", - value: testDate - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "since", value: testDateString) - ] - ) - } - - func testHeaderFieldsAdd_date_nil() throws { - var headerFields: [HeaderField] = [] - let date: Date? = nil - try converter.headerFieldAdd( - in: &headerFields, - strategy: .deferredToType, - name: "since", - value: date - ) - XCTAssertEqual(headerFields, []) - } - - func testHeaderFieldsGetOptional_date() throws { + // | common | get | header field | text | date | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_date() throws { let headerFields: [HeaderField] = [ - .init(name: "since", value: testDateString) + .init(name: "foo", value: testDateString) ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, - name: "since", + name: "foo", as: Date.self ) XCTAssertEqual(value, testDate) } - func testHeaderFieldsGetOptional_date_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( - in: headerFields, - strategy: .deferredToType, - name: "since", - as: Date.self - ) - XCTAssertNil(value) - } - - func testHeaderFieldsGetRequired_date() throws { + // | common | get | header field | text | date | required | getRequiredHeaderFieldAsText | + func test_getRequiredHeaderFieldAsText_date() throws { let headerFields: [HeaderField] = [ - .init(name: "since", value: testDateString) + .init(name: "foo", value: testDateString) ] - let value = try converter.headerFieldGetRequired( + let value = try converter.getRequiredHeaderFieldAsText( in: headerFields, - strategy: .deferredToType, - name: "since", + name: "foo", as: Date.self ) XCTAssertEqual(value, testDate) } - func testHeaderFieldsGetRequired_date_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - strategy: .deferredToType, - name: "since", - as: Date.self - ), - "Was expected to throw error on missing required header", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredHeader(let name) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "since") - } - ) - } - - // MARK: [HeaderField] - Complex - - func testHeaderFieldsAddComplex_struct() throws { - var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( - in: &headerFields, - strategy: .deferredToType, + // | common | get | header field | text | array of dates | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_arrayOfDates() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: testDateString), + .init(name: "foo", value: testDateString), + ] + let value = try converter.getOptionalHeaderFieldAsText( + in: headerFields, name: "foo", - value: testStruct - ) - XCTAssertEqual( - headerFields, - [ - .init(name: "foo", value: testStructString) - ] + as: [Date].self ) + XCTAssertEqual(value, [testDate, testDate]) } - func testHeaderFieldsAddComplex_nil() throws { - var headerFields: [HeaderField] = [] - let value: TestPet? = nil - try converter.headerFieldAdd( - in: &headerFields, - strategy: .deferredToType, + // | common | get | header field | text | array of dates | required | getRequiredHeaderFieldAsText | + func test_getRequiredHeaderFieldAsText_arrayOfDates() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: testDateString), + .init(name: "foo", value: testDateString), + ] + let value = try converter.getRequiredHeaderFieldAsText( + in: headerFields, name: "foo", - value: value + as: [Date].self ) - XCTAssertEqual(headerFields, []) + XCTAssertEqual(value, [testDate, testDate]) } - func testHeaderFieldsGetComplexOptional_struct() throws { + // | common | get | header field | JSON | codable | optional | getOptionalHeaderFieldAsJSON | + func test_getOptionalHeaderFieldAsJSON_codable() throws { let headerFields: [HeaderField] = [ - .init(name: "pet", value: testStructString) + .init(name: "foo", value: testStructString) ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getOptionalHeaderFieldAsJSON( in: headerFields, - strategy: .deferredToType, - name: "pet", + name: "foo", as: TestPet.self ) XCTAssertEqual(value, testStruct) } - func testHeaderFieldsGetComplexOptional_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( - in: headerFields, - strategy: .deferredToType, - name: "pet", - as: TestPet.self - ) - XCTAssertNil(value) - } - - func testHeaderFieldsGetComplexRequired_struct() throws { + // | common | get | header field | JSON | codable | required | getRequiredHeaderFieldAsJSON | + func test_getRequiredHeaderFieldAsJSON_codable() throws { let headerFields: [HeaderField] = [ - .init(name: "pet", value: testStructString) + .init(name: "foo", value: testStructString) ] - let value = try converter.headerFieldGetRequired( + let value = try converter.getRequiredHeaderFieldAsJSON( in: headerFields, - strategy: .deferredToType, - name: "pet", + name: "foo", as: TestPet.self ) XCTAssertEqual(value, testStruct) } - - func testHeaderFieldsGetComplexRequired_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - strategy: .deferredToType, - name: "pet", - as: TestPet.self - ), - "Was expected to throw error on missing required header", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredHeader(let name) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "pet") - } - ) - } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index fac45447..85bd541c 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -18,7 +18,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { // MARK: Miscs - func testValidateAccept_missing() throws { + func testValidateAccept() throws { let emptyHeaders: [HeaderField] = [] let wildcard: [HeaderField] = [ .init(name: "accept", value: "*/*") @@ -101,13 +101,14 @@ final class Test_ServerConverterExtensions: Test_Runtime { } } - // MARK: Path + // MARK: Converter helper methods - func testPathGetOptional_string() throws { + // | server | get | request path | text | string-convertible | required | getPathParameterAsText | + func test_getPathParameterAsText_stringConvertible() throws { let path: [String: String] = [ "foo": "bar" ] - let value = try converter.pathGetOptional( + let value = try converter.getPathParameterAsText( in: path, name: "foo", as: String.self @@ -115,84 +116,12 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(value, "bar") } - func testPathGetOptional_intMismatch() throws { - let path: [String: String] = [ - "foo": "bar" - ] - XCTAssertThrowsError( - try converter.pathGetOptional( - in: path, - name: "foo", - as: Int.self - ), - "Expected conversion from string to Int to throw", - { error in - guard - let err = error as? RuntimeError, - case let .failedToDecodePathParameter(name, type) = err - else { - XCTFail("Unexpected error thrown: \(error)") - return - } - XCTAssertEqual(name, "foo") - XCTAssertEqual(type, "Int") - } - ) - } - - func testPathGetOptional_nil() throws { - let path: [String: String] = [:] - let value = try converter.pathGetOptional( - in: path, - name: "foo", - as: String.self - ) - XCTAssertNil(value) - } - - func testPathGetRequired_string() throws { - let path: [String: String] = [ - "foo": "bar" - ] - let value = try converter.pathGetRequired( - in: path, - name: "foo", - as: String.self - ) - XCTAssertEqual(value, "bar") - } - - func testPathGetRequired_missing() throws { - let path: [String: String] = [ - "foo": "bar" - ] - XCTAssertThrowsError( - try converter.pathGetRequired( - in: path, - name: "pet", - as: String.self - ), - "Was expected to throw error on missing required path parameter", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredPathParameter(let name) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "pet") - } - ) - } - - // MARK: Query - LosslessStringConvertible - - func testQueryGetOptional_string() throws { + // | server | get | request query | text | string-convertible | optional | getOptionalQueryItemAsText | + func test_getOptionalQueryItemAsText_stringConvertible() throws { let query: [URLQueryItem] = [ .init(name: "search", value: "foo") ] - let value = try converter.queryGetOptional( + let value = try converter.getOptionalQueryItemAsText( in: query, name: "search", as: String.self @@ -200,364 +129,226 @@ final class Test_ServerConverterExtensions: Test_Runtime { XCTAssertEqual(value, "foo") } - func testQueryGetOptional_mismatch() throws { + // | server | get | request query | text | string-convertible | required | getRequiredQueryItemAsText | + func test_getRequiredQueryItemAsText_stringConvertible() throws { let query: [URLQueryItem] = [ .init(name: "search", value: "foo") ] - XCTAssertThrowsError( - try converter.queryGetOptional( - in: query, - name: "search", - as: Int.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? RuntimeError, - case let .failedToDecodeQueryParameter(name, type) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "search") - XCTAssertEqual(type, "Int") - } - ) - } - - func testQueryGetOptional_nil() throws { - let query: [URLQueryItem] = [] - let value = try converter.queryGetOptional( + let value = try converter.getRequiredQueryItemAsText( in: query, name: "search", as: String.self ) - XCTAssertNil(value) + XCTAssertEqual(value, "foo") } - func testQueryGetRequired_string() throws { + // | server | get | request query | text | array of string-convertibles | optional | getOptionalQueryItemAsText | + func test_getOptionalQueryItemAsText_arrayOfStringConvertibles() throws { let query: [URLQueryItem] = [ - .init(name: "search", value: "foo") + .init(name: "search", value: "foo"), + .init(name: "search", value: "bar"), ] - let value = try converter.queryGetRequired( + let value = try converter.getOptionalQueryItemAsText( in: query, name: "search", - as: String.self + as: [String].self ) - XCTAssertEqual(value, "foo") + XCTAssertEqual(value, ["foo", "bar"]) } - func testQueryGetRequired_mismatch() throws { + // | server | get | request query | text | array of string-convertibles | required | getRequiredQueryItemAsText | + func test_getRequiredQueryItemAsText_arrayOfStringConvertibles() throws { let query: [URLQueryItem] = [ - .init(name: "search", value: "foo") + .init(name: "search", value: "foo"), + .init(name: "search", value: "bar"), ] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "search", - as: Int.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? RuntimeError, - case let .failedToDecodeQueryParameter(name, type) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "search") - XCTAssertEqual(type, "Int") - } - ) - } - - func testQueryGetRequired_missing() throws { - let query: [URLQueryItem] = [] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "search", - as: String.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredQueryParameter(let queryKey) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(queryKey, "search") - } + let value = try converter.getRequiredQueryItemAsText( + in: query, + name: "search", + as: [String].self ) + XCTAssertEqual(value, ["foo", "bar"]) } - // MARK: Query - Date - - func testQueryGetOptional_date() throws { + // | server | get | request query | text | date | optional | getOptionalQueryItemAsText | + func test_getOptionalQueryItemAsText_date() throws { let query: [URLQueryItem] = [ - .init(name: "since", value: testDateString) + .init(name: "search", value: testDateString) ] - let value = try converter.queryGetOptional( + let value = try converter.getOptionalQueryItemAsText( in: query, - name: "since", + name: "search", as: Date.self ) XCTAssertEqual(value, testDate) } - func testQueryGetOptional_date_invalid() throws { + // | server | get | request query | text | date | required | getRequiredQueryItemAsText | + func test_getRequiredQueryItemAsText_date() throws { let query: [URLQueryItem] = [ - .init(name: "since", value: "invaliddate") + .init(name: "search", value: testDateString) ] - XCTAssertThrowsError( - try converter.queryGetOptional( - in: query, - name: "since", - as: Date.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? DecodingError, - case let .dataCorrupted(context) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(context.codingPath.map(\.description), []) - // Do not check the exact string of the error as that's not - // controlled by us - just ensure it's not empty. - XCTAssertFalse(context.debugDescription.isEmpty) - XCTAssertNil(context.underlyingError) - } - ) - } - - func testQueryGetOptional_date_nil() throws { - let query: [URLQueryItem] = [] - let value = try converter.queryGetOptional( + let value = try converter.getRequiredQueryItemAsText( in: query, - name: "since", + name: "search", as: Date.self ) - XCTAssertNil(value) + XCTAssertEqual(value, testDate) } - func testQueryGetRequired_date() throws { + // | server | get | request query | text | array of dates | optional | getOptionalQueryItemAsText | + func test_getOptionalQueryItemAsText_arrayOfDates() throws { let query: [URLQueryItem] = [ - .init(name: "since", value: testDateString) + .init(name: "search", value: testDateString), + .init(name: "search", value: testDateString), ] - let value = try converter.queryGetRequired( + let value = try converter.getOptionalQueryItemAsText( in: query, - name: "since", - as: Date.self + name: "search", + as: [Date].self ) - XCTAssertEqual(value, testDate) + XCTAssertEqual(value, [testDate, testDate]) } - func testQueryGetRequired_date_invalid() throws { + // | server | get | request query | text | array of dates | required | getRequiredQueryItemAsText | + func test_getRequiredQueryItemAsText_arrayOfDates() throws { let query: [URLQueryItem] = [ - .init(name: "since", value: "invaliddate") + .init(name: "search", value: testDateString), + .init(name: "search", value: testDateString), ] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "since", - as: Date.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? DecodingError, - case let .dataCorrupted(context) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(context.codingPath.map(\.description), []) - // Do not check the exact string of the error as that's not - // controlled by us - just ensure it's not empty. - XCTAssertFalse(context.debugDescription.isEmpty) - XCTAssertNil(context.underlyingError) - } + let value = try converter.getRequiredQueryItemAsText( + in: query, + name: "search", + as: [Date].self ) + XCTAssertEqual(value, [testDate, testDate]) } - func testQueryGetRequired_date_missing() throws { - let query: [URLQueryItem] = [] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "since", - as: Date.self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredQueryParameter(let queryKey) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(queryKey, "since") - } + // | server | get | request body | text | string-convertible | optional | getOptionalRequestBodyAsText | + func test_getOptionalRequestBodyAsText_stringConvertible() throws { + let body = try converter.getOptionalRequestBodyAsText( + String.self, + from: testStringData, + transforming: { $0 } ) + XCTAssertEqual(body, testString) } - // MARK: Query - Array of LosslessStringConvertibles + // | server | get | request body | text | string-convertible | required | getRequiredRequestBodyAsText | + func test_getRequiredRequestBodyAsText_stringConvertible() throws { + let body = try converter.getRequiredRequestBodyAsText( + String.self, + from: testStringData, + transforming: { $0 } + ) + XCTAssertEqual(body, testString) + } - func testQueryGetOptional_array() throws { - let query: [URLQueryItem] = [ - .init(name: "id", value: "1"), - .init(name: "id", value: "2"), - ] - let value = try converter.queryGetOptional( - in: query, - name: "id", - as: [String].self + // | server | get | request body | text | date | optional | getOptionalRequestBodyAsText | + func test_getOptionalRequestBodyAsText_date() throws { + let body = try converter.getOptionalRequestBodyAsText( + Date.self, + from: testDateStringData, + transforming: { $0 } ) - XCTAssertEqual(value, ["1", "2"]) + XCTAssertEqual(body, testDate) } - func testQueryGetOptional_array_mismatch() throws { - let query: [URLQueryItem] = [ - .init(name: "id", value: "1"), - .init(name: "id", value: "foo"), - ] - XCTAssertThrowsError( - try converter.queryGetOptional( - in: query, - name: "id", - as: [Int].self - ), - "Was expected to throw error on mismatched query", - { error in - guard - let err = error as? RuntimeError, - case let .failedToDecodeQueryParameter(name, type) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "id") - XCTAssertEqual(type, "Int") - } + // | server | get | request body | text | date | required | getRequiredRequestBodyAsText | + func test_getRequiredRequestBodyAsText_date() throws { + let body = try converter.getRequiredRequestBodyAsText( + Date.self, + from: testDateStringData, + transforming: { $0 } ) + XCTAssertEqual(body, testDate) } - func testQueryGetOptional_array_nil() throws { - let query: [URLQueryItem] = [] - let value = try converter.queryGetOptional( - in: query, - name: "id", - as: [String].self + // | server | get | request body | JSON | codable | optional | getOptionalRequestBodyAsJSON | + func test_getOptionalRequestBodyAsJSON_codable() throws { + let body = try converter.getOptionalRequestBodyAsJSON( + TestPet.self, + from: testStructData, + transforming: { $0 } ) - XCTAssertNil(value) + XCTAssertEqual(body, testStruct) } - func testQueryGetRequired_array() throws { - let query: [URLQueryItem] = [ - .init(name: "id", value: "1"), - .init(name: "id", value: "2"), - ] - let value = try converter.queryGetRequired( - in: query, - name: "id", - as: [String].self + func test_getOptionalRequestBodyAsJSON_codable_string() throws { + let body = try converter.getOptionalRequestBodyAsJSON( + String.self, + from: testQuotedStringData, + transforming: { $0 } ) - XCTAssertEqual(value, ["1", "2"]) + XCTAssertEqual(body, testString) } - func testQueryGetRequired_array_mismatch() throws { - let query: [URLQueryItem] = [ - .init(name: "id", value: "1"), - .init(name: "id", value: "foo"), - ] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "id", - as: [Int].self - ), - "Was expected to throw error on mismatched query", - { error in - guard - let err = error as? RuntimeError, - case let .failedToDecodeQueryParameter(name, type) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(name, "id") - XCTAssertEqual(type, "Int") - } + // | server | get | request body | JSON | codable | required | getRequiredRequestBodyAsJSON | + func test_getRequiredRequestBodyAsJSON_codable() throws { + let body = try converter.getRequiredRequestBodyAsJSON( + TestPet.self, + from: testStructData, + transforming: { $0 } ) + XCTAssertEqual(body, testStruct) } - func testQueryGetRequired_array_missing() throws { - let query: [URLQueryItem] = [] - XCTAssertThrowsError( - try converter.queryGetRequired( - in: query, - name: "id", - as: [String].self - ), - "Was expected to throw error on missing required query", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredQueryParameter(let queryKey) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(queryKey, "id") - } + // | server | get | request body | binary | data | optional | getOptionalRequestBodyAsBinary | + func test_getOptionalRequestBodyAsBinary_data() throws { + let body = try converter.getOptionalRequestBodyAsBinary( + Data.self, + from: testStringData, + transforming: { $0 } ) + XCTAssertEqual(body, testStringData) } - // MARK: Body + // | server | get | request body | binary | data | required | getRequiredRequestBodyAsBinary | + func test_getRequiredRequestBodyAsBinary_data() throws { + let body = try converter.getRequiredRequestBodyAsBinary( + Data.self, + from: testStringData, + transforming: { $0 } + ) + XCTAssertEqual(body, testStringData) + } - func testBodyAddComplex() throws { + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + func test_setResponseBodyAsText_stringConvertible() throws { var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testStruct, + let data = try converter.setResponseBodyAsText( + testString, headerFields: &headers, transforming: { .init( value: $0, - contentType: "application/json", - strategy: .codable + contentType: "text/plain" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(data, testStringData) XCTAssertEqual( headers, [ - .init(name: "content-type", value: "application/json") + .init(name: "content-type", value: "text/plain") ] ) } - func testBodyAddString_strategyString() throws { + // | server | set | response body | text | date | required | setResponseBodyAsText | + func test_setResponseBodyAsText_date() throws { var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testString, + let data = try converter.setResponseBodyAsText( + testDate, headerFields: &headers, transforming: { .init( value: $0, - contentType: "text/plain", - strategy: .string + contentType: "text/plain" ) } ) - XCTAssertEqual(String(data: data, encoding: .utf8)!, testString) + XCTAssertEqual(data, testDateStringData) XCTAssertEqual( headers, [ @@ -566,42 +357,42 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } - func testBodyAddString_strategyCodable() throws { + // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + func test_setResponseBodyAsJSON_codable() throws { var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testString, + let data = try converter.setResponseBodyAsJSON( + testStruct, headerFields: &headers, transforming: { .init( value: $0, - contentType: "text/plain", - strategy: .codable + contentType: "application/json" ) } ) - XCTAssertEqual(String(data: data, encoding: .utf8)!, testQuotedString) + XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( headers, [ - .init(name: "content-type", value: "text/plain") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyAddData() throws { + // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + func test_setResponseBodyAsBinary_data() throws { var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testStructPrettyData, + let data = try converter.setResponseBodyAsBinary( + testStringData, headerFields: &headers, transforming: { .init( value: $0, - contentType: "application/octet-stream", - strategy: .data + contentType: "application/octet-stream" ) } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(data, testStringData) XCTAssertEqual( headers, [ @@ -609,146 +400,4 @@ final class Test_ServerConverterExtensions: Test_Runtime { ] ) } - - func testBodyGetComplexOptional_success() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: testStructData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func testBodyGetComplexOptional_nil() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: nil, - strategy: .codable, - transforming: { _ -> TestPet in fatalError("Unreachable") } - ) - XCTAssertNil(body) - } - - func testBodyGetComplexRequired_success() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: testStructData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func testBodyGetComplexRequired_nil() throws { - XCTAssertThrowsError( - try converter.bodyGetRequired( - TestPet.self, - from: nil, - strategy: .codable, - transforming: { _ -> TestPet in fatalError("Unreachable") } - ), - "Was expected to throw error on missing required body", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredRequestBody = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - } - ) - } - - func testBodyGetDataOptional_success() throws { - let body = try converter.bodyGetOptional( - Data.self, - from: testStructPrettyData, - strategy: .data, - transforming: { $0 } - ) - XCTAssertEqual(body, testStructPrettyData) - } - - func testBodyGetDataRequired_success() throws { - let body = try converter.bodyGetRequired( - Data.self, - from: testStructPrettyData, - strategy: .data, - transforming: { $0 } - ) - XCTAssertEqual(body, testStructPrettyData) - } - - func testBodyGetDataRequired_missing() throws { - XCTAssertThrowsError( - try converter.bodyGetRequired( - Data.self, - from: nil, - strategy: .data, - transforming: { $0 } - ), - "Was expected to throw error on missing required body", - { error in - guard - let err = error as? RuntimeError, - case .missingRequiredRequestBody = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - } - ) - } - - func testBodyGetStringOptional_strategyData_success() throws { - let body = try converter.bodyGetOptional( - String.self, - from: testQuotedStringData, - strategy: .data, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetStringOptional_strategyString_success() throws { - let body = try converter.bodyGetOptional( - String.self, - from: testStringData, - strategy: .string, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetStringOptional_strategyCodable_success() throws { - let body = try converter.bodyGetOptional( - String.self, - from: testQuotedStringData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetStringRequired_strategyString_success() throws { - let body = try converter.bodyGetRequired( - String.self, - from: testStringData, - strategy: .string, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetStringRequired_strategyCodable_success() throws { - let body = try converter.bodyGetRequired( - String.self, - from: testQuotedStringData, - strategy: .codable, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift new file mode 100644 index 00000000..c49bb682 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_CurrencyExtensions.swift @@ -0,0 +1,86 @@ +//===----------------------------------------------------------------------===// +// +// 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 XCTest +@testable import OpenAPIRuntime + +final class Test_CurrencyExtensions: Test_Runtime { + + func testHeaderFields_add_string() throws { + var headerFields: [HeaderField] = [] + headerFields.add(name: "foo", value: "bar") + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "bar") + ] + ) + } + + func testHeaderFields_add_nil() throws { + var headerFields: [HeaderField] = [] + let value: String? = nil + headerFields.add(name: "foo", value: value) + XCTAssertEqual(headerFields, []) + } + + func testHeaderFields_firstValue_found() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: "bar") + ] + XCTAssertEqual(headerFields.firstValue(name: "foo"), "bar") + } + + func testHeaderFields_firstValue_nil() throws { + let headerFields: [HeaderField] = [] + XCTAssertNil(headerFields.firstValue(name: "foo")) + } + + func testHeaderFields_values() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), + ] + XCTAssertEqual(headerFields.values(name: "foo"), ["bar", "baz"]) + } + + func testHeaderFields_removeAll_noMatches() throws { + var headerFields: [HeaderField] = [ + .init(name: "one", value: "one"), + .init(name: "two", value: "two"), + ] + headerFields.removeAll(named: "three") + XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) + } + + func testHeaderFields_removeAll_oneMatch() throws { + var headerFields: [HeaderField] = [ + .init(name: "one", value: "one"), + .init(name: "two", value: "two"), + .init(name: "three", value: "three"), + ] + headerFields.removeAll(named: "three") + XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) + } + + func testHeaderFields_removeAll_manyMatches() throws { + var headerFields: [HeaderField] = [ + .init(name: "one", value: "one"), + .init(name: "three", value: "3"), + .init(name: "two", value: "two"), + .init(name: "three", value: "three"), + ] + headerFields.removeAll(named: "three") + XCTAssertEqual(headerFields.map(\.name), ["one", "two"]) + } +} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift index e775dbab..4a6790fb 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_FoundationExtensions.swift @@ -16,29 +16,9 @@ import XCTest final class Test_FoundationExtensions: Test_Runtime { - func testURLComponents_addQueryItem_losslessStringConvertible_string() throws { + func testURLComponents_addStringQueryItem() throws { var components = testComponents - components.addQueryItem(name: "key", value: "value") + components.addStringQueryItem(name: "key", value: "value") XCTAssertEqualURLString(components.url, "/api?key=value") } - - func testURLComponents_addQueryItem_losslessStringConvertible_nil() throws { - var components = testComponents - let value: String? = nil - components.addQueryItem(name: "key", value: value) - XCTAssertEqualURLString(components.url, "/api") - } - - func testURLComponents_addQueryItem_arrayOfLosslessStringConvertible_strings() throws { - var components = testComponents - components.addQueryItem(name: "key", value: ["1", "2"]) - XCTAssertEqualURLString(components.url, "/api?key=1&key=2") - } - - func testURLComponents_addQueryItem_arrayOfLosslessStringConvertible_nil() throws { - var components = testComponents - let values: [String]? = nil - components.addQueryItem(name: "key", value: values) - XCTAssertEqualURLString(components.url, "/api") - } } diff --git a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift index 3d942970..1f238583 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -54,12 +54,16 @@ class Test_Runtime: XCTestCase { "2023-01-18T10:04:11Z" } + var testDateStringData: Data { + Data(testDateString.utf8) + } + var testString: String { "hello" } var testStringData: Data { - testString.data(using: .utf8)! + Data(testString.utf8) } var testQuotedString: String { @@ -67,7 +71,7 @@ class Test_Runtime: XCTestCase { } var testQuotedStringData: Data { - testQuotedString.data(using: .utf8)! + Data(testQuotedString.utf8) } var testStruct: TestPet { @@ -87,11 +91,11 @@ class Test_Runtime: XCTestCase { } var testStructData: Data { - testStructString.data(using: .utf8)! + Data(testStructString.utf8) } var testStructPrettyData: Data { - testStructPrettyString.data(using: .utf8)! + Data(testStructPrettyString.utf8) } }