diff --git a/Sources/OpenAPIKit/Content/ContentEncoding.swift b/Sources/OpenAPIKit/Content/ContentEncoding.swift index 6f63992b96..f0c1c2079e 100644 --- a/Sources/OpenAPIKit/Content/ContentEncoding.swift +++ b/Sources/OpenAPIKit/Content/ContentEncoding.swift @@ -27,8 +27,6 @@ extension OpenAPI.Content { /// where the values are anything codable. public var vendorExtensions: [String: AnyCodable] - /// The singular `contentType` argument is only provided for backwards compatibility and - /// using the plural `contentTypes` argument should be preferred. public init( contentTypes: [OpenAPI.ContentType] = [], headers: OpenAPI.Header.Map? = nil, diff --git a/Sources/OpenAPIKit30/Content/ContentEncoding.swift b/Sources/OpenAPIKit30/Content/ContentEncoding.swift index 8d0596d6f6..d21c91acd4 100644 --- a/Sources/OpenAPIKit30/Content/ContentEncoding.swift +++ b/Sources/OpenAPIKit30/Content/ContentEncoding.swift @@ -14,7 +14,7 @@ extension OpenAPI.Content { public struct Encoding: Equatable, CodableVendorExtendable, Sendable { public typealias Style = OpenAPI.Parameter.SchemaContext.Style - public let contentType: OpenAPI.ContentType? + public let contentTypes: [OpenAPI.ContentType] public let headers: OpenAPI.Header.Map? public let style: Style public let explode: Bool @@ -27,14 +27,40 @@ extension OpenAPI.Content { /// where the values are anything codable. public var vendorExtensions: [String: AnyCodable] + /// Get the content type if only one is specified. + /// + /// - Important: This is **soft-deprecated**. Use the `contentTypes` + /// property instead. + public var contentType: OpenAPI.ContentType? { + guard contentTypes.count == 1 else { return nil } + return contentTypes.first + } + + /// - Important: This is **soft-deprecated**. Use the initializer + /// that takes a `contentTypes` array instead. + public init( + contentType: OpenAPI.ContentType?, + headers: OpenAPI.Header.Map? = nil, + style: Style = Self.defaultStyle, + allowReserved: Bool = false, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.contentTypes = contentType.map { [$0] } ?? [] + self.headers = headers + self.style = style + self.explode = style.defaultExplode + self.allowReserved = allowReserved + self.vendorExtensions = vendorExtensions + } + public init( - contentType: OpenAPI.ContentType? = nil, + contentTypes: [OpenAPI.ContentType] = [], headers: OpenAPI.Header.Map? = nil, style: Style = Self.defaultStyle, allowReserved: Bool = false, vendorExtensions: [String: AnyCodable] = [:] ) { - self.contentType = contentType + self.contentTypes = contentTypes self.headers = headers self.style = style self.explode = style.defaultExplode @@ -42,15 +68,33 @@ extension OpenAPI.Content { self.vendorExtensions = vendorExtensions } + /// - Important: This is **soft-deprecated**. Use the initializer + /// that takes a `contentTypes` array instead. public init( - contentType: OpenAPI.ContentType? = nil, + contentType: OpenAPI.ContentType?, headers: OpenAPI.Header.Map? = nil, style: Style = Self.defaultStyle, explode: Bool, allowReserved: Bool = false, vendorExtensions: [String: AnyCodable] = [:] ) { - self.contentType = contentType + self.contentTypes = contentType.map { [$0] } ?? [] + self.headers = headers + self.style = style + self.explode = explode + self.allowReserved = allowReserved + self.vendorExtensions = vendorExtensions + } + + public init( + contentTypes: [OpenAPI.ContentType] = [], + headers: OpenAPI.Header.Map? = nil, + style: Style = Self.defaultStyle, + explode: Bool, + allowReserved: Bool = false, + vendorExtensions: [String: AnyCodable] = [:] + ) { + self.contentTypes = contentTypes self.headers = headers self.style = style self.explode = explode @@ -67,7 +111,12 @@ extension OpenAPI.Content.Encoding: Encodable { public func encode(to encoder: Encoder) throws { var container = encoder.container(keyedBy: CodingKeys.self) - try container.encodeIfPresent(contentType, forKey: .contentType) + if contentTypes.count > 0 { + let contentTypesString = contentTypes + .map(\.rawValue) + .joined(separator: ", ") + try container.encode(contentTypesString, forKey: .contentType) + } try container.encodeIfPresent(headers, forKey: .headers) if style != Self.defaultStyle { @@ -90,7 +139,16 @@ extension OpenAPI.Content.Encoding: Decodable { public init(from decoder: Decoder) throws { let container = try decoder.container(keyedBy: CodingKeys.self) - contentType = try container.decodeIfPresent(OpenAPI.ContentType.self, forKey: .contentType) + let contentTypesString = try container.decodeIfPresent(String.self, forKey: .contentType) + if let contentTypesString { + contentTypes = contentTypesString + .split(separator: ",") + .compactMap { string in + OpenAPI.ContentType.init(rawValue: string.trimmingCharacters(in: .whitespaces)) + } + } else { + contentTypes = [] + } headers = try container.decodeIfPresent(OpenAPI.Header.Map.self, forKey: .headers) diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 506f70378d..eef874ece8 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -234,7 +234,7 @@ extension OpenAPIKit30.OpenAPI.Parameter.SchemaContext.Style: To31 { extension OpenAPIKit30.OpenAPI.Content.Encoding: To31 { fileprivate func to31() -> OpenAPIKit.OpenAPI.Content.Encoding { OpenAPIKit.OpenAPI.Content.Encoding( - contentTypes: [contentType].compactMap { $0 }, + contentTypes: contentTypes, headers: headers?.mapValues(eitherRefTo31), style: style.to31(), explode: explode, diff --git a/Tests/OpenAPIKit30Tests/Content/ContentTests.swift b/Tests/OpenAPIKit30Tests/Content/ContentTests.swift index 5f0fc1ce5e..6de4b68e1a 100644 --- a/Tests/OpenAPIKit30Tests/Content/ContentTests.swift +++ b/Tests/OpenAPIKit30Tests/Content/ContentTests.swift @@ -69,7 +69,7 @@ final class ContentTests: XCTestCase { example: nil, encoding: [ "hello": .init( - contentType: .json, + contentTypes: [.json], headers: [ "world": .init(OpenAPI.Header(schemaOrContent: .init(.header(.string)))) ], @@ -358,7 +358,7 @@ extension ContentTests { func test_encodingAndSchema_encode() { let content = OpenAPI.Content( schema: .init(.string), - encoding: ["json": .init(contentType: .json)] + encoding: ["json": .init(contentTypes: [.json])] ) let encodedContent = try! orderUnstableTestStringFromEncoding(of: content) @@ -400,7 +400,7 @@ extension ContentTests { content, OpenAPI.Content( schema: .init(.string), - encoding: ["json": .init(contentType: .json)] + encoding: ["json": .init(contentTypes: [.json])] ) ) } @@ -503,18 +503,18 @@ extension ContentTests { func test_encodingInit() { let _ = OpenAPI.Content.Encoding() - let _ = OpenAPI.Content.Encoding(contentType: .json) + let _ = OpenAPI.Content.Encoding(contentTypes: [.json]) let _ = OpenAPI.Content.Encoding(headers: ["special": .a(.external(URL(string: "hello.yml")!))]) let _ = OpenAPI.Content.Encoding(allowReserved: true) - let _ = OpenAPI.Content.Encoding(contentType: .form, + let _ = OpenAPI.Content.Encoding(contentTypes: [.form], headers: ["special": .a(.external(URL(string: "hello.yml")!))], allowReserved: true) - let _ = OpenAPI.Content.Encoding(contentType: .json, + let _ = OpenAPI.Content.Encoding(contentTypes: [.json], style: .form) - let _ = OpenAPI.Content.Encoding(contentType: .json, + let _ = OpenAPI.Content.Encoding(contentTypes: [.json], style: .form, explode: true) } @@ -545,7 +545,7 @@ extension ContentTests { } func test_encoding_contentType_encode() throws { - let encoding = OpenAPI.Content.Encoding(contentType: .csv) + let encoding = OpenAPI.Content.Encoding(contentTypes: [.csv]) let encodedEncoding = try! orderUnstableTestStringFromEncoding(of: encoding) @@ -568,7 +568,34 @@ extension ContentTests { """.data(using: .utf8)! let encoding = try! orderUnstableDecode(OpenAPI.Content.Encoding.self, from: encodingData) - XCTAssertEqual(encoding, OpenAPI.Content.Encoding(contentType: .csv)) + XCTAssertEqual(encoding, OpenAPI.Content.Encoding(contentTypes: [.csv])) + } + + func test_encoding_multiple_contentTypes_encode() throws { + let encoding = OpenAPI.Content.Encoding(contentTypes: [.csv, .json]) + + let encodedEncoding = try! orderUnstableTestStringFromEncoding(of: encoding) + + assertJSONEquivalent( + encodedEncoding, + """ + { + "contentType" : "text\\/csv, application\\/json" + } + """ + ) + } + + func test_encoding_multiple_contentTypes_decode() throws { + let encodingData = + """ + { + "contentType": "text/csv, application/json" + } + """.data(using: .utf8)! + let encoding = try! orderUnstableDecode(OpenAPI.Content.Encoding.self, from: encodingData) + + XCTAssertEqual(encoding, OpenAPI.Content.Encoding(contentTypes: [.csv, .json])) } func test_encoding_headers_encode() throws { @@ -711,7 +738,7 @@ extension ContentTests { func test_encoding_vendorExtensions_encode() throws { let encoding = OpenAPI.Content.Encoding( - contentType: .json, + contentTypes: [.json], vendorExtensions: [ "x-custom": "value", "x-nested": .init(["key": 123]) @@ -747,6 +774,7 @@ extension ContentTests { """.data(using: .utf8)! let encoding = try orderUnstableDecode(OpenAPI.Content.Encoding.self, from: encodingData) + XCTAssertEqual(encoding.contentTypes, [.json]) XCTAssertEqual(encoding.contentType, .json) XCTAssertEqual(encoding.vendorExtensions["x-custom"]?.value as? String, "value") XCTAssertEqual((encoding.vendorExtensions["x-nested"]?.value as? [String: Int])?["key"], 123) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index 071ec6c2f7..de11045515 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -841,6 +841,31 @@ final class DocumentConversionTests: XCTestCase { try assertEqualNewToOld(newParameter2, parameter2) } + func test_ContentEncoding() throws { + let encoding1 = OpenAPIKit30.OpenAPI.Content.Encoding( + contentTypes: [.json, .txt], headers: ["Content-Type": .header(.init(schema: .string))], + style: .form, allowReserved: false) + let encoding2 = OpenAPIKit30.OpenAPI.Content.Encoding(contentTypes: [.json], ) + + let content = OpenAPIKit30.OpenAPI.Content( + schema: .string, encoding: ["one": encoding1, "two": encoding2]) + + let oldDoc = OpenAPIKit30.OpenAPI.Document( + info: .init(title: "test", version: "1.0.0"), + servers: [], + paths: [ + "/root": .init( + summary: "path one", description: "the first path", servers: [], parameters: [], + get: .init(requestBody: .init(content: [.json: content]), responses: [:])) + ], + components: .noComponents + ) + + let newDoc = oldDoc.convert(to: .v3_2_0) + + try assertEqualNewToOld(newDoc, oldDoc) + } + // TODO: more tests } @@ -1302,7 +1327,7 @@ fileprivate func assertEqualNewToOld(_ newExample: OpenAPIKit.OpenAPI.Example, _ } fileprivate func assertEqualNewToOld(_ newEncoding: OpenAPIKit.OpenAPI.Content.Encoding, _ oldEncoding: OpenAPIKit30.OpenAPI.Content.Encoding) throws { - XCTAssertEqual(newEncoding.contentTypes.first, oldEncoding.contentType) + XCTAssertEqual(newEncoding.contentTypes, oldEncoding.contentTypes) if let newEncodingHeaders = newEncoding.headers { let oldEncodingHeaders = try XCTUnwrap(oldEncoding.headers) for ((newKey, newHeader), (oldKey, oldHeader)) in zip(newEncodingHeaders, oldEncodingHeaders) {