From f997ab15c34c3e372be6dd4d4409c9b25b0c85b4 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 27 Jul 2023 12:46:19 +0200 Subject: [PATCH 01/16] [WIP] Multiple content types --- .../Conversion/Converter+Common.swift | 78 ++++++++++++++++--- .../Conversion/Test_Converter+Common.swift | 29 +++++++ 2 files changed, 98 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 71034db3..b6effc1b 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -25,25 +25,85 @@ extension Converter { /// - Parameters: /// - 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. + /// - Throws: If the response's Content-Type value is not compatible with + /// the provided substring. + @available(*, deprecated, message: "Use isValidContentType instead.") public func validateContentTypeIfPresent( in headerFields: [HeaderField], substring: String ) throws { - guard - let contentType = try getOptionalHeaderFieldAsText( - in: headerFields, - name: "content-type", - as: String.self - ) - else { + guard let contentType = extractContentTypeIfPresent(in: headerFields) else { return } - guard contentType.localizedCaseInsensitiveContains(substring) else { + guard isValidContentType(received: contentType, expected: substring) else { throw RuntimeError.unexpectedContentTypeHeader(contentType) } } + /// Returns the content-type header from the provided header fields, if + /// present. + /// - Parameter headerFields: The header fields to inspect for the content + /// type header. + /// - Returns: The content type value, or nil if not found. + public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> String? { + headerFields.firstValue(name: "content-type") + } + + /// Checks whether a concrete content type matches an expected content type. + /// + /// The concrete content type can contain parameters, such as `charset`, but + /// they are ignored in the equality comparison. + /// + /// The expected content type can contain wildcards, such as */* and text/*. + /// - Parameters: + /// - received: The concrete content type to validate against the other. + /// - expected: The expected content type, can be a wildcard. + /// - Returns: A Boolean value representing whether the concrete content + /// type matches the expected one. + public func isValidContentType(received: String?, expected: String) -> Bool { + guard let received else { + return false + } + func parseContentType(_ value: String) -> (main: String, sub: String)? { + let components = + value + // Normalize to lowercase. + .lowercased() + // Drop any charset and other parameters. + .split(separator: ";")[0] + // Parse out main type and subtype. + .split(separator: "/") + .map(String.init) + guard components.count == 2 else { + return nil + } + return (components[0], components[1]) + } + guard + let receivedContentType = parseContentType(received), + let expectedContentType = parseContentType(expected) + else { + return false + } + if expectedContentType.main == "*" { + return true + } + if expectedContentType.main != receivedContentType.main { + return false + } + if expectedContentType.sub == "*" { + return true + } + return expectedContentType.sub == receivedContentType.sub + } + + /// Returns an error to be thrown when an unexpected content type is + /// received. + /// - Parameter contentType: The content type that was received. + public func makeUnexpectedContentTypeError(contentType: String?) -> any Error { + RuntimeError.unexpectedContentTypeHeader(contentType ?? "") + } + // MARK: - Converter helper methods // | common | set | header field | text | string-convertible | both | setHeaderFieldAsText | diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index 5eebceb4..c0ee4fcd 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -75,6 +75,35 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } + func testContentTypeMatching() throws { + let cases: [(received: String, expected: String, isMatch: Bool)] = [ + ("application/json", "application/json", true), + ("APPLICATION/JSON", "application/json", true), + ("application/json", "application/*", true), + ("application/json", "*/*", true), + ("application/json", "text/*", false), + ("application/json", "application/xml", false), + ("application/json", "text/plain", false), + + ("text/plain; charset=UTF-8", "text/plain", true), + ("TEXT/PLAIN; CHARSET=UTF-8", "text/plain", true), + ("text/plain; charset=UTF-8", "text/*", true), + ("text/plain; charset=UTF-8", "*/*", true), + ("text/plain; charset=UTF-8", "application/*", false), + ("text/plain; charset=UTF-8", "text/html", false), + ] + for testCase in cases { + XCTAssertEqual( + converter.isValidContentType( + received: testCase.received, + expected: testCase.expected + ), + testCase.isMatch, + "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" + ) + } + } + // MARK: Converter helper methods // | common | set | header field | text | string-convertible | both | setHeaderFieldAsText | From 66b78af5493c06c130cc91ea850ce53aa7d34616 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 28 Jul 2023 11:32:33 +0200 Subject: [PATCH 02/16] Try designing EncodedBodyContent away --- .../Base/EncodableBodyContent.swift | 1 + .../Conversion/Converter+Client.swift | 78 +++++++++++++++++++ .../Conversion/Converter+Server.swift | 4 + .../Conversion/CurrencyExtensions.swift | 30 +++++++ 4 files changed, 113 insertions(+) diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift index f4995314..b68b88c6 100644 --- a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift +++ b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift @@ -14,6 +14,7 @@ /// A wrapper of a body value with its content type. @_spi(Generated) +@available(*, deprecated) public struct EncodableBodyContent: Equatable { /// An encodable body value. diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index 300b0cb0..f6347d42 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -90,6 +90,7 @@ extension Converter { } // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) public func setOptionalRequestBodyAsText( _ value: C?, headerFields: inout [HeaderField], @@ -104,6 +105,7 @@ extension Converter { } // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + @available(*, deprecated) public func setRequiredRequestBodyAsText( _ value: C, headerFields: inout [HeaderField], @@ -117,7 +119,22 @@ extension Converter { ) } + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + public func setRequiredRequestBodyAsText( + _ value: T, + headerFields: inout [HeaderField], + contentType: String + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertStringConvertibleToTextData + ) + } + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) public func setOptionalRequestBodyAsText( _ value: C?, headerFields: inout [HeaderField], @@ -132,6 +149,7 @@ extension Converter { } // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + @available(*, deprecated) public func setRequiredRequestBodyAsText( _ value: C, headerFields: inout [HeaderField], @@ -145,7 +163,22 @@ extension Converter { ) } + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + public func setRequiredRequestBodyAsText( + _ value: Date, + headerFields: inout [HeaderField], + contentType: String + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertDateToTextData + ) + } + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + @available(*, deprecated) public func setOptionalRequestBodyAsJSON( _ value: C?, headerFields: inout [HeaderField], @@ -159,7 +192,22 @@ extension Converter { ) } + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + public func setOptionalRequestBodyAsJSON( + _ value: T?, + headerFields: inout [HeaderField], + contentType: String + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertBodyCodableToJSON + ) + } + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + @available(*, deprecated) public func setRequiredRequestBodyAsJSON( _ value: C, headerFields: inout [HeaderField], @@ -173,7 +221,22 @@ extension Converter { ) } + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + public func setRequiredRequestBodyAsJSON( + _ value: T, + headerFields: inout [HeaderField], + contentType: String + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertBodyCodableToJSON + ) + } + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + @available(*, deprecated) public func setOptionalRequestBodyAsBinary( _ value: C?, headerFields: inout [HeaderField], @@ -188,6 +251,7 @@ extension Converter { } // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + @available(*, deprecated) public func setRequiredRequestBodyAsBinary( _ value: C, headerFields: inout [HeaderField], @@ -201,6 +265,20 @@ extension Converter { ) } + // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + public func setRequiredRequestBodyAsBinary( + _ value: Data, + headerFields: inout [HeaderField], + contentType: String + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convertDataToBinary + ) + } + // | client | get | response body | text | string-convertible | required | getResponseBodyAsText | public func getResponseBodyAsText( _ type: T.Type, diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index cd8ad196..adacd1b7 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -296,6 +296,7 @@ public extension Converter { } // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + @available(*, deprecated) func setResponseBodyAsText( _ value: C, headerFields: inout [HeaderField], @@ -310,6 +311,7 @@ public extension Converter { } // | server | set | response body | text | date | required | setResponseBodyAsText | + @available(*, deprecated) func setResponseBodyAsText( _ value: C, headerFields: inout [HeaderField], @@ -324,6 +326,7 @@ public extension Converter { } // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + @available(*, deprecated) func setResponseBodyAsJSON( _ value: C, headerFields: inout [HeaderField], @@ -338,6 +341,7 @@ public extension Converter { } // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + @available(*, deprecated) func setResponseBodyAsBinary( _ value: C, headerFields: inout [HeaderField], diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index f5daf54f..2a10598c 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -347,6 +347,7 @@ extension Converter { return values } + @available(*, deprecated) func setRequiredRequestBody( _ value: C, headerFields: inout [HeaderField], @@ -359,6 +360,17 @@ extension Converter { return try convert(convertibleValue) } + func setRequiredRequestBody( + _ value: T, + headerFields: inout [HeaderField], + contentType: String, + convert: (T) throws -> Data + ) throws -> Data { + headerFields.add(name: "content-type", value: contentType) + return try convert(value) + } + + @available(*, deprecated) func setOptionalRequestBody( _ value: C?, headerFields: inout [HeaderField], @@ -376,6 +388,23 @@ extension Converter { ) } + func setOptionalRequestBody( + _ value: T?, + headerFields: inout [HeaderField], + contentType: String, + convert: (T) throws -> Data + ) throws -> Data? { + guard let value else { + return nil + } + return try setRequiredRequestBody( + value, + headerFields: &headerFields, + contentType: contentType, + convert: convert + ) + } + func getOptionalRequestBody( _ type: T.Type, from data: Data?, @@ -419,6 +448,7 @@ extension Converter { return transformedValue } + @available(*, deprecated) func setResponseBody( _ value: C, headerFields: inout [HeaderField], From 00e9b4017c904ac44c5d246d813f6c32d8a15127 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 28 Jul 2023 14:26:58 +0200 Subject: [PATCH 03/16] Deprecate unit tests that call deprecated methods --- .../Conversion/Test_Converter+Client.swift | 9 +++++++++ .../Conversion/Test_Converter+Common.swift | 4 ++++ .../Conversion/Test_Converter+Server.swift | 4 ++++ 3 files changed, 17 insertions(+) diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 09a28f19..92cfb886 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -85,6 +85,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) func test_setOptionalRequestBodyAsText_stringConvertible() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsText( @@ -107,6 +108,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + @available(*, deprecated) func test_setRequiredRequestBodyAsText_stringConvertible() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsText( @@ -129,6 +131,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) func test_setOptionalRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsText( @@ -151,6 +154,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + @available(*, deprecated) func test_setRequiredRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsText( @@ -173,6 +177,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + @available(*, deprecated) func test_setOptionalRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsJSON( @@ -194,6 +199,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } + @available(*, deprecated) func test_setOptionalRequestBodyAsJSON_codable_string() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsJSON( @@ -216,6 +222,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + @available(*, deprecated) func test_setRequiredRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsJSON( @@ -238,6 +245,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + @available(*, deprecated) func test_setOptionalRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsBinary( @@ -260,6 +268,7 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + @available(*, deprecated) func test_setRequiredRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsBinary( diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index c0ee4fcd..bfb1ae70 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -18,6 +18,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { // MARK: Miscs + @available(*, deprecated) func testValidateContentType_match() throws { let headerFields: [HeaderField] = [ .init(name: "content-type", value: "application/json") @@ -30,6 +31,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } + @available(*, deprecated) func testValidateContentType_match_substring() throws { let headerFields: [HeaderField] = [ .init(name: "content-type", value: "application/json; charset=utf-8") @@ -42,6 +44,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } + @available(*, deprecated) func testValidateContentType_missing() throws { let headerFields: [HeaderField] = [] XCTAssertNoThrow( @@ -52,6 +55,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { ) } + @available(*, deprecated) func testValidateContentType_mismatch() throws { let headerFields: [HeaderField] = [ .init(name: "content-type", value: "text/plain") diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index f35b94b7..7fa522e7 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -314,6 +314,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + @available(*, deprecated) func test_setResponseBodyAsText_stringConvertible() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsText( @@ -336,6 +337,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | text | date | required | setResponseBodyAsText | + @available(*, deprecated) func test_setResponseBodyAsText_date() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsText( @@ -358,6 +360,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + @available(*, deprecated) func test_setResponseBodyAsJSON_codable() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsJSON( @@ -380,6 +383,7 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + @available(*, deprecated) func test_setResponseBodyAsBinary_data() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsBinary( From f6c7f2352cc9f50b616957aa97212a4e8e5b4661 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 28 Jul 2023 15:51:01 +0200 Subject: [PATCH 04/16] Stop using EncodableBodyContent in responses as well --- .../Base/EncodableBodyContent.swift | 37 --- .../Conversion/Converter+Client.swift | 104 +------- .../Conversion/Converter+Server.swift | 38 ++- .../Conversion/CurrencyExtensions.swift | 54 +--- .../Deprecated/Deprecated.swift | 251 ++++++++++++++++++ 5 files changed, 291 insertions(+), 193 deletions(-) delete mode 100644 Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift diff --git a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift b/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift deleted file mode 100644 index b68b88c6..00000000 --- a/Sources/OpenAPIRuntime/Base/EncodableBodyContent.swift +++ /dev/null @@ -1,37 +0,0 @@ -//===----------------------------------------------------------------------===// -// -// This source file is part of the SwiftOpenAPIGenerator open source project -// -// Copyright (c) 2023 Apple Inc. and the SwiftOpenAPIGenerator project authors -// Licensed under Apache License v2.0 -// -// See LICENSE.txt for license information -// See CONTRIBUTORS.txt for the list of SwiftOpenAPIGenerator project authors -// -// SPDX-License-Identifier: Apache-2.0 -// -//===----------------------------------------------------------------------===// - -/// A wrapper of a body value with its content type. -@_spi(Generated) -@available(*, deprecated) -public struct EncodableBodyContent: 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 - } -} diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index f6347d42..ffa043f8 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -88,33 +88,17 @@ extension Converter { convert: convertDateToText ) } - + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | - @available(*, deprecated) - public func setOptionalRequestBodyAsText( - _ value: C?, + public func setOptionalRequestBodyAsText( + _ value: T?, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data? { try setOptionalRequestBody( value, headerFields: &headerFields, - transforming: transform, - convert: convertStringConvertibleToTextData - ) - } - - // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | - @available(*, deprecated) - public func setRequiredRequestBodyAsText( - _ value: C, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertStringConvertibleToTextData ) } @@ -134,31 +118,15 @@ extension Converter { } // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | - @available(*, deprecated) - public func setOptionalRequestBodyAsText( - _ value: C?, + public func setOptionalRequestBodyAsText( + _ value: Date?, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data? { try setOptionalRequestBody( value, headerFields: &headerFields, - transforming: transform, - convert: convertDateToTextData - ) - } - - // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | - @available(*, deprecated) - public func setRequiredRequestBodyAsText( - _ value: C, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertDateToTextData ) } @@ -177,21 +145,6 @@ extension Converter { ) } - // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | - @available(*, deprecated) - public func setOptionalRequestBodyAsJSON( - _ value: C?, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data? { - try setOptionalRequestBody( - value, - headerFields: &headerFields, - transforming: transform, - convert: convertBodyCodableToJSON - ) - } - // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | public func setOptionalRequestBodyAsJSON( _ value: T?, @@ -206,21 +159,6 @@ extension Converter { ) } - // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | - @available(*, deprecated) - public func setRequiredRequestBodyAsJSON( - _ value: C, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - transforming: transform, - convert: convertBodyCodableToJSON - ) - } - // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | public func setRequiredRequestBodyAsJSON( _ value: T, @@ -236,31 +174,15 @@ extension Converter { } // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | - @available(*, deprecated) - public func setOptionalRequestBodyAsBinary( - _ value: C?, + public func setOptionalRequestBodyAsBinary( + _ value: Data?, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data? { try setOptionalRequestBody( value, headerFields: &headerFields, - transforming: transform, - convert: convertDataToBinary - ) - } - - // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | - @available(*, deprecated) - public func setRequiredRequestBodyAsBinary( - _ value: C, - headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent - ) throws -> Data { - try setRequiredRequestBody( - value, - headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertDataToBinary ) } diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index adacd1b7..d71b168e 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -294,63 +294,59 @@ public extension Converter { convert: convertBinaryToData ) } - + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | - @available(*, deprecated) - func setResponseBodyAsText( - _ value: C, + func setResponseBodyAsText( + _ value: T, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data { try setResponseBody( value, headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertStringConvertibleToTextData ) } // | server | set | response body | text | date | required | setResponseBodyAsText | - @available(*, deprecated) - func setResponseBodyAsText( - _ value: C, + func setResponseBodyAsText( + _ value: Date, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data { try setResponseBody( value, headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertDateToTextData ) } // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | - @available(*, deprecated) - func setResponseBodyAsJSON( - _ value: C, + func setResponseBodyAsJSON( + _ value: T, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data { try setResponseBody( value, headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertBodyCodableToJSON ) } // | server | set | response body | binary | data | required | setResponseBodyAsBinary | - @available(*, deprecated) - func setResponseBodyAsBinary( - _ value: C, + func setResponseBodyAsBinary( + _ value: Data, headerFields: inout [HeaderField], - transforming transform: (C) -> EncodableBodyContent + contentType: String ) throws -> Data { try setResponseBody( value, headerFields: &headerFields, - transforming: transform, + contentType: contentType, convert: convertDataToBinary ) } diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 2a10598c..950c5a7c 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -347,19 +347,6 @@ extension Converter { return values } - @available(*, deprecated) - 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 setRequiredRequestBody( _ value: T, headerFields: inout [HeaderField], @@ -370,24 +357,6 @@ extension Converter { return try convert(value) } - @available(*, deprecated) - 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 setOptionalRequestBody( _ value: T?, headerFields: inout [HeaderField], @@ -437,6 +406,16 @@ extension Converter { return body } + func setResponseBody( + _ value: T, + headerFields: inout [HeaderField], + contentType: String, + convert: (T) throws -> Data + ) throws -> Data { + headerFields.add(name: "content-type", value: contentType) + return try convert(value) + } + func getResponseBody( _ type: T.Type, from data: Data, @@ -448,19 +427,6 @@ extension Converter { return transformedValue } - @available(*, deprecated) - 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 { diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index f702eb2c..cb82c040 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -15,6 +15,30 @@ import Foundation // MARK: - Functionality to be removed in the future +/// A wrapper of a body value with its content type. +@available(*, deprecated) +@_spi(Generated) +public struct EncodableBodyContent: 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 { /// Gets a deserialized value from body data. /// - Parameters: @@ -702,3 +726,230 @@ extension Converter { return typedValue } } + +extension Converter { + + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) + public func setOptionalRequestBodyAsText( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertStringConvertibleToTextData + ) + } + + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + @available(*, deprecated) + public func setRequiredRequestBodyAsText( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertStringConvertibleToTextData + ) + } + + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) + public func setOptionalRequestBodyAsText( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDateToTextData + ) + } + + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + @available(*, deprecated) + public func setRequiredRequestBodyAsText( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDateToTextData + ) + } + + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + @available(*, deprecated) + public func setOptionalRequestBodyAsJSON( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertBodyCodableToJSON + ) + } + + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + @available(*, deprecated) + public func setRequiredRequestBodyAsJSON( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertBodyCodableToJSON + ) + } + + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + @available(*, deprecated) + public func setOptionalRequestBodyAsBinary( + _ value: C?, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data? { + try setOptionalRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDataToBinary + ) + } + + // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + @available(*, deprecated) + public func setRequiredRequestBodyAsBinary( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setRequiredRequestBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDataToBinary + ) + } + + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + @available(*, deprecated) + public 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 | + @available(*, deprecated) + public 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 | + @available(*, deprecated) + public 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 | + @available(*, deprecated) + public func setResponseBodyAsBinary( + _ value: C, + headerFields: inout [HeaderField], + transforming transform: (C) -> EncodableBodyContent + ) throws -> Data { + try setResponseBody( + value, + headerFields: &headerFields, + transforming: transform, + convert: convertDataToBinary + ) + } + + @available(*, deprecated) + public 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) + } + + @available(*, deprecated) + public 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 + ) + } + + @available(*, deprecated) + public 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) + } +} From 59f199f242b3c156ab4ddfe66e24d38eb46e06b6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Fri, 28 Jul 2023 16:09:17 +0200 Subject: [PATCH 05/16] Update tests as well --- .../Conversion/Converter+Client.swift | 2 +- .../Conversion/Converter+Common.swift | 23 -- .../Conversion/Converter+Server.swift | 2 +- .../Deprecated/Deprecated.swift | 33 +- .../Conversion/Test_Converter+Client.swift | 72 +--- .../Conversion/Test_Converter+Common.swift | 61 --- .../Conversion/Test_Converter+Server.swift | 32 +- .../Deprecated/Test_Deprecated.swift | 378 ++++++++++++++++++ 8 files changed, 421 insertions(+), 182 deletions(-) create mode 100644 Tests/OpenAPIRuntimeTests/Deprecated/Test_Deprecated.swift diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift index ffa043f8..2dd4d89c 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Client.swift @@ -88,7 +88,7 @@ extension Converter { convert: convertDateToText ) } - + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | public func setOptionalRequestBodyAsText( _ value: T?, diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index b6effc1b..5040ece4 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -17,29 +17,6 @@ extension Converter { // MARK: Miscs - /// Validates that the Content-Type header field (if present) - /// is compatible with the provided content-type substring. - /// - /// Succeeds if no Content-Type header is found in the response headers. - /// - /// - Parameters: - /// - 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. - @available(*, deprecated, message: "Use isValidContentType instead.") - public func validateContentTypeIfPresent( - in headerFields: [HeaderField], - substring: String - ) throws { - guard let contentType = extractContentTypeIfPresent(in: headerFields) else { - return - } - guard isValidContentType(received: contentType, expected: substring) else { - throw RuntimeError.unexpectedContentTypeHeader(contentType) - } - } - /// Returns the content-type header from the provided header fields, if /// present. /// - Parameter headerFields: The header fields to inspect for the content diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift index d71b168e..d709382d 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Server.swift @@ -294,7 +294,7 @@ public extension Converter { convert: convertBinaryToData ) } - + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | func setResponseBodyAsText( _ value: T, diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index cb82c040..e9044f9a 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -728,7 +728,30 @@ extension Converter { } extension Converter { - + + /// Validates that the Content-Type header field (if present) + /// is compatible with the provided content-type substring. + /// + /// Succeeds if no Content-Type header is found in the response headers. + /// + /// - Parameters: + /// - 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. + @available(*, deprecated, message: "Use isValidContentType instead.") + public func validateContentTypeIfPresent( + in headerFields: [HeaderField], + substring: String + ) throws { + guard let contentType = extractContentTypeIfPresent(in: headerFields) else { + return + } + guard isValidContentType(received: contentType, expected: substring) else { + throw RuntimeError.unexpectedContentTypeHeader(contentType) + } + } + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | @available(*, deprecated) public func setOptionalRequestBodyAsText( @@ -773,7 +796,7 @@ extension Converter { convert: convertDateToTextData ) } - + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | @available(*, deprecated) public func setRequiredRequestBodyAsText( @@ -878,7 +901,7 @@ extension Converter { convert: convertDateToTextData ) } - + // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | @available(*, deprecated) public func setResponseBodyAsJSON( @@ -893,7 +916,7 @@ extension Converter { convert: convertBodyCodableToJSON ) } - + // | server | set | response body | binary | data | required | setResponseBodyAsBinary | @available(*, deprecated) public func setResponseBodyAsBinary( @@ -908,7 +931,7 @@ extension Converter { convert: convertDataToBinary ) } - + @available(*, deprecated) public func setRequiredRequestBody( _ value: C, diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift index 92cfb886..aefd7b30 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Client.swift @@ -85,18 +85,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | - @available(*, deprecated) 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" - ) - } + contentType: "text/plain" ) XCTAssertEqual(body, testStringData) XCTAssertEqual( @@ -108,18 +102,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | - @available(*, deprecated) 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" - ) - } + contentType: "text/plain" ) XCTAssertEqual(body, testStringData) XCTAssertEqual( @@ -131,18 +119,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | - @available(*, deprecated) func test_setOptionalRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsText( testDate, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "text/plain" - ) - } + contentType: "text/plain" ) XCTAssertEqual(body, testDateStringData) XCTAssertEqual( @@ -154,18 +136,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | - @available(*, deprecated) func test_setRequiredRequestBodyAsText_date() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsText( testDate, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "text/plain" - ) - } + contentType: "text/plain" ) XCTAssertEqual(body, testDateStringData) XCTAssertEqual( @@ -177,18 +153,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | - @available(*, deprecated) func test_setOptionalRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsJSON( testStruct, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "application/json" - ) - } + contentType: "application/json" ) XCTAssertEqual(body, testStructPrettyData) XCTAssertEqual( @@ -199,18 +169,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { ) } - @available(*, deprecated) func test_setOptionalRequestBodyAsJSON_codable_string() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsJSON( testString, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "application/json" - ) - } + contentType: "application/json" ) XCTAssertEqual(body, testQuotedStringData) XCTAssertEqual( @@ -222,18 +186,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | - @available(*, deprecated) func test_setRequiredRequestBodyAsJSON_codable() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsJSON( testStruct, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "application/json" - ) - } + contentType: "application/json" ) XCTAssertEqual(body, testStructPrettyData) XCTAssertEqual( @@ -245,18 +203,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | - @available(*, deprecated) func test_setOptionalRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] let body = try converter.setOptionalRequestBodyAsBinary( testStringData, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "application/octet-stream" - ) - } + contentType: "application/octet-stream" ) XCTAssertEqual(body, testStringData) XCTAssertEqual( @@ -268,18 +220,12 @@ final class Test_ClientConverterExtensions: Test_Runtime { } // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | - @available(*, deprecated) func test_setRequiredRequestBodyAsBinary_data() throws { var headerFields: [HeaderField] = [] let body = try converter.setRequiredRequestBodyAsBinary( testStringData, headerFields: &headerFields, - transforming: { value in - .init( - value: value, - contentType: "application/octet-stream" - ) - } + contentType: "application/octet-stream" ) XCTAssertEqual(body, testStringData) XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index bfb1ae70..f90a122c 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -18,67 +18,6 @@ final class Test_CommonConverterExtensions: Test_Runtime { // MARK: Miscs - @available(*, deprecated) - func testValidateContentType_match() throws { - let headerFields: [HeaderField] = [ - .init(name: "content-type", value: "application/json") - ] - XCTAssertNoThrow( - try converter.validateContentTypeIfPresent( - in: headerFields, - substring: "application/json" - ) - ) - } - - @available(*, deprecated) - func testValidateContentType_match_substring() throws { - let headerFields: [HeaderField] = [ - .init(name: "content-type", value: "application/json; charset=utf-8") - ] - XCTAssertNoThrow( - try converter.validateContentTypeIfPresent( - in: headerFields, - substring: "application/json" - ) - ) - } - - @available(*, deprecated) - func testValidateContentType_missing() throws { - let headerFields: [HeaderField] = [] - XCTAssertNoThrow( - try converter.validateContentTypeIfPresent( - in: headerFields, - substring: "application/json" - ) - ) - } - - @available(*, deprecated) - func testValidateContentType_mismatch() throws { - let headerFields: [HeaderField] = [ - .init(name: "content-type", value: "text/plain") - ] - XCTAssertThrowsError( - try converter.validateContentTypeIfPresent( - in: headerFields, - substring: "application/json" - ), - "Was expected to throw error on mismatch", - { error in - guard - let err = error as? RuntimeError, - case .unexpectedContentTypeHeader(let contentType) = err - else { - XCTFail("Unexpected kind of error thrown") - return - } - XCTAssertEqual(contentType, "text/plain") - } - ) - } - func testContentTypeMatching() throws { let cases: [(received: String, expected: String, isMatch: Bool)] = [ ("application/json", "application/json", true), diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift index 7fa522e7..03f80d8a 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Server.swift @@ -314,18 +314,12 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | - @available(*, deprecated) func test_setResponseBodyAsText_stringConvertible() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsText( testString, headerFields: &headers, - transforming: { - .init( - value: $0, - contentType: "text/plain" - ) - } + contentType: "text/plain" ) XCTAssertEqual(data, testStringData) XCTAssertEqual( @@ -337,18 +331,12 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | text | date | required | setResponseBodyAsText | - @available(*, deprecated) func test_setResponseBodyAsText_date() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsText( testDate, headerFields: &headers, - transforming: { - .init( - value: $0, - contentType: "text/plain" - ) - } + contentType: "text/plain" ) XCTAssertEqual(data, testDateStringData) XCTAssertEqual( @@ -360,18 +348,12 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | - @available(*, deprecated) func test_setResponseBodyAsJSON_codable() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsJSON( testStruct, headerFields: &headers, - transforming: { - .init( - value: $0, - contentType: "application/json" - ) - } + contentType: "application/json" ) XCTAssertEqual(data, testStructPrettyData) XCTAssertEqual( @@ -383,18 +365,12 @@ final class Test_ServerConverterExtensions: Test_Runtime { } // | server | set | response body | binary | data | required | setResponseBodyAsBinary | - @available(*, deprecated) func test_setResponseBodyAsBinary_data() throws { var headers: [HeaderField] = [] let data = try converter.setResponseBodyAsBinary( testStringData, headerFields: &headers, - transforming: { - .init( - value: $0, - contentType: "application/octet-stream" - ) - } + contentType: "application/octet-stream" ) XCTAssertEqual(data, testStringData) XCTAssertEqual( diff --git a/Tests/OpenAPIRuntimeTests/Deprecated/Test_Deprecated.swift b/Tests/OpenAPIRuntimeTests/Deprecated/Test_Deprecated.swift new file mode 100644 index 00000000..4faafbd8 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Deprecated/Test_Deprecated.swift @@ -0,0 +1,378 @@ +//===----------------------------------------------------------------------===// +// +// 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_Deprecated: Test_Runtime { + + // | client | set | request body | text | string-convertible | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) + func test_deprecated_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") + ] + ) + } + + // | client | set | request body | text | string-convertible | required | setRequiredRequestBodyAsText | + @available(*, deprecated) + func test_deprecated_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") + ] + ) + } + + // | client | set | request body | text | date | optional | setOptionalRequestBodyAsText | + @available(*, deprecated) + func test_deprecated_setOptionalRequestBodyAsText_date() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setOptionalRequestBodyAsText( + testDate, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(body, testDateStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + // | client | set | request body | text | date | required | setRequiredRequestBodyAsText | + @available(*, deprecated) + func test_deprecated_setRequiredRequestBodyAsText_date() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setRequiredRequestBodyAsText( + testDate, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(body, testDateStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + // | client | set | request body | JSON | codable | optional | setOptionalRequestBodyAsJSON | + @available(*, deprecated) + func test_deprecated_setOptionalRequestBodyAsJSON_codable() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setOptionalRequestBodyAsJSON( + testStruct, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "application/json" + ) + } + ) + XCTAssertEqual(body, testStructPrettyData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "application/json") + ] + ) + } + + @available(*, deprecated) + func test_deprecated_setOptionalRequestBodyAsJSON_codable_string() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setOptionalRequestBodyAsJSON( + testString, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "application/json" + ) + } + ) + XCTAssertEqual(body, testQuotedStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "application/json") + ] + ) + } + + // | client | set | request body | JSON | codable | required | setRequiredRequestBodyAsJSON | + @available(*, deprecated) + func test_deprecated_setRequiredRequestBodyAsJSON_codable() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setRequiredRequestBodyAsJSON( + testStruct, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "application/json" + ) + } + ) + XCTAssertEqual(body, testStructPrettyData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "application/json") + ] + ) + } + + // | client | set | request body | binary | data | optional | setOptionalRequestBodyAsBinary | + @available(*, deprecated) + func test_deprecated_setOptionalRequestBodyAsBinary_data() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setOptionalRequestBodyAsBinary( + testStringData, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "application/octet-stream" + ) + } + ) + XCTAssertEqual(body, testStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "application/octet-stream") + ] + ) + } + + // | client | set | request body | binary | data | required | setRequiredRequestBodyAsBinary | + @available(*, deprecated) + func test_deprecated_setRequiredRequestBodyAsBinary_data() throws { + var headerFields: [HeaderField] = [] + let body = try converter.setRequiredRequestBodyAsBinary( + testStringData, + headerFields: &headerFields, + transforming: { value in + .init( + value: value, + contentType: "application/octet-stream" + ) + } + ) + XCTAssertEqual(body, testStringData) + XCTAssertEqual( + headerFields, + [ + .init(name: "content-type", value: "application/octet-stream") + ] + ) + } + + @available(*, deprecated) + func testValidateContentType_match() throws { + let headerFields: [HeaderField] = [ + .init(name: "content-type", value: "application/json") + ] + XCTAssertNoThrow( + try converter.validateContentTypeIfPresent( + in: headerFields, + substring: "application/json" + ) + ) + } + + @available(*, deprecated) + func testValidateContentType_match_substring() throws { + let headerFields: [HeaderField] = [ + .init(name: "content-type", value: "application/json; charset=utf-8") + ] + XCTAssertNoThrow( + try converter.validateContentTypeIfPresent( + in: headerFields, + substring: "application/json" + ) + ) + } + + @available(*, deprecated) + func testValidateContentType_missing() throws { + let headerFields: [HeaderField] = [] + XCTAssertNoThrow( + try converter.validateContentTypeIfPresent( + in: headerFields, + substring: "application/json" + ) + ) + } + + @available(*, deprecated) + func testValidateContentType_mismatch() throws { + let headerFields: [HeaderField] = [ + .init(name: "content-type", value: "text/plain") + ] + XCTAssertThrowsError( + try converter.validateContentTypeIfPresent( + in: headerFields, + substring: "application/json" + ), + "Was expected to throw error on mismatch", + { error in + guard + let err = error as? RuntimeError, + case .unexpectedContentTypeHeader(let contentType) = err + else { + XCTFail("Unexpected kind of error thrown") + return + } + XCTAssertEqual(contentType, "text/plain") + } + ) + } + + // | server | set | response body | text | string-convertible | required | setResponseBodyAsText | + @available(*, deprecated) + func test_deprecated_setResponseBodyAsText_stringConvertible() throws { + var headers: [HeaderField] = [] + let data = try converter.setResponseBodyAsText( + testString, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(data, testStringData) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + // | server | set | response body | text | date | required | setResponseBodyAsText | + @available(*, deprecated) + func test_deprecated_setResponseBodyAsText_date() throws { + var headers: [HeaderField] = [] + let data = try converter.setResponseBodyAsText( + testDate, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "text/plain" + ) + } + ) + XCTAssertEqual(data, testDateStringData) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "text/plain") + ] + ) + } + + // | server | set | response body | JSON | codable | required | setResponseBodyAsJSON | + @available(*, deprecated) + func test_deprecated_setResponseBodyAsJSON_codable() throws { + var headers: [HeaderField] = [] + let data = try converter.setResponseBodyAsJSON( + testStruct, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "application/json" + ) + } + ) + XCTAssertEqual(data, testStructPrettyData) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "application/json") + ] + ) + } + + // | server | set | response body | binary | data | required | setResponseBodyAsBinary | + @available(*, deprecated) + func test_deprecated_setResponseBodyAsBinary_data() throws { + var headers: [HeaderField] = [] + let data = try converter.setResponseBodyAsBinary( + testStringData, + headerFields: &headers, + transforming: { + .init( + value: $0, + contentType: "application/octet-stream" + ) + } + ) + XCTAssertEqual(data, testStringData) + XCTAssertEqual( + headers, + [ + .init(name: "content-type", value: "application/octet-stream") + ] + ) + } + +} From 25049e8d1c0109105588f3785c054a47b247d369 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Mon, 31 Jul 2023 09:49:20 +0200 Subject: [PATCH 06/16] Move a function for a smaller PR diff --- .../Conversion/CurrencyExtensions.swift | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift index 950c5a7c..4859ea1f 100644 --- a/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift +++ b/Sources/OpenAPIRuntime/Conversion/CurrencyExtensions.swift @@ -406,16 +406,6 @@ extension Converter { return body } - func setResponseBody( - _ value: T, - headerFields: inout [HeaderField], - contentType: String, - convert: (T) throws -> Data - ) throws -> Data { - headerFields.add(name: "content-type", value: contentType) - return try convert(value) - } - func getResponseBody( _ type: T.Type, from data: Data, @@ -427,6 +417,16 @@ extension Converter { return transformedValue } + func setResponseBody( + _ value: T, + headerFields: inout [HeaderField], + contentType: String, + convert: (T) throws -> Data + ) throws -> Data { + headerFields.add(name: "content-type", value: contentType) + return try convert(value) + } + func convertBinaryToData( _ binary: Data ) throws -> Data { From ddc431db50384e27be1c929eba378a1ee360d9aa Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 08:44:40 +0200 Subject: [PATCH 07/16] Update Sources/OpenAPIRuntime/Conversion/Converter+Common.swift Co-authored-by: Gustavo Cairo --- Sources/OpenAPIRuntime/Conversion/Converter+Common.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 5040ece4..0a84423f 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -34,7 +34,7 @@ extension Converter { /// The expected content type can contain wildcards, such as */* and text/*. /// - Parameters: /// - received: The concrete content type to validate against the other. - /// - expected: The expected content type, can be a wildcard. + /// - expected: The expected content type, can contain wildcards. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. public func isValidContentType(received: String?, expected: String) -> Bool { From efc85c2b8f1be9b5931782c4b606d07b845015f6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 10:22:27 +0200 Subject: [PATCH 08/16] PR feedback: Introduce a MIMEType wrapper type --- .../Conversion/Converter+Common.swift | 47 ++---- .../OpenAPIRuntime/Conversion/MIMEType.swift | 152 ++++++++++++++++++ .../Deprecated/Deprecated.swift | 2 +- .../Base/Test_MIMEType.swift | 102 ++++++++++++ .../Conversion/Test_Converter+Common.swift | 2 +- 5 files changed, 272 insertions(+), 33 deletions(-) create mode 100644 Sources/OpenAPIRuntime/Conversion/MIMEType.swift create mode 100644 Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 0a84423f..a751d3e0 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -17,13 +17,15 @@ extension Converter { // MARK: Miscs - /// Returns the content-type header from the provided header fields, if - /// present. + /// Returns the MIME type from the content-type header, if present. /// - Parameter headerFields: The header fields to inspect for the content /// type header. - /// - Returns: The content type value, or nil if not found. - public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> String? { - headerFields.firstValue(name: "content-type") + /// - Returns: The content type value, or nil if not found or invalid. + public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> MIMEType? { + guard let rawValue = headerFields.firstValue(name: "content-type") else { + return nil + } + return MIMEType(rawValue) } /// Checks whether a concrete content type matches an expected content type. @@ -37,48 +39,31 @@ extension Converter { /// - expected: The expected content type, can contain wildcards. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. - public func isValidContentType(received: String?, expected: String) -> Bool { + public func isValidContentType(received: MIMEType?, expected: String) -> Bool { guard let received else { return false } - func parseContentType(_ value: String) -> (main: String, sub: String)? { - let components = - value - // Normalize to lowercase. - .lowercased() - // Drop any charset and other parameters. - .split(separator: ";")[0] - // Parse out main type and subtype. - .split(separator: "/") - .map(String.init) - guard components.count == 2 else { - return nil - } - return (components[0], components[1]) - } - guard - let receivedContentType = parseContentType(received), - let expectedContentType = parseContentType(expected) - else { + let receivedContentType = received + guard let expectedContentType = MIMEType(expected) else { return false } - if expectedContentType.main == "*" { + if expectedContentType.type == .wildcard { return true } - if expectedContentType.main != receivedContentType.main { + if expectedContentType.type != receivedContentType.type { return false } - if expectedContentType.sub == "*" { + if expectedContentType.subtype == .wildcard { return true } - return expectedContentType.sub == receivedContentType.sub + return expectedContentType.subtype == receivedContentType.subtype } /// Returns an error to be thrown when an unexpected content type is /// received. /// - Parameter contentType: The content type that was received. - public func makeUnexpectedContentTypeError(contentType: String?) -> any Error { - RuntimeError.unexpectedContentTypeHeader(contentType ?? "") + public func makeUnexpectedContentTypeError(contentType: MIMEType?) -> any Error { + RuntimeError.unexpectedContentTypeHeader(contentType?.description ?? "") } // MARK: - Converter helper methods diff --git a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift new file mode 100644 index 00000000..32da9377 --- /dev/null +++ b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift @@ -0,0 +1,152 @@ +//===----------------------------------------------------------------------===// +// +// 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 Foundation + +/// A container for a parsed, valid MIME type. +@_spi(Generated) +public struct MIMEType: Equatable { + + /// The value of the type or subtype in the MIME type. + public enum Token: Equatable { + + /// Any value, represented as `*`. + case wildcard + + /// A concrete value. + case concrete(String) + + public static func ==(lhs: Token, rhs: Token) -> Bool { + // Case-insensitive + lhs.description.lowercased() == rhs.description.lowercased() + } + } + + /// The type – the first token. + public var type: Token + + /// The subtype – the second token. + public var subtype: Token + + /// Any optional parameters. + public var parameters: [String: String] + + /// Creates a new MIME type. + /// - Parameters: + /// - type: The type – the first token. + /// - subtype: The subtype – the second token. + /// - parameters: Any optional parameters. + public init(type: Token, subtype: Token, parameters: [String: String] = [:]) { + self.type = type + self.subtype = subtype + self.parameters = parameters + } + + public static func ==(lhs: MIMEType, rhs: MIMEType) -> Bool { + guard lhs.type == rhs.type else { + return false + } + guard lhs.subtype == rhs.subtype else { + return false + } + // Parameter names are case-insensitive, parameter values are + // case-sensitive. + guard lhs.parameters.count == rhs.parameters.count else { + return false + } + func normalizeKeyValue(key: String, value: String) -> (String, String) { + (key.lowercased(), value) + } + let normalizedLeftParams = Dictionary( + uniqueKeysWithValues: lhs.parameters.map(normalizeKeyValue) + ) + let normalizedRightParams = Dictionary( + uniqueKeysWithValues: rhs.parameters.map(normalizeKeyValue) + ) + return normalizedLeftParams == normalizedRightParams + } +} + +extension MIMEType.Token: LosslessStringConvertible { + public init?(_ description: String) { + if description == "*" { + self = .wildcard + } else { + self = .concrete(description) + } + } + + public var description: String { + switch self { + case .wildcard: + return "*" + case .concrete(let string): + return string + } + } +} + +extension MIMEType: LosslessStringConvertible { + public init?(_ description: String) { + var components = description + // Split by semicolon + .split(separator: ";") + .map(String.init) + // Trim leading/trailing spaces + .map { $0.soar_trimmingLeadingAndTrailingSpaces } + guard !components.isEmpty else { + return nil + } + let firstComponent = components.removeFirst() + let typeAndSubtype = firstComponent + .split(separator: "/") + .map(String.init) + .compactMap(MIMEType.Token.init) + guard typeAndSubtype.count == 2 else { + return nil + } + func parseParameter(_ string: String) -> (String, String)? { + let components = string + .split(separator: "=") + .map(String.init) + guard components.count == 2 else { + return nil + } + return (components[0], components[1]) + } + let parameters = components + .compactMap(parseParameter) + self.init( + type: typeAndSubtype[0], + subtype: typeAndSubtype[1], + parameters: Dictionary( + parameters, + uniquingKeysWith: { a, _ in a } + ) + ) + } + + public var description: String { + (["\(type.description)/\(subtype.description)"] + + parameters + .sorted(by: { a, b in a.key < b.key }) + .map { "\($0)=\($1)" }) + .joined(separator: "; ") + } +} + +extension String { + fileprivate var soar_trimmingLeadingAndTrailingSpaces: Self { + trimmingCharacters(in: .whitespacesAndNewlines) + } +} diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 9ca2e85c..1caac70b 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -748,7 +748,7 @@ extension Converter { return } guard isValidContentType(received: contentType, expected: substring) else { - throw RuntimeError.unexpectedContentTypeHeader(contentType) + throw RuntimeError.unexpectedContentTypeHeader(contentType.description) } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift new file mode 100644 index 00000000..4a19dea9 --- /dev/null +++ b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift @@ -0,0 +1,102 @@ +//===----------------------------------------------------------------------===// +// +// 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) import OpenAPIRuntime + +final class Test_MIMEType: Test_Runtime { + func test() throws { + let cases: [(String, MIMEType?, String?)] = [ + + // Common + ( + "application/json", + MIMEType( + type: .concrete("application"), + subtype: .concrete("json") + ), + "application/json" + ), + + // Subtype wildcard + ( + "application/*", + MIMEType( + type: .concrete("application"), + subtype: .wildcard + ), + "application/*" + ), + + // Type wildcard + ( + "*/*", + MIMEType( + type: .wildcard, + subtype: .wildcard + ), + "*/*" + ), + + // Common with a parameter + ( + "application/json; charset=UTF-8", + MIMEType( + type: .concrete("application"), + subtype: .concrete("json"), + parameters: [ + "charset": "UTF-8" + ] + ), + "application/json; charset=UTF-8" + ), + + // Common with two parameters + ( + "application/json; charset=UTF-8; boundary=1234", + MIMEType( + type: .concrete("application"), + subtype: .concrete("json"), + parameters: [ + "charset": "UTF-8", + "boundary": "1234" + ] + ), + "application/json; boundary=1234; charset=UTF-8" + ), + + // Common case preserving, but case insensitive equality + ( + "APPLICATION/JSON;CHARSET=UTF-8", + MIMEType( + type: .concrete("application"), + subtype: .concrete("json"), + parameters: [ + "charset": "UTF-8" + ] + ), + "APPLICATION/JSON; CHARSET=UTF-8" + ), + + // Invalid + ("application", nil, nil), + ("application/foo/bar", nil, nil), + ("", nil, nil), + ] + for (inputString, expectedMIME, outputString) in cases { + let mime = MIMEType(inputString) + XCTAssertEqual(mime, expectedMIME) + XCTAssertEqual(mime?.description, outputString) + } + } +} diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index f90a122c..a254f32c 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -38,7 +38,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { for testCase in cases { XCTAssertEqual( converter.isValidContentType( - received: testCase.received, + received: .init(testCase.received), expected: testCase.expected ), testCase.isMatch, From 7f832ac9e4aeb400f46c99c0c8880a4e3e673325 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 10:25:52 +0200 Subject: [PATCH 09/16] Fix formatting --- .../OpenAPIRuntime/Conversion/MIMEType.swift | 42 ++++++++++--------- .../Base/Test_MIMEType.swift | 10 ++--- 2 files changed, 28 insertions(+), 24 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift index 32da9377..db07d0b1 100644 --- a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift +++ b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift @@ -16,31 +16,31 @@ import Foundation /// A container for a parsed, valid MIME type. @_spi(Generated) public struct MIMEType: Equatable { - + /// The value of the type or subtype in the MIME type. public enum Token: Equatable { - + /// Any value, represented as `*`. case wildcard - + /// A concrete value. case concrete(String) - - public static func ==(lhs: Token, rhs: Token) -> Bool { + + public static func == (lhs: Token, rhs: Token) -> Bool { // Case-insensitive lhs.description.lowercased() == rhs.description.lowercased() } } - + /// The type – the first token. public var type: Token - + /// The subtype – the second token. public var subtype: Token - + /// Any optional parameters. public var parameters: [String: String] - + /// Creates a new MIME type. /// - Parameters: /// - type: The type – the first token. @@ -51,8 +51,8 @@ public struct MIMEType: Equatable { self.subtype = subtype self.parameters = parameters } - - public static func ==(lhs: MIMEType, rhs: MIMEType) -> Bool { + + public static func == (lhs: MIMEType, rhs: MIMEType) -> Bool { guard lhs.type == rhs.type else { return false } @@ -85,7 +85,7 @@ extension MIMEType.Token: LosslessStringConvertible { self = .concrete(description) } } - + public var description: String { switch self { case .wildcard: @@ -98,7 +98,8 @@ extension MIMEType.Token: LosslessStringConvertible { extension MIMEType: LosslessStringConvertible { public init?(_ description: String) { - var components = description + var components = + description // Split by semicolon .split(separator: ";") .map(String.init) @@ -108,7 +109,8 @@ extension MIMEType: LosslessStringConvertible { return nil } let firstComponent = components.removeFirst() - let typeAndSubtype = firstComponent + let typeAndSubtype = + firstComponent .split(separator: "/") .map(String.init) .compactMap(MIMEType.Token.init) @@ -116,7 +118,8 @@ extension MIMEType: LosslessStringConvertible { return nil } func parseParameter(_ string: String) -> (String, String)? { - let components = string + let components = + string .split(separator: "=") .map(String.init) guard components.count == 2 else { @@ -124,7 +127,8 @@ extension MIMEType: LosslessStringConvertible { } return (components[0], components[1]) } - let parameters = components + let parameters = + components .compactMap(parseParameter) self.init( type: typeAndSubtype[0], @@ -135,13 +139,13 @@ extension MIMEType: LosslessStringConvertible { ) ) } - + public var description: String { (["\(type.description)/\(subtype.description)"] - + parameters + + parameters .sorted(by: { a, b in a.key < b.key }) .map { "\($0)=\($1)" }) - .joined(separator: "; ") + .joined(separator: "; ") } } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift index 4a19dea9..d3099b46 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift @@ -17,7 +17,7 @@ import XCTest final class Test_MIMEType: Test_Runtime { func test() throws { let cases: [(String, MIMEType?, String?)] = [ - + // Common ( "application/json", @@ -27,7 +27,7 @@ final class Test_MIMEType: Test_Runtime { ), "application/json" ), - + // Subtype wildcard ( "application/*", @@ -47,7 +47,7 @@ final class Test_MIMEType: Test_Runtime { ), "*/*" ), - + // Common with a parameter ( "application/json; charset=UTF-8", @@ -60,7 +60,7 @@ final class Test_MIMEType: Test_Runtime { ), "application/json; charset=UTF-8" ), - + // Common with two parameters ( "application/json; charset=UTF-8; boundary=1234", @@ -69,7 +69,7 @@ final class Test_MIMEType: Test_Runtime { subtype: .concrete("json"), parameters: [ "charset": "UTF-8", - "boundary": "1234" + "boundary": "1234", ] ), "application/json; boundary=1234; charset=UTF-8" From 606aa0ee56db8cfee6f61108c4e75a5773571f52 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 11:42:45 +0200 Subject: [PATCH 10/16] PR feedback: s/isValidContentType/isMatchingContentType --- Sources/OpenAPIRuntime/Conversion/Converter+Common.swift | 2 +- Sources/OpenAPIRuntime/Deprecated/Deprecated.swift | 4 ++-- .../Conversion/Test_Converter+Common.swift | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index a751d3e0..58b356b6 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -39,7 +39,7 @@ extension Converter { /// - expected: The expected content type, can contain wildcards. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. - public func isValidContentType(received: MIMEType?, expected: String) -> Bool { + public func isMatchingContentType(received: MIMEType?, expected: String) -> Bool { guard let received else { return false } diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 1caac70b..6409e19b 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -739,7 +739,7 @@ extension Converter { /// - substring: Expected content type. /// - Throws: If the response's Content-Type value is not compatible with /// the provided substring. - @available(*, deprecated, message: "Use isValidContentType instead.") + @available(*, deprecated, message: "Use isMatchingContentType instead.") public func validateContentTypeIfPresent( in headerFields: [HeaderField], substring: String @@ -747,7 +747,7 @@ extension Converter { guard let contentType = extractContentTypeIfPresent(in: headerFields) else { return } - guard isValidContentType(received: contentType, expected: substring) else { + guard isMatchingContentType(received: contentType, expected: substring) else { throw RuntimeError.unexpectedContentTypeHeader(contentType.description) } } diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index a254f32c..eda28cc7 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -37,7 +37,7 @@ final class Test_CommonConverterExtensions: Test_Runtime { ] for testCase in cases { XCTAssertEqual( - converter.isValidContentType( + converter.isMatchingContentType( received: .init(testCase.received), expected: testCase.expected ), From 5752e18b1ae469197ae5e38f7533edafb8ddbc10 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 11:59:54 +0200 Subject: [PATCH 11/16] PR feedback: no need to prefix fileprivate extensions --- Sources/OpenAPIRuntime/Conversion/MIMEType.swift | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift index db07d0b1..4fb14f57 100644 --- a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift +++ b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift @@ -104,7 +104,7 @@ extension MIMEType: LosslessStringConvertible { .split(separator: ";") .map(String.init) // Trim leading/trailing spaces - .map { $0.soar_trimmingLeadingAndTrailingSpaces } + .map { $0.trimmingLeadingAndTrailingSpaces } guard !components.isEmpty else { return nil } @@ -150,7 +150,7 @@ extension MIMEType: LosslessStringConvertible { } extension String { - fileprivate var soar_trimmingLeadingAndTrailingSpaces: Self { + fileprivate var trimmingLeadingAndTrailingSpaces: Self { trimmingCharacters(in: .whitespacesAndNewlines) } } From de2248b6beecc256d2b5be5c74fcd1c849e0f4db Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 12:30:29 +0200 Subject: [PATCH 12/16] PR feedback: restructure MIMEType to disallow json/* --- .../Conversion/Converter+Common.swift | 17 ++-- .../OpenAPIRuntime/Conversion/MIMEType.swift | 95 +++++++++++-------- .../Base/Test_MIMEType.swift | 24 ++--- 3 files changed, 69 insertions(+), 67 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 58b356b6..88adf95c 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -43,20 +43,21 @@ extension Converter { guard let received else { return false } - let receivedContentType = received - guard let expectedContentType = MIMEType(expected) else { + guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { return false } - if expectedContentType.type == .wildcard { - return true - } - if expectedContentType.type != receivedContentType.type { + guard let expectedContentType = MIMEType(expected) else { return false } - if expectedContentType.subtype == .wildcard { + switch expectedContentType.kind { + case .any: return true + case .anySubtype(let expectedType): + return receivedType.lowercased() == expectedType.lowercased() + case .concrete(let expectedType, let expectedSubtype): + return receivedType.lowercased() == expectedType.lowercased() + && receivedSubtype.lowercased() == expectedSubtype.lowercased() } - return expectedContentType.subtype == receivedContentType.subtype } /// Returns an error to be thrown when an unexpected content type is diff --git a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift index 4fb14f57..76c8da00 100644 --- a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift +++ b/Sources/OpenAPIRuntime/Conversion/MIMEType.swift @@ -17,46 +17,50 @@ import Foundation @_spi(Generated) public struct MIMEType: Equatable { - /// The value of the type or subtype in the MIME type. - public enum Token: Equatable { + /// The kind of the MIME type. + public enum Kind: Equatable { - /// Any value, represented as `*`. - case wildcard + /// Any, spelled as `*/*`. + case any - /// A concrete value. - case concrete(String) + /// Any subtype of a concrete type, spelled as `type/*`. + case anySubtype(type: String) - public static func == (lhs: Token, rhs: Token) -> Bool { - // Case-insensitive - lhs.description.lowercased() == rhs.description.lowercased() + /// A concrete value, spelled as `type/subtype`. + case concrete(type: String, subtype: String) + + public static func == (lhs: Kind, rhs: Kind) -> Bool { + switch (lhs, rhs) { + case (.any, .any): + return true + case let (.anySubtype(lhsType), .anySubtype(rhsType)): + return lhsType.lowercased() == rhsType.lowercased() + case let (.concrete(lhsType, lhsSubtype), .concrete(rhsType, rhsSubtype)): + return lhsType.lowercased() == rhsType.lowercased() + && lhsSubtype.lowercased() == rhsSubtype.lowercased() + default: + return false + } } } - /// The type – the first token. - public var type: Token - - /// The subtype – the second token. - public var subtype: Token + /// The kind of the MIME type. + public var kind: Kind /// Any optional parameters. public var parameters: [String: String] /// Creates a new MIME type. /// - Parameters: - /// - type: The type – the first token. - /// - subtype: The subtype – the second token. + /// - kind: The kind of the MIME type. /// - parameters: Any optional parameters. - public init(type: Token, subtype: Token, parameters: [String: String] = [:]) { - self.type = type - self.subtype = subtype + public init(kind: Kind, parameters: [String: String] = [:]) { + self.kind = kind self.parameters = parameters } public static func == (lhs: MIMEType, rhs: MIMEType) -> Bool { - guard lhs.type == rhs.type else { - return false - } - guard lhs.subtype == rhs.subtype else { + guard lhs.kind == rhs.kind else { return false } // Parameter names are case-insensitive, parameter values are @@ -77,21 +81,36 @@ public struct MIMEType: Equatable { } } -extension MIMEType.Token: LosslessStringConvertible { +extension MIMEType.Kind: LosslessStringConvertible { public init?(_ description: String) { - if description == "*" { - self = .wildcard - } else { - self = .concrete(description) + let typeAndSubtype = + description + .split(separator: "/") + .map(String.init) + guard typeAndSubtype.count == 2 else { + return nil + } + switch (typeAndSubtype[0], typeAndSubtype[1]) { + case ("*", let subtype): + guard subtype == "*" else { + return nil + } + self = .any + case (let type, "*"): + self = .anySubtype(type: type) + case (let type, let subtype): + self = .concrete(type: type, subtype: subtype) } } public var description: String { switch self { - case .wildcard: - return "*" - case .concrete(let string): - return string + case .any: + return "*/*" + case .anySubtype(let type): + return "\(type)/*" + case .concrete(let type, let subtype): + return "\(type)/\(subtype)" } } } @@ -109,12 +128,7 @@ extension MIMEType: LosslessStringConvertible { return nil } let firstComponent = components.removeFirst() - let typeAndSubtype = - firstComponent - .split(separator: "/") - .map(String.init) - .compactMap(MIMEType.Token.init) - guard typeAndSubtype.count == 2 else { + guard let kind = MIMEType.Kind(firstComponent) else { return nil } func parseParameter(_ string: String) -> (String, String)? { @@ -131,8 +145,7 @@ extension MIMEType: LosslessStringConvertible { components .compactMap(parseParameter) self.init( - type: typeAndSubtype[0], - subtype: typeAndSubtype[1], + kind: kind, parameters: Dictionary( parameters, uniquingKeysWith: { a, _ in a } @@ -141,7 +154,7 @@ extension MIMEType: LosslessStringConvertible { } public var description: String { - (["\(type.description)/\(subtype.description)"] + ([kind.description] + parameters .sorted(by: { a, b in a.key < b.key }) .map { "\($0)=\($1)" }) diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift index d3099b46..411e0851 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift @@ -21,30 +21,21 @@ final class Test_MIMEType: Test_Runtime { // Common ( "application/json", - MIMEType( - type: .concrete("application"), - subtype: .concrete("json") - ), + MIMEType(kind: .concrete(type: "application", subtype: "json")), "application/json" ), // Subtype wildcard ( "application/*", - MIMEType( - type: .concrete("application"), - subtype: .wildcard - ), + MIMEType(kind: .anySubtype(type: "application")), "application/*" ), // Type wildcard ( "*/*", - MIMEType( - type: .wildcard, - subtype: .wildcard - ), + MIMEType(kind: .any), "*/*" ), @@ -52,8 +43,7 @@ final class Test_MIMEType: Test_Runtime { ( "application/json; charset=UTF-8", MIMEType( - type: .concrete("application"), - subtype: .concrete("json"), + kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8" ] @@ -65,8 +55,7 @@ final class Test_MIMEType: Test_Runtime { ( "application/json; charset=UTF-8; boundary=1234", MIMEType( - type: .concrete("application"), - subtype: .concrete("json"), + kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8", "boundary": "1234", @@ -79,8 +68,7 @@ final class Test_MIMEType: Test_Runtime { ( "APPLICATION/JSON;CHARSET=UTF-8", MIMEType( - type: .concrete("application"), - subtype: .concrete("json"), + kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8" ] From 09b68a74d1821fcb694e79f77adf7c05408ae326 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 12:34:18 +0200 Subject: [PATCH 13/16] PR feedback: rename MIMEType to OpenAPIMIMEType --- .../OpenAPIMIMEType.swift} | 10 +++++----- .../Conversion/Converter+Common.swift | 10 +++++----- ...METype.swift => Test_OpenAPIMIMEType.swift} | 18 +++++++++--------- 3 files changed, 19 insertions(+), 19 deletions(-) rename Sources/OpenAPIRuntime/{Conversion/MIMEType.swift => Base/OpenAPIMIMEType.swift} (94%) rename Tests/OpenAPIRuntimeTests/Base/{Test_MIMEType.swift => Test_OpenAPIMIMEType.swift} (84%) diff --git a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift similarity index 94% rename from Sources/OpenAPIRuntime/Conversion/MIMEType.swift rename to Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index 76c8da00..2c1759ca 100644 --- a/Sources/OpenAPIRuntime/Conversion/MIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -15,7 +15,7 @@ import Foundation /// A container for a parsed, valid MIME type. @_spi(Generated) -public struct MIMEType: Equatable { +public struct OpenAPIMIMEType: Equatable { /// The kind of the MIME type. public enum Kind: Equatable { @@ -59,7 +59,7 @@ public struct MIMEType: Equatable { self.parameters = parameters } - public static func == (lhs: MIMEType, rhs: MIMEType) -> Bool { + public static func == (lhs: OpenAPIMIMEType, rhs: OpenAPIMIMEType) -> Bool { guard lhs.kind == rhs.kind else { return false } @@ -81,7 +81,7 @@ public struct MIMEType: Equatable { } } -extension MIMEType.Kind: LosslessStringConvertible { +extension OpenAPIMIMEType.Kind: LosslessStringConvertible { public init?(_ description: String) { let typeAndSubtype = description @@ -115,7 +115,7 @@ extension MIMEType.Kind: LosslessStringConvertible { } } -extension MIMEType: LosslessStringConvertible { +extension OpenAPIMIMEType: LosslessStringConvertible { public init?(_ description: String) { var components = description @@ -128,7 +128,7 @@ extension MIMEType: LosslessStringConvertible { return nil } let firstComponent = components.removeFirst() - guard let kind = MIMEType.Kind(firstComponent) else { + guard let kind = OpenAPIMIMEType.Kind(firstComponent) else { return nil } func parseParameter(_ string: String) -> (String, String)? { diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 88adf95c..83dbedfb 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -21,11 +21,11 @@ extension Converter { /// - Parameter headerFields: The header fields to inspect for the content /// type header. /// - Returns: The content type value, or nil if not found or invalid. - public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> MIMEType? { + public func extractContentTypeIfPresent(in headerFields: [HeaderField]) -> OpenAPIMIMEType? { guard let rawValue = headerFields.firstValue(name: "content-type") else { return nil } - return MIMEType(rawValue) + return OpenAPIMIMEType(rawValue) } /// Checks whether a concrete content type matches an expected content type. @@ -39,14 +39,14 @@ extension Converter { /// - expected: The expected content type, can contain wildcards. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. - public func isMatchingContentType(received: MIMEType?, expected: String) -> Bool { + public func isMatchingContentType(received: OpenAPIMIMEType?, expected: String) -> Bool { guard let received else { return false } guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { return false } - guard let expectedContentType = MIMEType(expected) else { + guard let expectedContentType = OpenAPIMIMEType(expected) else { return false } switch expectedContentType.kind { @@ -63,7 +63,7 @@ extension Converter { /// Returns an error to be thrown when an unexpected content type is /// received. /// - Parameter contentType: The content type that was received. - public func makeUnexpectedContentTypeError(contentType: MIMEType?) -> any Error { + public func makeUnexpectedContentTypeError(contentType: OpenAPIMIMEType?) -> any Error { RuntimeError.unexpectedContentTypeHeader(contentType?.description ?? "") } diff --git a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift similarity index 84% rename from Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift rename to Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift index 411e0851..5aacd455 100644 --- a/Tests/OpenAPIRuntimeTests/Base/Test_MIMEType.swift +++ b/Tests/OpenAPIRuntimeTests/Base/Test_OpenAPIMIMEType.swift @@ -14,35 +14,35 @@ import XCTest @_spi(Generated) import OpenAPIRuntime -final class Test_MIMEType: Test_Runtime { +final class Test_OpenAPIMIMEType: Test_Runtime { func test() throws { - let cases: [(String, MIMEType?, String?)] = [ + let cases: [(String, OpenAPIMIMEType?, String?)] = [ // Common ( "application/json", - MIMEType(kind: .concrete(type: "application", subtype: "json")), + OpenAPIMIMEType(kind: .concrete(type: "application", subtype: "json")), "application/json" ), // Subtype wildcard ( "application/*", - MIMEType(kind: .anySubtype(type: "application")), + OpenAPIMIMEType(kind: .anySubtype(type: "application")), "application/*" ), // Type wildcard ( "*/*", - MIMEType(kind: .any), + OpenAPIMIMEType(kind: .any), "*/*" ), // Common with a parameter ( "application/json; charset=UTF-8", - MIMEType( + OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8" @@ -54,7 +54,7 @@ final class Test_MIMEType: Test_Runtime { // Common with two parameters ( "application/json; charset=UTF-8; boundary=1234", - MIMEType( + OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8", @@ -67,7 +67,7 @@ final class Test_MIMEType: Test_Runtime { // Common case preserving, but case insensitive equality ( "APPLICATION/JSON;CHARSET=UTF-8", - MIMEType( + OpenAPIMIMEType( kind: .concrete(type: "application", subtype: "json"), parameters: [ "charset": "UTF-8" @@ -82,7 +82,7 @@ final class Test_MIMEType: Test_Runtime { ("", nil, nil), ] for (inputString, expectedMIME, outputString) in cases { - let mime = MIMEType(inputString) + let mime = OpenAPIMIMEType(inputString) XCTAssertEqual(mime, expectedMIME) XCTAssertEqual(mime?.description, outputString) } From 6df8431d85d837cb59bbbbe850f8554f4b4d6833 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 12:37:06 +0200 Subject: [PATCH 14/16] PR feedback: Add a comment about how we're handling duplicate parameter names --- Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index 2c1759ca..5479dbb4 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -148,6 +148,7 @@ extension OpenAPIMIMEType: LosslessStringConvertible { kind: kind, parameters: Dictionary( parameters, + // Pick the first value when duplicate parameters are provided. uniquingKeysWith: { a, _ in a } ) ) From e0dae8558525888f24b2d875375dfd0856b150c6 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 13:52:13 +0200 Subject: [PATCH 15/16] PR feedback: Make isMatchingContentType stricter + rename expected to expetedRaw --- Sources/OpenAPIRuntime/Conversion/Converter+Common.swift | 9 +++++---- Sources/OpenAPIRuntime/Deprecated/Deprecated.swift | 2 +- Sources/OpenAPIRuntime/Errors/RuntimeError.swift | 3 +++ .../Conversion/Test_Converter+Common.swift | 4 ++-- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift index 83dbedfb..373b8105 100644 --- a/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift +++ b/Sources/OpenAPIRuntime/Conversion/Converter+Common.swift @@ -36,18 +36,19 @@ extension Converter { /// The expected content type can contain wildcards, such as */* and text/*. /// - Parameters: /// - received: The concrete content type to validate against the other. - /// - expected: The expected content type, can contain wildcards. + /// - expectedRaw: The expected content type, can contain wildcards. + /// - Throws: A `RuntimeError` when `expectedRaw` is not a valid content type. /// - Returns: A Boolean value representing whether the concrete content /// type matches the expected one. - public func isMatchingContentType(received: OpenAPIMIMEType?, expected: String) -> Bool { + public func isMatchingContentType(received: OpenAPIMIMEType?, expectedRaw: String) throws -> Bool { guard let received else { return false } guard case let .concrete(type: receivedType, subtype: receivedSubtype) = received.kind else { return false } - guard let expectedContentType = OpenAPIMIMEType(expected) else { - return false + guard let expectedContentType = OpenAPIMIMEType(expectedRaw) else { + throw RuntimeError.invalidExpectedContentType(expectedRaw) } switch expectedContentType.kind { case .any: diff --git a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift index 6409e19b..6ddef107 100644 --- a/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift +++ b/Sources/OpenAPIRuntime/Deprecated/Deprecated.swift @@ -747,7 +747,7 @@ extension Converter { guard let contentType = extractContentTypeIfPresent(in: headerFields) else { return } - guard isMatchingContentType(received: contentType, expected: substring) else { + guard try isMatchingContentType(received: contentType, expectedRaw: substring) else { throw RuntimeError.unexpectedContentTypeHeader(contentType.description) } } diff --git a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift index 1a2143b9..d8a72841 100644 --- a/Sources/OpenAPIRuntime/Errors/RuntimeError.swift +++ b/Sources/OpenAPIRuntime/Errors/RuntimeError.swift @@ -19,6 +19,7 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret // Miscs case invalidServerURL(String) + case invalidExpectedContentType(String) // Data conversion case failedToDecodeStringConvertibleValue(type: String) @@ -51,6 +52,8 @@ internal enum RuntimeError: Error, CustomStringConvertible, LocalizedError, Pret switch self { case .invalidServerURL(let string): return "Invalid server URL: \(string)" + case .invalidExpectedContentType(let string): + return "Invalid expected content type: '\(string)'" case .failedToDecodeStringConvertibleValue(let string): return "Failed to decode a value of type '\(string)'." case .missingRequiredHeaderField(let name): diff --git a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift index eda28cc7..27408e60 100644 --- a/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift +++ b/Tests/OpenAPIRuntimeTests/Conversion/Test_Converter+Common.swift @@ -37,9 +37,9 @@ final class Test_CommonConverterExtensions: Test_Runtime { ] for testCase in cases { XCTAssertEqual( - converter.isMatchingContentType( + try converter.isMatchingContentType( received: .init(testCase.received), - expected: testCase.expected + expectedRaw: testCase.expected ), testCase.isMatch, "Wrong result for (\(testCase.received), \(testCase.expected), \(testCase.isMatch))" From f4678b2b263abc33d3dc367cb0be1d07e161c2f8 Mon Sep 17 00:00:00 2001 From: Honza Dvorsky Date: Thu, 3 Aug 2023 18:05:30 +0200 Subject: [PATCH 16/16] Only allocate dictionaries if there are parameters --- Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift index 5479dbb4..d7386cb4 100644 --- a/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift +++ b/Sources/OpenAPIRuntime/Base/OpenAPIMIMEType.swift @@ -68,6 +68,9 @@ public struct OpenAPIMIMEType: Equatable { guard lhs.parameters.count == rhs.parameters.count else { return false } + if lhs.parameters.isEmpty { + return true + } func normalizeKeyValue(key: String, value: String) -> (String, String) { (key.lowercased(), value) }