diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift index 37717c8b..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,8 +22,14 @@ 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) { + /// Creates a new content wrapper. + /// - Parameters: + /// - value: An encodable body value. + /// - contentType: The header value of the content type. + public init( + value: T, + contentType: String + ) { self.value = value self.contentType = contentType } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index da1444c8..561febc2 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -15,189 +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. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value. - public func bodyGet( - _ type: T.Type, - from data: Data, - transforming transform: (T) -> C - ) throws -> C { - let decoded: T - if let myType = T.self as? _StringConvertible.Type { - guard - let stringValue = String(data: data, encoding: .utf8), - let decodedValue = myType.init(stringValue) - else { - throw RuntimeError.failedToDecodePrimitiveBodyFromData - } - 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 { - guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodePrimitiveBodyIntoData - } - 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. - /// - transform: Closure for transforming the Decodable type into a final type. - /// - Returns: Deserialized body value. - public func bodyGet( - _ type: Data.Type, - from data: Data, - 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 bcbb3597..46243e26 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -13,49 +13,7 @@ //===----------------------------------------------------------------------===// 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 } - } -} - -public extension Converter { +extension Converter { // MARK: Miscs @@ -68,12 +26,12 @@ 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( + let contentType = try getOptionalHeaderFieldAsText( in: headerFields, name: "content-type", as: String.self @@ -86,138 +44,215 @@ public extension Converter { } } - // MARK: Headers - Date + // MARK: - Converter helper methods - /// 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. - func headerFieldAdd( + // | common | set | header field | text | string-convertible | both | setHeaderFieldAsText | + public func setHeaderFieldAsText( + in headerFields: inout [HeaderField], + name: String, + value: T? + ) throws { + try setHeaderField( + in: &headerFields, + name: name, + value: value, + convert: convertStringConvertibleToText + ) + } + + // | common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | + public func setHeaderFieldAsText( + in headerFields: inout [HeaderField], + name: String, + value values: [T]? + ) throws { + 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], 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. - /// - name: The name of the header field (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name, if one exists. - 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], 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. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name. - func headerFieldGetRequired( + // | common | get | header field | text | date | required | getRequiredHeaderFieldAsText | + public func getRequiredHeaderFieldAsText( 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 + 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. - /// - name: Header name. - /// - value: Encodable header value. - func headerFieldAdd( - in headerFields: inout [HeaderField], + // | 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 { - 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. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name, if one exists. - func headerFieldGetOptional( + // | common | get | header field | JSON | codable | optional | getOptionalHeaderFieldAsJSON | + public func getOptionalHeaderFieldAsJSON( 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? _StringConvertible.Type { - 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. - /// - name: Header name (case-insensitive). - /// - type: Date type. - /// - Returns: First value for the given name. - func headerFieldGetRequired( + // | common | get | header field | JSON | codable | required | getRequiredHeaderFieldAsJSON | + public func getRequiredHeaderFieldAsJSON( 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 + ) 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 e0172490..765a2ad5 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -57,316 +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. - /// - 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?, transforming transform: (T) -> C ) throws -> C? { - guard let data else { - return nil - } - let decoded: T - if let myType = T.self as? _StringConvertible.Type { - guard - let stringValue = String(data: data, encoding: .utf8), - let decodedValue = myType.init(stringValue) - else { - throw RuntimeError.failedToDecodePrimitiveBodyFromData - } - 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. - /// - 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?, 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 = 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 { - guard let data = value.description.data(using: .utf8) else { - throw RuntimeError.failedToEncodePrimitiveBodyIntoData - } - 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. - /// - 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?, - 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. - /// - 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?, 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 5b4e89af..f5daf54f 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -42,3 +42,416 @@ 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 } + } +} + +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 cb20c7e6..a1118bfe 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -14,3 +14,383 @@ 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.missingRequiredHeaderField(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? _StringConvertible { + headerFields.add(name: name, value: value.description) + return + } + let data = try headerFieldEncoder.encode(value) + let stringValue = String(decoding: data, as: UTF8.self) + 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? _StringConvertible.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.missingRequiredHeaderField(name) + } + return value + } +} + +/// 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. + public init( + value: T, + contentType: String + ) { + 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/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 67b90ed7..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 failedToEncodePrimitiveBodyIntoData - case failedToDecodePrimitiveBodyFromData // 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 .failedToEncodePrimitiveBodyIntoData: - return "Failed to encode a primitive body into data" - case .failedToDecodePrimitiveBodyFromData: - return "Failed to decode a primitive body 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 d0696ef2..bcdb201a 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -12,15 +12,28 @@ // //===----------------------------------------------------------------------===// import XCTest -@_spi(Generated) import OpenAPIRuntime +@_spi(Generated)@testable import OpenAPIRuntime 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,95 +51,141 @@ 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"] + name: "search", + value: testDate ) - XCTAssertEqual(request.query, "id=1&id=2") + XCTAssertEqual(request.query, "search=2023-01-18T10:04:11Z") } - // MARK: Body - - func testBodyGetComplex_success() throws { - let body = try converter.bodyGet( - TestPet.self, - from: testStructData, - transforming: { $0 } + // | 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(body, testStruct) + XCTAssertEqual(request.query, "search=2023-01-18T10:04:11Z&search=2023-01-18T10:04:11Z") } - func testBodyGetData_success() throws { - let body = try converter.bodyGet( - Data.self, - from: testStructData, - transforming: { $0 } + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + func test_setOptionalRequestBodyAsText_stringConvertible() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setOptionalRequestBodyAsText( + testString, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(body, testStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] ) - XCTAssertEqual(body, testStructData) } - func testBodyGetString_success() throws { - let body = try converter.bodyGet( - String.self, - from: testStringData, - transforming: { $0 } + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + func test_setRequiredRequestBodyAsText_stringConvertible() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setRequiredRequestBodyAsText( + testString, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(body, testStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] ) - XCTAssertEqual(body, testString) } - func testBodyAddComplexOptional_success() throws { + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + func test_setOptionalRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testStruct, + let body = try converter.setOptionalRequestBodyAsText( + testDate, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/json") } + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testDateStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/json") + .init(name: "content-type", value: "text/plain") ] ) } - func testBodyAddComplexOptional_nil() throws { - let value: TestPet? = 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 testBodyAddComplexRequired_success() throws { + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + func test_setOptionalRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( + let body = try converter.setOptionalRequestBodyAsJSON( testStruct, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/json") } + transforming: { value in + .init( + value: value, + contentType: "application/json" + ) + } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testStructPrettyData) XCTAssertEqual( headerFields, [ @@ -135,67 +194,130 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - func testBodyAddDataOptional_success() throws { + func test_setOptionalRequestBodyAsJSON_codable_string() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testStructPrettyData, + let body = try converter.setOptionalRequestBodyAsJSON( + testString, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { value in + .init( + value: value, + contentType: "application/json" + ) + } ) - XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual(body, testQuotedStringData) XCTAssertEqual( headerFields, [ - .init(name: "content-type", value: "application/octet-stream") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyAddDataRequired_success() throws { + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + func test_setRequiredRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddRequired( - testStructPrettyData, + let body = try converter.setRequiredRequestBodyAsJSON( + testStruct, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { value in + .init( + 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_success() throws { + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + func test_setOptionalRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] - let data = try converter.bodyAddOptional( - testString, + let body = try converter.setOptionalRequestBodyAsBinary( + testStringData, headerFields: &headerFields, - transforming: { .init(value: $0, contentType: "text/plain") } + transforming: { value in + .init( + 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_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: { .init(value: $0, contentType: "text/plain") } + transforming: { value in + .init( + 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") ] ) } + + // | 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 d3a0c1c4..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,12 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - // MARK: [HeaderField] - _StringConvertible + // MARK: Converter helper methods - func testHeaderAdd_string() throws { + // | 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, name: "foo", value: "bar" @@ -162,227 +93,218 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } - func testHeaderGetOptional_string() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - let value = try converter.headerFieldGetOptional( - in: headerFields, + // | common | set | header field | text | array of string-convertibles | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_arrayOfStringConvertible() throws { + var headerFields: [HeaderField] = [] + try converter.setHeaderFieldAsText( + in: &headerFields, name: "foo", - as: String.self + value: ["bar", "baz"] as [String] + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), + ] ) - XCTAssertEqual(value, "bar") } - func testHeaderGetOptional_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( - in: headerFields, + // | common | set | header field | text | date | both | setHeaderFieldAsText | + func test_setHeaderFieldAsText_date() throws { + var headerFields: [HeaderField] = [] + try converter.setHeaderFieldAsText( + in: &headerFields, name: "foo", - as: String.self + value: testDate + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: testDateString) + ] ) - XCTAssertNil(value) } - func testHeaderGetRequired_string() throws { - let headerFields: [HeaderField] = [ - .init(name: "foo", value: "bar") - ] - let value = try converter.headerFieldGetRequired( - in: headerFields, + // | 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(value, "bar") - } - - func testHeaderGetRequired_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - 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") - } + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: testDateString), + .init(name: "foo", value: testDateString), + ] ) } - // MARK: [HeaderField] - (Date) - - func testHeaderAdd_date() throws { + // | common | set | header field | JSON | codable | both | setHeaderFieldAsJSON | + func test_setHeaderFieldAsJSON_codable() throws { var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( + try converter.setHeaderFieldAsJSON( in: &headerFields, - name: "since", - value: testDate + name: "foo", + value: testStruct ) XCTAssertEqual( headerFields, [ - .init(name: "since", value: testDateString) + .init(name: "foo", value: testStructString) ] ) } - func testHeaderAdd_date_nil() throws { + func test_setHeaderFieldAsJSON_codable_string() throws { var headerFields: [HeaderField] = [] - let date: Date? = nil - try converter.headerFieldAdd( + try converter.setHeaderFieldAsJSON( in: &headerFields, - name: "since", - value: date + name: "foo", + value: "hello" + ) + XCTAssertEqual( + headerFields, + [ + .init(name: "foo", value: "\"hello\"") + ] ) - XCTAssertEqual(headerFields, []) } - func testHeaderGetOptional_date() throws { + // | common | get | header field | text | string-convertible | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_stringConvertible() throws { let headerFields: [HeaderField] = [ - .init(name: "since", value: testDateString) + .init(name: "foo", value: "bar") ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - name: "since", - as: Date.self + name: "foo", + as: String.self ) - XCTAssertEqual(value, testDate) + XCTAssertEqual(value, "bar") } - func testHeaderGetOptional_date_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( + // | 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.getRequiredHeaderFieldAsText( in: headerFields, - name: "since", - as: Date.self + name: "foo", + as: String.self ) - XCTAssertNil(value) + XCTAssertEqual(value, "bar") } - func testHeaderGetRequired_date() throws { + // | common | get | header field | text | array of string-convertibles | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_arrayOfStringConvertibles() throws { let headerFields: [HeaderField] = [ - .init(name: "since", value: testDateString) + .init(name: "foo", value: "bar"), + .init(name: "foo", value: "baz"), ] - let value = try converter.headerFieldGetRequired( + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - name: "since", - as: Date.self + name: "foo", + as: [String].self ) - XCTAssertEqual(value, testDate) + XCTAssertEqual(value, ["bar", "baz"]) } - func testHeaderGetRequired_date_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - 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") - } + // | 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: "baz"), + ] + let value = try converter.getRequiredHeaderFieldAsText( + in: headerFields, + name: "foo", + as: [String].self ) + XCTAssertEqual(value, ["bar", "baz"]) } - // MARK: [HeaderField] - Complex - - func testHeaderAddComplex_struct() throws { - var headerFields: [HeaderField] = [] - try converter.headerFieldAdd( - in: &headerFields, + // | common | get | header field | text | date | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_date() throws { + let headerFields: [HeaderField] = [ + .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) } - func testHeaderAddComplex_nil() throws { - var headerFields: [HeaderField] = [] - let value: TestPet? = nil - try converter.headerFieldAdd( - in: &headerFields, + // | common | get | header field | text | date | required | getRequiredHeaderFieldAsText | + func test_getRequiredHeaderFieldAsText_date() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: testDateString) + ] + let value = try converter.getRequiredHeaderFieldAsText( + in: headerFields, name: "foo", - value: value + as: Date.self ) - XCTAssertEqual(headerFields, []) + XCTAssertEqual(value, testDate) } - func testHeaderGetComplexOptional_struct() throws { + // | common | get | header field | text | array of dates | optional | getOptionalHeaderFieldAsText | + func test_getOptionalHeaderFieldAsText_arrayOfDates() throws { let headerFields: [HeaderField] = [ - .init(name: "pet", value: testStructString) + .init(name: "foo", value: testDateString), + .init(name: "foo", value: testDateString), ] - let value = try converter.headerFieldGetOptional( + let value = try converter.getOptionalHeaderFieldAsText( in: headerFields, - name: "pet", - as: TestPet.self + name: "foo", + as: [Date].self ) - XCTAssertEqual(value, testStruct) + XCTAssertEqual(value, [testDate, testDate]) } - func testHeaderGetComplexOptional_missing() throws { - let headerFields: [HeaderField] = [] - let value = try converter.headerFieldGetOptional( + // | 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: "pet", - as: TestPet.self + name: "foo", + as: [Date].self ) - XCTAssertNil(value) + XCTAssertEqual(value, [testDate, testDate]) } - func testHeaderGetComplexRequired_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.headerFieldGetRequired( + let value = try converter.getOptionalHeaderFieldAsJSON( in: headerFields, - name: "pet", + name: "foo", as: TestPet.self ) XCTAssertEqual(value, testStruct) } - func testHeaderGetComplexRequired_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertThrowsError( - try converter.headerFieldGetRequired( - in: headerFields, - 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") - } + // | common | get | header field | JSON | codable | required | getRequiredHeaderFieldAsJSON | + func test_getRequiredHeaderFieldAsJSON_codable() throws { + let headerFields: [HeaderField] = [ + .init(name: "foo", value: testStructString) + ] + let value = try converter.getRequiredHeaderFieldAsJSON( + in: headerFields, + name: "foo", + as: TestPet.self ) + XCTAssertEqual(value, testStruct) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index d8855615..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,352 +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") } + transforming: { + .init( + value: $0, + 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() 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") } + transforming: { + .init( + value: $0, + contentType: "text/plain" + ) + } ) - XCTAssertEqual(String(data: data, encoding: .utf8)!, testString) + XCTAssertEqual(data, testDateStringData) XCTAssertEqual( headers, [ @@ -554,102 +357,47 @@ final class Test_ServerConverterExtensions: Test_Runtime { ) } - func testBodyAddData() throws { + // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + func test_setResponseBodyAsJSON_codable() throws { var headers: [HeaderField] = [] - let data = try converter.bodyAdd( - testStructPrettyData, + let data = try converter.setResponseBodyAsJSON( + testStruct, headerFields: &headers, - transforming: { .init(value: $0, contentType: "application/octet-stream") } + transforming: { + .init( + value: $0, + contentType: "application/json" + ) + } ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( headers, [ - .init(name: "content-type", value: "application/octet-stream") + .init(name: "content-type", value: "application/json") ] ) } - func testBodyGetComplexOptional_success() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: testStructData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func testBodyGetComplexOptional_nil() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: nil, - transforming: { _ -> TestPet in fatalError("Unreachable") } - ) - XCTAssertNil(body) - } - - func testBodyGetComplexRequired_success() throws { - let body = try converter.bodyGetOptional( - TestPet.self, - from: testStructData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStruct) - } - - func testBodyGetComplexRequired_nil() throws { - XCTAssertThrowsError( - try converter.bodyGetRequired( - TestPet.self, - from: nil, - 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 - } + // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + func test_setResponseBodyAsBinary_data() throws { + var headers: [HeaderField] = [] + let data = try converter.setResponseBodyAsBinary( + testStringData, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "application/octet-stream" + ) } ) - } - - func testBodyGetDataOptional_success() throws { - let body = try converter.bodyGetOptional( - Data.self, - from: testStructPrettyData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStructPrettyData) - } - - func testBodyGetDataRequired_success() throws { - let body = try converter.bodyGetOptional( - Data.self, - from: testStructPrettyData, - transforming: { $0 } - ) - XCTAssertEqual(body, testStructPrettyData) - } - - func testBodyGetStringOptional_success() throws { - let body = try converter.bodyGetOptional( - String.self, - from: testStringData, - transforming: { $0 } - ) - XCTAssertEqual(body, testString) - } - - func testBodyGetStringRequired_success() throws { - let body = try converter.bodyGetOptional( - String.self, - from: testStringData, - transforming: { $0 } + XCTAssertEqual(data, testStringData) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "application/octet-stream") + ] ) - 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 451b07ec..1f238583 100644 --- a/Tests/OpenAPIRuntimeTests/Test_Runtime.swift +++ b/Tests/OpenAPIRuntimeTests/Test_Runtime.swift @@ -54,12 +54,24 @@ class Test_Runtime: XCTestCase { "2023-01-18T10:04:11Z" } + var testDateStringData: Data { + Data(testDateString.utf8) + } + var testString: String { "hello" } var testStringData: Data { - "hello".data(using: .utf8)! + Data(testString.utf8) + } + + var testQuotedString: String { + "\"hello\"" + } + + var testQuotedStringData: Data { + Data(testQuotedString.utf8) } var testStruct: TestPet { @@ -79,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) } }