diff --git a/Sources/OpenAPIKit/CodableVendorExtendable.swift b/Sources/OpenAPIKit/CodableVendorExtendable.swift index 34c151bbfb..e17811231c 100644 --- a/Sources/OpenAPIKit/CodableVendorExtendable.swift +++ b/Sources/OpenAPIKit/CodableVendorExtendable.swift @@ -114,7 +114,8 @@ extension CodableVendorExtendable { throw GenericError( subjectName: "Vendor Extension", details: "Found at least one vendor extension property that does not begin with the required 'x-' prefix. Invalid properties: \(invalidKeysList)", - codingPath: decoder.codingPath + codingPath: decoder.codingPath, + pathIncludesSubject: false ) } diff --git a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift index 441ba805f4..8392277dbf 100644 --- a/Sources/OpenAPIKit/Components Object/Components+Locatable.swift +++ b/Sources/OpenAPIKit/Components Object/Components+Locatable.swift @@ -64,6 +64,11 @@ extension OpenAPI.Link: ComponentDictionaryLocatable { public static var openAPIComponentsKeyPath: Either>, WritableKeyPath>> { .b(\.links) } } +extension OpenAPI.Content: ComponentDictionaryLocatable { + public static var openAPIComponentsKey: String { "mediaTypes" } + public static var openAPIComponentsKeyPath: Either>, WritableKeyPath>> { .b(\.mediaTypes) } +} + extension OpenAPI.PathItem: ComponentDictionaryLocatable { public static var openAPIComponentsKey: String { "pathItems" } public static var openAPIComponentsKeyPath: Either>, WritableKeyPath>> { .a(\.pathItems) } diff --git a/Sources/OpenAPIKit/Components Object/Components.swift b/Sources/OpenAPIKit/Components Object/Components.swift index 035644c430..49c28636fa 100644 --- a/Sources/OpenAPIKit/Components Object/Components.swift +++ b/Sources/OpenAPIKit/Components Object/Components.swift @@ -45,7 +45,7 @@ extension OpenAPI { /// "my_param": .reference(.component(named: "my_direct_param")) /// ] /// ) - public struct Components: Equatable, CodableVendorExtendable, Sendable { + public struct Components: HasConditionalWarnings, CodableVendorExtendable, Sendable { public var schemas: ComponentDictionary public var responses: ComponentReferenceDictionary @@ -56,6 +56,8 @@ extension OpenAPI { public var securitySchemes: ComponentReferenceDictionary public var links: ComponentReferenceDictionary public var callbacks: ComponentReferenceDictionary + /// Media Type Objects (aka `OpenAPI.Content`) + public var mediaTypes: ComponentReferenceDictionary public var pathItems: ComponentDictionary @@ -66,6 +68,8 @@ extension OpenAPI { /// where the values are anything codable. public var vendorExtensions: [String: AnyCodable] + public let conditionalWarnings: [(any Condition, OpenAPI.Warning)] + public init( schemas: ComponentDictionary = [:], responses: ComponentReferenceDictionary = [:], @@ -76,6 +80,7 @@ extension OpenAPI { securitySchemes: ComponentReferenceDictionary = [:], links: ComponentReferenceDictionary = [:], callbacks: ComponentReferenceDictionary = [:], + mediaTypes: ComponentReferenceDictionary = [:], pathItems: ComponentDictionary = [:], vendorExtensions: [String: AnyCodable] = [:] ) { @@ -88,8 +93,13 @@ extension OpenAPI { self.securitySchemes = securitySchemes self.links = links self.callbacks = callbacks + self.mediaTypes = mediaTypes self.pathItems = pathItems self.vendorExtensions = vendorExtensions + + self.conditionalWarnings = [ + nonEmptyVersionWarning(fieldName: "mediaTypes", value: mediaTypes, minimumVersion: .v3_2_0) + ].compactMap { $0 } } /// Construct components as "direct" entries (no references). When @@ -105,6 +115,7 @@ extension OpenAPI { securitySchemes: ComponentDictionary = [:], links: ComponentDictionary = [:], callbacks: ComponentDictionary = [:], + mediaTypes: ComponentDictionary = [:], pathItems: ComponentDictionary = [:], vendorExtensions: [String: AnyCodable] = [:] ) -> Self { @@ -118,6 +129,7 @@ extension OpenAPI { securitySchemes: securitySchemes.mapValues { .b($0) }, links: links.mapValues { .b($0) }, callbacks: callbacks.mapValues { .b($0) }, + mediaTypes: mediaTypes.mapValues { .b($0) }, pathItems: pathItems, vendorExtensions: vendorExtensions ) @@ -132,6 +144,32 @@ extension OpenAPI { } } +extension OpenAPI.Components: Equatable { + public static func == (lhs: Self, rhs: Self) -> Bool { + lhs.schemas == rhs.schemas + && lhs.responses == rhs.responses + && lhs.parameters == rhs.parameters + && lhs.examples == rhs.examples + && lhs.requestBodies == rhs.requestBodies + && lhs.headers == rhs.headers + && lhs.securitySchemes == rhs.securitySchemes + && lhs.links == rhs.links + && lhs.callbacks == rhs.callbacks + && lhs.mediaTypes == rhs.mediaTypes + && lhs.pathItems == rhs.pathItems + && lhs.vendorExtensions == rhs.vendorExtensions + } +} + +fileprivate func nonEmptyVersionWarning(fieldName: String, value: any Collection, minimumVersion: OpenAPI.Document.Version) -> (any Condition, OpenAPI.Warning)? { + if value.isEmpty { return nil } + + return OpenAPI.Document.ConditionalWarnings.version( + lessThan: minimumVersion, + doesNotSupport: "The Components \(fieldName) map" + ) +} + extension OpenAPI { public typealias ComponentDictionary = OrderedDictionary @@ -170,6 +208,7 @@ extension OpenAPI.Components { try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: detectCollision(type: "securitySchemes")) try links.merge(other.links, uniquingKeysWith: detectCollision(type: "links")) try callbacks.merge(other.callbacks, uniquingKeysWith: detectCollision(type: "callbacks")) + try mediaTypes.merge(other.mediaTypes, uniquingKeysWith: detectCollision(type: "mediaTypes")) try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems")) try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions")) } @@ -185,6 +224,7 @@ extension OpenAPI.Components { securitySchemes.sortKeys() links.sortKeys() callbacks.sortKeys() + mediaTypes.sortKeys() pathItems.sortKeys() } } @@ -237,6 +277,10 @@ extension OpenAPI.Components: Encodable { if !callbacks.isEmpty { try container.encode(callbacks, forKey: .callbacks) } + + if !mediaTypes.isEmpty { + try container.encode(mediaTypes, forKey: .mediaTypes) + } if !pathItems.isEmpty { try container.encode(pathItems, forKey: .pathItems) @@ -276,10 +320,16 @@ extension OpenAPI.Components: Decodable { links = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary.self, forKey: .links) ?? [:] callbacks = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary.self, forKey: .callbacks) ?? [:] + + mediaTypes = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary.self, forKey: .mediaTypes) ?? [:] pathItems = try container.decodeIfPresent(OpenAPI.ComponentDictionary.self, forKey: .pathItems) ?? [:] vendorExtensions = try Self.extensions(from: decoder) + + conditionalWarnings = [ + nonEmptyVersionWarning(fieldName: "mediaTypes", value: mediaTypes, minimumVersion: .v3_2_0) + ].compactMap { $0 } } catch let error as EitherDecodeNoTypesMatchedError { if let underlyingError = OpenAPI.Error.Decoding.Document.eitherBranchToDigInto(error) { throw (underlyingError.underlyingError ?? underlyingError) @@ -310,6 +360,7 @@ extension OpenAPI.Components { case securitySchemes case links case callbacks + case mediaTypes case pathItems case extended(String) @@ -325,6 +376,7 @@ extension OpenAPI.Components { .securitySchemes, .links, .callbacks, + .mediaTypes, .pathItems ] } @@ -353,6 +405,8 @@ extension OpenAPI.Components { self = .links case "callbacks": self = .callbacks + case "mediaTypes": + self = .mediaTypes case "pathItems": self = .pathItems default: @@ -380,6 +434,8 @@ extension OpenAPI.Components { return "links" case .callbacks: return "callbacks" + case .mediaTypes: + return "mediaTypes" case .pathItems: return "pathItems" case .extended(let key): @@ -409,6 +465,7 @@ extension OpenAPI.Components { let oldSecuritySchemes = securitySchemes let oldLinks = links let oldCallbacks = callbacks + let oldMediaTypes = mediaTypes let oldPathItems = pathItems async let (newSchemas, c1, m1) = oldSchemas.externallyDereferenced(with: loader) @@ -420,7 +477,8 @@ extension OpenAPI.Components { async let (newSecuritySchemes, c7, m7) = oldSecuritySchemes.externallyDereferenced(with: loader) // async let (newLinks, c8, m8) = oldLinks.externallyDereferenced(with: loader) // async let (newCallbacks, c9, m9) = oldCallbacks.externallyDereferenced(with: loader) - async let (newPathItems, c10, m10) = oldPathItems.externallyDereferenced(with: loader) + async let (newMediaTypes, c10, m10) = oldMediaTypes.externallyDereferenced(with: loader) + async let (newPathItems, c11, m11) = oldPathItems.externallyDereferenced(with: loader) schemas = try await newSchemas responses = try await newResponses @@ -431,6 +489,7 @@ extension OpenAPI.Components { securitySchemes = try await newSecuritySchemes // links = try await newLinks // callbacks = try await newCallbacks + mediaTypes = try await newMediaTypes pathItems = try await newPathItems let c1Resolved = try await c1 @@ -443,6 +502,7 @@ extension OpenAPI.Components { // let c8Resolved = try await c8 // let c9Resolved = try await c9 let c10Resolved = try await c10 + let c11Resolved = try await c11 // For Swift 5.10+ we can delete the following links and callbacks code and uncomment the // preferred code above. @@ -464,8 +524,9 @@ extension OpenAPI.Components { && c8Resolved.isEmpty && c9Resolved.isEmpty && c10Resolved.isEmpty + && c11Resolved.isEmpty - let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + m11 if noNewComponents { return newMessages } @@ -479,6 +540,7 @@ extension OpenAPI.Components { try merge(c8Resolved) try merge(c9Resolved) try merge(c10Resolved) + try merge(c11Resolved) switch depth { case .iterations(let number): diff --git a/Sources/OpenAPIKit/Content/Content.swift b/Sources/OpenAPIKit/Content/Content.swift index d394250b6f..ac87d8f1b6 100644 --- a/Sources/OpenAPIKit/Content/Content.swift +++ b/Sources/OpenAPIKit/Content/Content.swift @@ -319,7 +319,15 @@ extension OpenAPI.Content { } extension OpenAPI.Content { - public typealias Map = OrderedDictionary + public typealias Map = OrderedDictionary, OpenAPI.Content>> +} + +extension OpenAPI.Content.Map { + /// Construct an OpenAPI.Content.Map for which none of the values are + /// references (all values are OpenAPI.Content). + public static func direct(_ map: OrderedDictionary) -> OpenAPI.Content.Map { + map.mapValues { .b($0) } + } } extension OpenAPI.Content { diff --git a/Sources/OpenAPIKit/Content/DereferencedContent.swift b/Sources/OpenAPIKit/Content/DereferencedContent.swift index d122422e51..9a0016298e 100644 --- a/Sources/OpenAPIKit/Content/DereferencedContent.swift +++ b/Sources/OpenAPIKit/Content/DereferencedContent.swift @@ -37,7 +37,8 @@ public struct DereferencedContent: Equatable { internal init( _ content: OpenAPI.Content, resolvingIn components: OpenAPI.Components, - following references: Set + following references: Set, + dereferencedFromComponentNamed name: String? ) throws { self.schema = try content.schema?._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) self.itemSchema = try content.itemSchema?._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil) @@ -67,6 +68,11 @@ public struct DereferencedContent: Equatable { self.encoding = nil } + var content = content + if let name { + content.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name) + } + self.underlyingContent = content } @@ -85,7 +91,7 @@ extension OpenAPI.Content: LocallyDereferenceable { following references: Set, dereferencedFromComponentNamed name: String? ) throws -> DereferencedContent { - return try DereferencedContent(self, resolvingIn: components, following: references) + return try DereferencedContent(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name) } } diff --git a/Sources/OpenAPIKit/Either/Either+Convenience.swift b/Sources/OpenAPIKit/Either/Either+Convenience.swift index 4fb851c6fd..dade06100e 100644 --- a/Sources/OpenAPIKit/Either/Either+Convenience.swift +++ b/Sources/OpenAPIKit/Either/Either+Convenience.swift @@ -103,6 +103,11 @@ extension Either where B == OpenAPI.Link { public var linkValue: B? { b } } +extension Either where B == OpenAPI.Content { + /// Retrieve the content if that is what this property contains. + public var contentValue: B? { b } +} + extension Either where B == OpenAPI.Content.Map { /// Retrieve the content map if that is what this property contains. public var contentValue: B? { b } @@ -221,6 +226,11 @@ extension Either where B == OpenAPI.Parameter { public static func parameter(_ parameter: OpenAPI.Parameter) -> Self { .b(parameter) } } +extension Either where B == OpenAPI.Content { + /// Construct content. + public static func content(_ content: OpenAPI.Content) -> Self { .b(content) } +} + extension Either where B == OpenAPI.Content.Map { /// Construct a content map. public static func content(_ map: OpenAPI.Content.Map) -> Self { .b(map) } diff --git a/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift b/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift index 48259de940..c23ad3af8e 100644 --- a/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift +++ b/Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift @@ -70,6 +70,11 @@ extension OpenAPI.Error.Decoding.Response { internal init(_ error: GenericError) { var codingPath = Self.relativePath(from: error.codingPath) + + if error.pathIncludesSubject { + codingPath = codingPath.dropLast() + } + let code = codingPath.removeFirst().stringValue.lowercased() // this part of the coding path is structurally guaranteed to be a status code diff --git a/Sources/OpenAPIKit/Header/DereferencedHeader.swift b/Sources/OpenAPIKit/Header/DereferencedHeader.swift index 40509234a5..0972264e38 100644 --- a/Sources/OpenAPIKit/Header/DereferencedHeader.swift +++ b/Sources/OpenAPIKit/Header/DereferencedHeader.swift @@ -47,10 +47,10 @@ public struct DereferencedHeader: Equatable { case .b(let contentMap): self.schemaOrContent = .b( try contentMap.mapValues { - try DereferencedContent( - $0, - resolvingIn: components, - following: references + try $0._dereferenced( + in: components, + following: references, + dereferencedFromComponentNamed: nil ) } ) diff --git a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift index 23c079e82a..963eb7487f 100644 --- a/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift +++ b/Sources/OpenAPIKit/Parameter/DereferencedParameter.swift @@ -49,10 +49,10 @@ public struct DereferencedParameter: Equatable { case .b(let contentMap): self.schemaOrContent = .b( try contentMap.mapValues { - try DereferencedContent( - $0, - resolvingIn: components, - following: references + try $0._dereferenced( + in: components, + following: references, + dereferencedFromComponentNamed: nil ) } ) diff --git a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift index b340f8557d..2967daee72 100644 --- a/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift +++ b/Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift @@ -1173,11 +1173,13 @@ extension JSONSchema.IntegerContext: Decodable { let value = try intAttempt ?? doubleAttempt.map { floatVal in guard let integer = Int(exactly: floatVal) else { + let key = max ? CodingKeys.maximum : CodingKeys.minimum + let subject = key.rawValue throw GenericError( - subjectName: max ? "maximum" : "minimum", + subjectName: subject, details: "Expected an Integer literal but found a floating point value (\(String(describing: floatVal)))", - codingPath: decoder.codingPath, - pathIncludesSubject: false + codingPath: decoder.codingPath + [key], + pathIncludesSubject: true ) } return integer diff --git a/Sources/OpenAPIKitCompat/Compat30To31.swift b/Sources/OpenAPIKitCompat/Compat30To31.swift index 19bda09613..91e8b257f1 100644 --- a/Sources/OpenAPIKitCompat/Compat30To31.swift +++ b/Sources/OpenAPIKitCompat/Compat30To31.swift @@ -116,7 +116,7 @@ extension Either where A == OpenAPIKit30.OpenAPI.Parameter.SchemaContext, B == O case .a(let context): .a(context.to31()) case .b(let contentMap): - .b(contentMap.mapValues { $0.to31() }) + .b(contentMap.mapValues { .b($0.to31()) }) } } } @@ -328,7 +328,7 @@ extension OpenAPIKit30.OpenAPI.Response: To31 { OpenAPIKit.OpenAPI.Response( description: description, headers: headers?.mapValues(eitherRefTo31), - content: content.mapValues { $0.to31() }, + content: content.mapValues { .b($0.to31()) }, links: links.mapValues(eitherRefTo31), vendorExtensions: vendorExtensions ) @@ -339,7 +339,7 @@ extension OpenAPIKit30.OpenAPI.Request: To31 { fileprivate func to31() -> OpenAPIKit.OpenAPI.Request { OpenAPIKit.OpenAPI.Request( description: description, - content: content.mapValues { $0.to31() }, + content: content.mapValues { .b($0.to31()) }, required: `required`, vendorExtensions: vendorExtensions ) diff --git a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift index b35a4e0648..01b7fd6ae0 100644 --- a/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift +++ b/Tests/OpenAPIKitCompatTests/DocumentConversionTests.swift @@ -1086,6 +1086,10 @@ fileprivate func assertEqualNewToOld(_ newRequest: OpenAPIKit.OpenAPI.Request, _ fileprivate func assertEqualNewToOld(_ newContentMap: OpenAPIKit.OpenAPI.Content.Map, _ oldContentMap: OpenAPIKit30.OpenAPI.Content.Map) throws { for ((newCt, newContent), (oldCt, oldContent)) in zip(newContentMap, oldContentMap) { XCTAssertEqual(newCt, oldCt) + guard case let .b(newContent) = newContent else { + XCTFail("Expected new content not to be a reference but found a reference: \(newContent)") + return + } switch (newContent.schema?.value, oldContent.schema) { case (.reference(let ref1, _), .a(let ref2)): XCTAssertEqual(ref1.absoluteString, ref2.absoluteString) diff --git a/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift index 7771fe7a6d..c5e2518181 100644 --- a/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/RequestContentMapErrorTests.swift @@ -11,46 +11,6 @@ import OpenAPIKit @preconcurrency import Yams final class RequestContentMapErrorTests: XCTestCase { - /** - - The "wrong type content map key" does not fail right now because OrderedDictionary does not - throw for keys it cannot decode. This is a shortcoming of OrderedDictionary that should be solved - in a future release. - - */ - -// func test_wrongTypeContentMapKey() { -// let documentYML = -//""" -//openapi: "3.1.0" -//info: -// title: test -// version: 1.0 -//paths: -// /hello/world: -// get: -// requestBody: -// content: -// blablabla: -// schema: -// $ref: #/components/schemas/one -// responses: {} -//""" -// -// XCTAssertThrowsError(try testDecoder.decode(OpenAPI.Document.self, from: documentYML)) { error in -// -// let openAPIError = OpenAPI.Error(from: error) -// -// XCTAssertEqual(openAPIError.localizedDescription, "Expected to find either a $ref or a Request in .requestBody for the **GET** endpoint under `/hello/world` but found neither. \n\nJSONReference could not be decoded because:\nExpected `requestBody` value in .paths./hello/world.get to be parsable as Mapping but it was not.\n\nRequest could not be decoded because:\nExpected `requestBody` value in .paths./hello/world.get to be parsable as Mapping but it was not..") -// XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [ -// "paths", -// "/hello/world", -// "get", -// "requestBody" -// ]) -// } -// } - func test_wrongTypeContentValue() { let documentYML = """ @@ -71,7 +31,7 @@ final class RequestContentMapErrorTests: XCTestCase { let openAPIError = OpenAPI.Error(from: error) - XCTAssertEqual(openAPIError.localizedDescription, "Expected `application/json` value in .content for the request body of the **GET** endpoint under `/hello/world` to be parsable as Mapping but it was not.") + XCTAssertEqual(openAPIError.localizedDescription, "Found neither a $ref nor a Content in .content['application/json'] for the request body of the **GET** endpoint under `/hello/world`. \n\nReference could not be decoded because:\nExpected value to be parsable as Mapping but it was not.\n\nContent could not be decoded because:\nExpected value to be parsable as Mapping but it was not..") XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [ "paths", "/hello/world", @@ -107,7 +67,7 @@ final class RequestContentMapErrorTests: XCTestCase { let openAPIError = OpenAPI.Error(from: error) - XCTAssertEqual(openAPIError.localizedDescription, "Problem encountered when parsing `Vendor Extension` in .content['application/json'] for the request body of the **GET** endpoint under `/hello/world`: Found at least one vendor extension property that does not begin with the required 'x-' prefix. Invalid properties: [ invalid ].") + XCTAssertEqual(openAPIError.localizedDescription, "Found neither a $ref nor a Content in .content['application/json'] for the request body of the **GET** endpoint under `/hello/world`. \n\nContent could not be decoded because:\nProblem encountered when parsing `Vendor Extension`: Found at least one vendor extension property that does not begin with the required 'x-' prefix. Invalid properties: [ invalid ]..") XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [ "paths", "/hello/world", diff --git a/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift b/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift index a8f913f354..572efec6fa 100644 --- a/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift +++ b/Tests/OpenAPIKitErrorReportingTests/SchemaErrorTests.swift @@ -44,7 +44,8 @@ final class SchemaErrorTests: XCTestCase { "200", "content", "application/json", - "schema" + "schema", + "maximum" ]) } } diff --git a/Tests/OpenAPIKitTests/ComponentsTests.swift b/Tests/OpenAPIKitTests/ComponentsTests.swift index 0d15cca4c1..31ae5e77e6 100644 --- a/Tests/OpenAPIKitTests/ComponentsTests.swift +++ b/Tests/OpenAPIKitTests/ComponentsTests.swift @@ -67,8 +67,11 @@ final class ComponentsTests: XCTestCase { ) ]) ], + mediaTypes: [ + "ten": .content(.init(schema: .string)) + ], pathItems: [ - "ten": .init(get: .init(responses: [200: .response(description: "response")])) + "eleven": .init(get: .init(responses: [200: .response(description: "response")])) ], vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) @@ -114,8 +117,11 @@ final class ComponentsTests: XCTestCase { ) ] ], + mediaTypes: [ + "ten": .init(schema: .string) + ], pathItems: [ - "ten": .init(get: .init(responses: [200: .response(description: "response")])) + "eleven": .init(get: .init(responses: [200: .response(description: "response")])) ], vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) @@ -246,8 +252,11 @@ final class ComponentsTests: XCTestCase { OpenAPI.CallbackURL(rawValue: "{$url}")!: .pathItem(.init(post: .init(responses: [:]))) ] ], + mediaTypes: [ + "ten": .init(schema: .string) + ], pathItems: [ - "ten": .init(get: .init(responses: [:])) + "eleven": .init(get: .init(responses: [:])) ] ) @@ -260,7 +269,8 @@ final class ComponentsTests: XCTestCase { let ref7 = try components.reference(named: "seven", ofType: OpenAPI.SecurityScheme.self) let ref8 = try components.reference(named: "eight", ofType: OpenAPI.Link.self) let ref9 = try components.reference(named: "nine", ofType: OpenAPI.Callbacks.self) - let ref10 = try components.reference(named: "ten", ofType: OpenAPI.PathItem.self) + let ref10 = try components.reference(named: "ten", ofType: OpenAPI.Content.self) + let ref11 = try components.reference(named: "eleven", ofType: OpenAPI.PathItem.self) XCTAssertEqual(components[ref1], .string) XCTAssertEqual(components[ref2], .init(description: "hello", content: [:])) @@ -276,7 +286,8 @@ final class ComponentsTests: XCTestCase { OpenAPI.CallbackURL(rawValue: "{$url}")!: .pathItem(.init(post: .init(responses: [:]))) ] ) - XCTAssertEqual(components[ref10], .init(get: .init(responses: [:]))) + XCTAssertEqual(components[ref10], .init(schema: .string)) + XCTAssertEqual(components[ref11], .init(get: .init(responses: [:]))) } func test_subscriptLookup() throws { @@ -502,8 +513,11 @@ extension ComponentsTests { ) ] ], + mediaTypes: [ + "ten": .init(schema: .string) + ], pathItems: [ - "ten": .init(get: .init(responses: [200: .response(description: "response")])) + "eleven": .init(get: .init(responses: [200: .response(description: "response")])) ], vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) @@ -544,6 +558,13 @@ extension ComponentsTests { "operationId" : "op1" } }, + "mediaTypes" : { + "ten" : { + "schema" : { + "type" : "string" + } + } + }, "parameters" : { "three" : { "content" : { @@ -554,7 +575,7 @@ extension ComponentsTests { } }, "pathItems" : { - "ten" : { + "eleven" : { "get" : { "responses" : { "200" : { @@ -630,6 +651,13 @@ extension ComponentsTests { "operationId" : "op1" } }, + "mediaTypes" : { + "ten" : { + "schema" : { + "type" : "string" + } + } + }, "parameters" : { "three" : { "content" : { @@ -640,7 +668,7 @@ extension ComponentsTests { } }, "pathItems" : { - "ten" : { + "eleven" : { "get" : { "responses" : { "200" : { @@ -724,8 +752,11 @@ extension ComponentsTests { ) ] ], + mediaTypes: [ + "ten": .init(schema: .string) + ], pathItems: [ - "ten": .init(get: .init(responses: [200: .response(description: "response")])) + "eleven": .init(get: .init(responses: [200: .response(description: "response")])) ], vendorExtensions: ["x-specialFeature": .init(["hello", "world"])] ) diff --git a/Tests/OpenAPIKitTests/Content/ContentTests.swift b/Tests/OpenAPIKitTests/Content/ContentTests.swift index 7a5a3d197d..e1cdca2ef4 100644 --- a/Tests/OpenAPIKitTests/Content/ContentTests.swift +++ b/Tests/OpenAPIKitTests/Content/ContentTests.swift @@ -163,7 +163,7 @@ final class ContentTests: XCTestCase { } func test_contentMap() { - let _: OpenAPI.Content.Map = [ + let _: OpenAPI.Content.Map = .direct([ .bmp: .init(schema: .init(.string(contentEncoding: .binary))), .css: .init(schema: .init(.string)), .csv: .init(schema: .init(.string)), @@ -197,7 +197,7 @@ final class ContentTests: XCTestCase { .anyVideo: .init(schema: .string(contentEncoding: .binary)), .any: .init(schema: .string(contentEncoding: .binary)) - ] + ]) } } diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 1934c46bf4..c515bac83d 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -258,7 +258,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [] ) ) @@ -280,7 +280,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [] ) ) @@ -302,7 +302,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [s1, s2], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [] ) ) @@ -324,7 +324,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [s1, s2] ) ) @@ -346,7 +346,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [s1, s2], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [s1, s2] ) ) @@ -369,7 +369,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [s2], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [s3] ) ) @@ -393,7 +393,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [s2, s4], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [s3] ) ) @@ -417,7 +417,7 @@ final class DocumentTests: XCTestCase { "/hello/world": .init( servers: [s2, s4], get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])], + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])], servers: [s3] ) ) @@ -438,7 +438,7 @@ final class DocumentTests: XCTestCase { paths: [ "/hello/world": .init( get: .init( - responses: [.default: .response(description: "test", content: [.json: .init(schema: .string)])] + responses: [.default: .response(description: "test", content: [.json: .content(.init(schema: .string))])] ) ) ], diff --git a/Tests/OpenAPIKitTests/EaseOfUseTests.swift b/Tests/OpenAPIKitTests/EaseOfUseTests.swift index 917d065441..ee476324ce 100644 --- a/Tests/OpenAPIKitTests/EaseOfUseTests.swift +++ b/Tests/OpenAPIKitTests/EaseOfUseTests.swift @@ -65,13 +65,16 @@ final class DeclarativeEaseOfUseTests: XCTestCase { 200: .response( description: "Successful Retrieve", content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] - ), - example: #"{ "hello": "world" }"# + .json: .content( + .init( + schema: + .object( + properties: [ + "hello": .string + ] + ), + example: #"{ "hello": "world" }"# + ) ) ] ) @@ -84,11 +87,13 @@ final class DeclarativeEaseOfUseTests: XCTestCase { parameters: [], requestBody: .init( content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] + .json: .content( + .init( + schema: .object( + properties: [ + "hello": .string + ] + ) ) ) ] @@ -97,11 +102,13 @@ final class DeclarativeEaseOfUseTests: XCTestCase { 202: .response( description: "Successful Create", content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] + .json: .content( + .init( + schema: .object( + properties: [ + "hello": .string + ] + ) ) ) ] @@ -185,13 +192,15 @@ final class DeclarativeEaseOfUseTests: XCTestCase { 200: .response( description: "Successful Retrieve", content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] - ), - example: #"{ "hello": "world" }"# + .json: .content( + .init( + schema: .object( + properties: [ + "hello": .string + ] + ), + example: #"{ "hello": "world" }"# + ) ) ] ) @@ -205,11 +214,13 @@ final class DeclarativeEaseOfUseTests: XCTestCase { parameters: [], requestBody: .init( content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] + .json: .content( + .init( + schema: .object( + properties: [ + "hello": .string + ] + ) ) ) ] @@ -218,11 +229,13 @@ final class DeclarativeEaseOfUseTests: XCTestCase { 202: .response( description: "Successful Create", content: [ - .json: .init( - schema: .object( - properties: [ - "hello": .string - ] + .json: .content( + .init( + schema: .object( + properties: [ + "hello": .string + ] + ) ) ) ] @@ -404,7 +417,7 @@ final class DeclarativeEaseOfUseTests: XCTestCase { let successfulHelloResponse = OpenAPI.Response( description: "Hello", content: [ - .txt: .init(schemaReference: try components.reference(named: "hello_string", ofType: JSONSchema.self)) + .txt: .content(.init(schemaReference: try components.reference(named: "hello_string", ofType: JSONSchema.self))) ] ) @@ -463,7 +476,7 @@ final class DeclarativeEaseOfUseTests: XCTestCase { let endpoint = document.paths["/widgets/{id}"]?.pathItemValue?.get let response = endpoint?.responses[status: 200]?.responseValue - let responseSchemaReference = response?.content[.json]?.schema + let responseSchemaReference = response?.content[.json]?.contentValue?.schema // this response schema is a reference found in the Components Object. We dereference // it to get at the schema. let responseSchema = responseSchemaReference.flatMap { document.components[$0] } @@ -476,7 +489,7 @@ final class DeclarativeEaseOfUseTests: XCTestCase { let endpoint = document.paths["/widgets/{id}"]?.pathItemValue?.post let request = endpoint?.requestBody?.requestValue - let requestSchemaReference = request?.content[.json]?.schema + let requestSchemaReference = request?.content[.json]?.contentValue?.schema // this request schema is defined inline but dereferencing still produces the schema // (dereferencing is just a no-op in this case). let requestSchema = requestSchemaReference.flatMap { document.components[$0] } @@ -531,7 +544,7 @@ fileprivate let testDocument = OpenAPI.Document( 200: .response( description: "A single widget", content: [ - .json: .init(schemaReference: .component(named: "testWidgetSchema")) + .json: .content(.init(schemaReference: .component(named: "testWidgetSchema"))) ] ) ] @@ -542,11 +555,13 @@ fileprivate let testDocument = OpenAPI.Document( description: "Create a new widget by adding a description. The created widget will be returned in the response body including a new part number.", requestBody: OpenAPI.Request( content: [ - .json: .init( - schema: JSONSchema.object( - properties: [ - "description": .string - ] + .json: .content( + .init( + schema: JSONSchema.object( + properties: [ + "description": .string + ] + ) ) ) ] @@ -555,7 +570,7 @@ fileprivate let testDocument = OpenAPI.Document( 201: .response( description: "The newly created widget", content: [ - .json: .init(schemaReference: .component(named: "testWidgetSchema")) + .json: .content(.init(schemaReference: .component(named: "testWidgetSchema"))) ] ) ] @@ -568,7 +583,7 @@ fileprivate let testDocument = OpenAPI.Document( 200: .response( description: "Get documentation on this API.", content: [ - .html: .init(schema: .string) + .html: .content(.init(schema: .string)) ] ) ] diff --git a/Tests/OpenAPIKitTests/Header/DereferencedHeaderTests.swift b/Tests/OpenAPIKitTests/Header/DereferencedHeaderTests.swift index a1d9424c90..8d153bf4bc 100644 --- a/Tests/OpenAPIKitTests/Header/DereferencedHeaderTests.swift +++ b/Tests/OpenAPIKitTests/Header/DereferencedHeaderTests.swift @@ -22,7 +22,7 @@ final class DereferencedHeaderTests: XCTestCase { func test_inlineContentHeader() throws { let t1 = try OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ).dereferenced(in: .noComponents) @@ -52,7 +52,7 @@ final class DereferencedHeaderTests: XCTestCase { ] ) let t1 = try OpenAPI.Header( - content: [.json: .init(schemaReference: .component(named: "test"))] + content: [.json: .content(.init(schemaReference: .component(named: "test")))] ).dereferenced(in: components) XCTAssertEqual( diff --git a/Tests/OpenAPIKitTests/Header/HeaderTests.swift b/Tests/OpenAPIKitTests/Header/HeaderTests.swift index d54ee70f72..304344b124 100644 --- a/Tests/OpenAPIKitTests/Header/HeaderTests.swift +++ b/Tests/OpenAPIKitTests/Header/HeaderTests.swift @@ -11,7 +11,7 @@ import OpenAPIKit final class HeaderTests: XCTestCase { func test_init() { let contentMap: OpenAPI.Content.Map = [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] let t1 = OpenAPI.Header(schemaOrContent: .init(contentMap)) @@ -64,7 +64,7 @@ final class HeaderTests: XCTestCase { extension HeaderTests { func test_header_contentMap_encode() throws { let header = OpenAPI.Header(content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ]) let headerEncoding = try orderUnstableTestStringFromEncoding(of: header) @@ -103,7 +103,7 @@ extension HeaderTests { XCTAssertEqual( header, OpenAPI.Header(content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ]) ) } @@ -217,7 +217,7 @@ extension HeaderTests { func test_header_required_encode() throws { let header = OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], required: true ) @@ -261,7 +261,7 @@ extension HeaderTests { header, OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], required: true ) @@ -271,7 +271,7 @@ extension HeaderTests { func test_header_withDescription_encode() throws { let header = OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], description: "hello" ) @@ -315,7 +315,7 @@ extension HeaderTests { header, OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], description: "hello" ) @@ -325,7 +325,7 @@ extension HeaderTests { func test_header_deprecated_encode() throws { let header = OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], deprecated: true ) @@ -369,7 +369,7 @@ extension HeaderTests { header, OpenAPI.Header( content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], deprecated: true ) diff --git a/Tests/OpenAPIKitTests/Operation/DereferencedOperationTests.swift b/Tests/OpenAPIKitTests/Operation/DereferencedOperationTests.swift index cc4cbc2c9e..c4552ae196 100644 --- a/Tests/OpenAPIKitTests/Operation/DereferencedOperationTests.swift +++ b/Tests/OpenAPIKitTests/Operation/DereferencedOperationTests.swift @@ -28,13 +28,13 @@ final class DereferencedOperationTests: XCTestCase { context: .header(schema: .string) ) ], - requestBody: OpenAPI.Request(content: [.json: .init(schema: .string)]), + requestBody: OpenAPI.Request(content: [.json: .content(.init(schema: .string))]), responses: [ 200: .response(description: "test") ] ).dereferenced(in: .noComponents) XCTAssertEqual(t1.parameters.count, 1) - XCTAssertEqual(t1.requestBody?.underlyingRequest, OpenAPI.Request(content: [.json: .init(schema: .string)])) + XCTAssertEqual(t1.requestBody?.underlyingRequest, OpenAPI.Request(content: [.json: .content(.init(schema: .string))])) XCTAssertEqual(t1.responses.count, 1) XCTAssertEqual(t1.responseOutcomes.first?.response, t1.responses[status: 200]) XCTAssertEqual(t1.responseOutcomes.first?.status, 200) @@ -79,7 +79,7 @@ final class DereferencedOperationTests: XCTestCase { func test_requestReference() throws { let components = OpenAPI.Components.direct( requestBodies: [ - "test": OpenAPI.Request(content: [.json: .init(schema: .string)]) + "test": OpenAPI.Request(content: [.json: .content(.init(schema: .string))]) ] ) let t1 = try OpenAPI.Operation( @@ -91,7 +91,7 @@ final class DereferencedOperationTests: XCTestCase { XCTAssertEqual( t1.requestBody?.underlyingRequest, OpenAPI.Request( - content: [.json: .init(schema: .string)], + content: [.json: .content(.init(schema: .string))], vendorExtensions: ["x-component-name": "test"] ) ) diff --git a/Tests/OpenAPIKitTests/Operation/OperationTests.swift b/Tests/OpenAPIKitTests/Operation/OperationTests.swift index 6cd1756526..912b134518 100644 --- a/Tests/OpenAPIKitTests/Operation/OperationTests.swift +++ b/Tests/OpenAPIKitTests/Operation/OperationTests.swift @@ -116,7 +116,7 @@ extension OperationTests { parameters: [ .reference(.component(named: "hello")) ], - requestBody: .init(content: [.json: .init(schema: .init(.string))]), + requestBody: .init(content: [.json: .content(.init(schema: .init(.string)))]), responses: [200: .reference(.component(named: "test"))], callbacks: [ "callback": .init( @@ -290,7 +290,7 @@ extension OperationTests { parameters: [ .reference(.component(named: "hello")) ], - requestBody: .init(content: [.json: .init(schema: .init(.string))]), + requestBody: .init(content: [.json: .content(.init(schema: .init(.string)))]), responses: [200: .reference(.component(named: "test"))], callbacks: [ "callback": .init( @@ -317,9 +317,9 @@ extension OperationTests { ) // compare request to construction of Either - XCTAssertEqual(operation.requestBody, .request(.init(content: [.json: .init(schema: .init(.string))]))) + XCTAssertEqual(operation.requestBody, .request(.init(content: [.json: .content(.init(schema: .init(.string)))]))) // compare request having extracted from Either - XCTAssertEqual(operation.requestBody?.requestValue, .init(content: [.json: .init(schema: .init(.string))])) + XCTAssertEqual(operation.requestBody?.requestValue, .init(content: [.json: .content(.init(schema: .init(.string)))])) XCTAssertNil(operation.responses[200]?.responseValue) XCTAssertEqual(operation.responses[200]?.reference, .component(named: "test")) diff --git a/Tests/OpenAPIKitTests/Parameter/DereferencedParameterTests.swift b/Tests/OpenAPIKitTests/Parameter/DereferencedParameterTests.swift index 559491a89b..ccdc106ad0 100644 --- a/Tests/OpenAPIKitTests/Parameter/DereferencedParameterTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/DereferencedParameterTests.swift @@ -27,13 +27,13 @@ final class DereferencedParameterTests: XCTestCase { let t2 = try OpenAPI.Parameter.path( name: "test2", content: [ - .anyText: .init(schema: .string) + .anyText: .content(.init(schema: .string)) ] ).dereferenced(in: .noComponents) XCTAssertEqual(t2.name, "test2") XCTAssertEqual(t2.context, .path(content: [ - .anyText: .init(schema: .string) + .anyText: .content(.init(schema: .string)) ])) XCTAssertEqual( t2.schemaOrContent.contentValue, @@ -49,7 +49,7 @@ final class DereferencedParameterTests: XCTestCase { let t1 = try OpenAPI.Parameter.header( name: "test", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ).dereferenced(in: .noComponents) @@ -81,7 +81,7 @@ final class DereferencedParameterTests: XCTestCase { ) let t1 = try OpenAPI.Parameter.header( name: "test", - content: [.json: .init(schemaReference: .component(named: "test"))] + content: [.json: .content(.init(schemaReference: .component(named: "test")))] ).dereferenced(in: components) XCTAssertEqual( diff --git a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift index 554779cde8..e2eb0c66d1 100644 --- a/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift +++ b/Tests/OpenAPIKitTests/Parameter/ParameterTests.swift @@ -13,7 +13,7 @@ final class ParameterTests: XCTestCase { let t1 = OpenAPI.Parameter.cookie( name: "hello", required: true, - schemaOrContent: .init([.json: OpenAPI.Content(schema: .string)]), + schemaOrContent: .init([.json: .content(OpenAPI.Content(schema: .string))]), description: "hi", deprecated: true ) @@ -22,7 +22,7 @@ final class ParameterTests: XCTestCase { let t2 = OpenAPI.Parameter.cookie( name: "hello", required: true, - schemaOrContent: .content([.json: OpenAPI.Content(schema: .string)]), + schemaOrContent: .content([.json: .content(OpenAPI.Content(schema: .string))]), description: "hi", deprecated: true ) @@ -80,8 +80,8 @@ final class ParameterTests: XCTestCase { let t1: OpenAPI.Parameter.Array = [ .parameter(OpenAPI.Parameter.cookie(name: "hello", schema: .string)), .parameter(name: "hello", context: .cookie(schema: .string)), - .parameter(OpenAPI.Parameter.cookie(name: "hello", content: [.json: OpenAPI.Content(schema: .string)])), - .parameter(name: "hello", context: .cookie(content: [.json: OpenAPI.Content(schema: .string)])), + .parameter(OpenAPI.Parameter.cookie(name: "hello", content: [.json: .content(OpenAPI.Content(schema: .string))])), + .parameter(name: "hello", context: .cookie(content: [.json: .content(OpenAPI.Content(schema: .string))])), .reference(.component( named: "hello")) ] @@ -107,7 +107,7 @@ extension ParameterTests { func test_minimalContent_encode() throws { let parameter = OpenAPI.Parameter.path( name: "hello", - content: [ .json: .init(schema: .string)] + content: [ .json: .content(.init(schema: .string))] ) let encodedParameter = try orderUnstableTestStringFromEncoding(of: parameter) @@ -154,10 +154,10 @@ extension ParameterTests { parameter, OpenAPI.Parameter.path( name: "hello", - content: [ .json: .init(schema: .string)] + content: [ .json: .content(.init(schema: .string))] ) ) - XCTAssertEqual(parameter.schemaOrContent.contentValue, [ .json: .init(schema: .string) ]) + XCTAssertEqual(parameter.schemaOrContent.contentValue, [ .json: .content(.init(schema: .string) )]) } func test_minimalSchema_encode() throws { diff --git a/Tests/OpenAPIKitTests/Request/DereferencedRequestTests.swift b/Tests/OpenAPIKitTests/Request/DereferencedRequestTests.swift index 822ce2f983..8369a62414 100644 --- a/Tests/OpenAPIKitTests/Request/DereferencedRequestTests.swift +++ b/Tests/OpenAPIKitTests/Request/DereferencedRequestTests.swift @@ -22,7 +22,7 @@ final class DereferencedRequestTests: XCTestCase { func test_allInlinedComponents() throws { let t1 = try OpenAPI.Request( description: "test", - content: [.json: .init(schema: .string)] + content: [.json: .content(.init(schema: .string))] ).dereferenced(in: .noComponents) XCTAssertEqual(t1.content[.json]?.schema?.jsonSchema, .string) // test dynamic member lookup @@ -38,7 +38,7 @@ final class DereferencedRequestTests: XCTestCase { let t1 = try OpenAPI.Request( description: "test", content: [ - .json: .init(schemaReference: .component(named: "test")) + .json: .content(.init(schemaReference: .component(named: "test"))) ] ).dereferenced(in: components) XCTAssertEqual( @@ -52,7 +52,7 @@ final class DereferencedRequestTests: XCTestCase { try OpenAPI.Request( description: "test", content: [ - .json: .init(schemaReference: .component(named: "test")) + .json: .content(.init(schemaReference: .component(named: "test"))) ] ).dereferenced(in: .noComponents) ) diff --git a/Tests/OpenAPIKitTests/Request/RequestTests.swift b/Tests/OpenAPIKitTests/Request/RequestTests.swift index efa58f603e..0d717b2df6 100644 --- a/Tests/OpenAPIKitTests/Request/RequestTests.swift +++ b/Tests/OpenAPIKitTests/Request/RequestTests.swift @@ -21,24 +21,31 @@ final class RequestTests: XCTestCase { content: [:], required: true) - let _ = OpenAPI.Request(description: "A Request", - content: [ - .json: .init(schema: .init(simpleSchema)) - + let _ = OpenAPI.Request( + description: "A Request", + content: [ + .json: .content(.init(schema: .init(simpleSchema))) ], - required: false) + required: false + ) - let _ = OpenAPI.Request(content: [ - .xml: .init(schema: .init(simpleSchema)) - ]) + let _ = OpenAPI.Request( + content: [ + .xml: .content(.init(schema: .init(simpleSchema))) + ] + ) - let _ = OpenAPI.Request(content: [ - .form: .init(schema: .init(simpleSchema)) - ]) + let _ = OpenAPI.Request( + content: [ + .form: .content(.init(schema: .init(simpleSchema))) + ] + ) - let _ = OpenAPI.Request(content: [ - .json: .init(schema: .reference(.external(URL(string: "hello.json#/world")!))) - ]) + let _ = OpenAPI.Request( + content: [ + .json: .content(.init(schema: .reference(.external(URL(string: "hello.json#/world")!)))) + ] + ) } } @@ -60,9 +67,11 @@ extension RequestTests { } func test_onlyReferenceContent_encode() { - let request = OpenAPI.Request(content: [ - .json: .init(schema: .reference(.external(URL(string: "hello.json#/world")!))) - ]) + let request = OpenAPI.Request( + content: [ + .json: .content(.init(schema: .reference(.external(URL(string: "hello.json#/world")!)))) + ] + ) let encodedString = try! orderUnstableTestStringFromEncoding(of: request) assertJSONEquivalent( @@ -85,9 +94,13 @@ extension RequestTests { let requestData = #"{ "content": { "application/json": { "schema": { "$ref": "hello.json#/world" } } } }"#.data(using: .utf8)! let request = try! orderUnstableDecode(OpenAPI.Request.self, from: requestData) - XCTAssertEqual(request, OpenAPI.Request(content: [ - .json : .init(schema: .reference(.external(URL(string: "hello.json#/world")!))) - ])) + XCTAssertEqual(request, + OpenAPI.Request( + content: [ + .json : .content(.init(schema: .reference(.external(URL(string: "hello.json#/world")!)))) + ] + ) + ) } func test_onlySchemaContent_encode() { @@ -97,7 +110,7 @@ extension RequestTests { ] ) let request = OpenAPI.Request(content: [ - .json: .init(schema: .init(schema)) + .json: .content(.init(schema: .init(schema))) ]) let encodedString = try! orderUnstableTestStringFromEncoding(of: request) @@ -147,10 +160,12 @@ extension RequestTests { request, OpenAPI.Request( content: [ - .json : .init( - schema: .init( - .object( - properties: ["hello": .string(required: false)] + .json : .content( + .init( + schema: .init( + .object( + properties: ["hello": .string(required: false)] + ) ) ) ) @@ -289,7 +304,7 @@ extension RequestTests { ] ) let request = OpenAPI.Request(content: [ - .xml: .init(schema: .init(schema)) + .xml: .content(.init(schema: .init(schema))) ]) let encodedString = try! orderUnstableTestStringFromEncoding(of: request) @@ -339,9 +354,11 @@ extension RequestTests { request, OpenAPI.Request( content: [ - .xml : .init( - schema: .init( - .object(properties: ["hello": .string(required: false)]) + .xml : .content( + .init( + schema: .init( + .object(properties: ["hello": .string(required: false)]) + ) ) ) ] @@ -356,7 +373,7 @@ extension RequestTests { ] ) let request = OpenAPI.Request(content: [ - .form: .init(schema: .init(schema)) + .form: .content(.init(schema: .init(schema))) ]) let encodedString = try! orderUnstableTestStringFromEncoding(of: request) @@ -406,9 +423,11 @@ extension RequestTests { request, OpenAPI.Request( content: [ - .form : .init( - schema: .init( - .object(properties: ["hello": .string(required: false)]) + .form : .content( + .init( + schema: .init( + .object(properties: ["hello": .string(required: false)]) + ) ) ) ] diff --git a/Tests/OpenAPIKitTests/Response/DereferencedResponseTests.swift b/Tests/OpenAPIKitTests/Response/DereferencedResponseTests.swift index 8d15a86ffa..3b42bc1b1f 100644 --- a/Tests/OpenAPIKitTests/Response/DereferencedResponseTests.swift +++ b/Tests/OpenAPIKitTests/Response/DereferencedResponseTests.swift @@ -24,7 +24,7 @@ final class DereferencedResponseTests: XCTestCase { "Header": .header(.init(schema: .string)) ], content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ], links: [ "Link": .link(operationId: "link1") @@ -73,7 +73,7 @@ final class DereferencedResponseTests: XCTestCase { let t1 = try OpenAPI.Response( description: "test", content: [ - .json: .init(schemaReference: .component(named: "test")) + .json: .content(.init(schemaReference: .component(named: "test"))) ] ).dereferenced(in: components) XCTAssertEqual( @@ -87,7 +87,7 @@ final class DereferencedResponseTests: XCTestCase { try OpenAPI.Response( description: "test", content: [ - .json: .init(schemaReference: .component(named: "test")) + .json: .content(.init(schemaReference: .component(named: "test"))) ] ).dereferenced(in: .noComponents) ) diff --git a/Tests/OpenAPIKitTests/Response/ResponseTests.swift b/Tests/OpenAPIKitTests/Response/ResponseTests.swift index fcc27d71fa..1be7883653 100644 --- a/Tests/OpenAPIKitTests/Response/ResponseTests.swift +++ b/Tests/OpenAPIKitTests/Response/ResponseTests.swift @@ -21,10 +21,10 @@ final class ResponseTests: XCTestCase { let header = OpenAPI.Header(schemaOrContent: .init(.header(.string))) let r2 = OpenAPI.Response(description: "", headers: ["hello": .init(header)], - content: [.json: content]) + content: [.json: .content(content)]) XCTAssertEqual(r2.description, "") XCTAssertEqual(r2.headers?["hello"]?.headerValue, header) - XCTAssertEqual(r2.content, [.json: content]) + XCTAssertEqual(r2.content, [.json: .content(content)]) XCTAssertEqual(r2.conditionalWarnings.count, 0) // two OAS 3.2.0 warnings: summary is used and description is not @@ -209,7 +209,7 @@ extension ResponseTests { let response = OpenAPI.Response( description: "hello world", headers: ["hello": .init(header)], - content: [.json: content] + content: [.json: .content(content)] ) let encodedResponse = try! orderUnstableTestStringFromEncoding(of: response) @@ -259,7 +259,7 @@ extension ResponseTests { OpenAPI.Response( description: "hello world", headers: ["hello": .init(header)], - content: [.json: content] + content: [.json: .content(content)] ) ) } @@ -324,7 +324,7 @@ extension ResponseTests { let response = OpenAPI.Response( description: "hello world", headers: ["hello": .init(header)], - content: [.json: content], + content: [.json: .content(content)], vendorExtensions: [ "x-specialFeature": true ] ) @@ -377,7 +377,7 @@ extension ResponseTests { OpenAPI.Response( description: "hello world", headers: ["hello": .init(header)], - content: [.json: content], + content: [.json: .content(content)], vendorExtensions: [ "x-specialFeature": true ] ) ) diff --git a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift index 0f9bac4fa2..72c446a636 100644 --- a/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift +++ b/Tests/OpenAPIKitTests/Validator/BuiltinValidationTests.swift @@ -94,9 +94,9 @@ final class BuiltinValidationTests: XCTestCase { 200: .response( description: "Test", content: [ - .json: .init( + .json: .content(.init( schema: .fragment(.init()) - ) + )) ] ) ] @@ -121,11 +121,13 @@ final class BuiltinValidationTests: XCTestCase { 200: .response( description: "Test", content: [ - .json: .init( - schema: .object( - properties: [ - "nested": .fragment(.init()) - ] + .json: .content( + .init( + schema: .object( + properties: [ + "nested": .fragment(.init()) + ] + ) ) ) ] @@ -157,11 +159,13 @@ final class BuiltinValidationTests: XCTestCase { 200: .response( description: "Test", content: [ - .json: .init( - // following is _not_ an empty schema component - // because it is actually a `{ "type": "object" }` - // instead of a `{ }` - schema: .object + .json: .content( + .init( + // following is _not_ an empty schema component + // because it is actually a `{ "type": "object" }` + // instead of a `{ }` + schema: .object + ) ) ] ) @@ -731,13 +735,15 @@ final class BuiltinValidationTests: XCTestCase { description: "response2", headers: ["header1": .reference(.component(named: "header1"))], content: [ - .json: .init( - schema: .string, - examples: [ - "example1": .reference(.component(named: "example1")) - ] + .json: .content( + .init( + schema: .string, + examples: [ + "example1": .reference(.component(named: "example1")) + ] + ) ), - .xml: .init(schemaReference: .component(named: "schema1")) + .xml: .content(.init(schemaReference: .component(named: "schema1"))) ], links: ["linky": .reference(.component(named: "link1"))] ) @@ -806,7 +812,7 @@ final class BuiltinValidationTests: XCTestCase { "header1": .reference(.component(named: "header1")), "external": .reference(.external(URL(string: "https://website.com/file.json#/hello/world")!)) ], - content: [ + content: .direct([ .json: .init( schema: .string, examples: [ @@ -816,7 +822,7 @@ final class BuiltinValidationTests: XCTestCase { ), .xml: .init(schemaReference: .component(named: "schema1")), .txt: .init(schemaReference: .external(URL(string: "https://website.com/file.json#/hello/world")!)) - ], + ]), links: [ "linky": .reference(.component(named: "link1")), "linky2": .reference(.external(URL(string: "https://linky.com")!)) @@ -851,7 +857,7 @@ final class BuiltinValidationTests: XCTestCase { "example1": .init(value: .b("hello")) ], requestBodies: [ - "request1": .init(content: [.json: .init(schema: .object)]) + "request1": .init(content: [.json: .content(.init(schema: .object))]) ], headers: [ "header1": .init(schema: .string) diff --git a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift index 54bb6f7b5b..57b475ac7a 100644 --- a/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift +++ b/Tests/OpenAPIKitTests/Validator/ValidatorTests.swift @@ -71,8 +71,8 @@ final class ValidatorTests: XCTestCase { ) let _ = Validation( - check: unwrap(\OpenAPI.Content.Map[.json], into: contentValidation), - when: \OpenAPI.Content.Map[.json] != nil + check: unwrap(\OpenAPI.Content.Map[.json]?.contentValue, into: contentValidation), + when: \OpenAPI.Content.Map[.json]?.contentValue != nil ) } @@ -541,7 +541,7 @@ final class ValidatorTests: XCTestCase { 200: .response( description: "Get the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ) ] @@ -573,13 +573,13 @@ final class ValidatorTests: XCTestCase { 200: .response( description: "Get the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ), 404: .response( description: "Leave the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ) ] @@ -616,13 +616,13 @@ final class ValidatorTests: XCTestCase { 200: .response( description: "Get the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ), 404: .response( description: "Leave the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ) ] @@ -655,7 +655,7 @@ final class ValidatorTests: XCTestCase { 200: .response( description: "Get the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ) ] @@ -765,7 +765,7 @@ final class ValidatorTests: XCTestCase { 200: .response( description: "Get the world", content: [ - .json: .init(schema: .string) + .json: .content(.init(schema: .string)) ] ) ] @@ -1094,26 +1094,26 @@ final class ValidatorTests: XCTestCase { let createRequest = OpenAPI.Request( content: [ - .json: .init( + .json: .content(.init( schema: .object( properties: [ "classification": .string(allowedValues: "big", "small") ] ) - ) + )) ] ) let successCreateResponse = OpenAPI.Response( description: "Created Widget", content: [ - .json: .init( + .json: .content(.init( schema: .object( properties: [ "classification": .string(allowedValues: "big", "small") ] ) - ) + )) ] ) @@ -1157,20 +1157,20 @@ final class ValidatorTests: XCTestCase { let requestBodyContainsName = Validation( check: unwrap( - \.content[.json]?.schema, + \.content[.json]?.contentValue?.schema, into: resourceContainsName ), - when: \OpenAPI.Request.content[.json]?.schema != nil + when: \OpenAPI.Request.content[.json]?.contentValue?.schema != nil ) let responseBodyContainsNameAndId = Validation( check: unwrap( - \.content[.json]?.schema, + \.content[.json]?.contentValue?.schema, into: resourceContainsName, responseResourceContainsId ), - when: \OpenAPI.Response.content[.json]?.schema != nil + when: \OpenAPI.Response.content[.json]?.contentValue?.schema != nil ) let successResponseBodyContainsNameAndId = Validation( @@ -1221,21 +1221,21 @@ final class ValidatorTests: XCTestCase { func test_requestBodySchemaValidationSucceeds() throws { let createRequest = OpenAPI.Request( content: [ - .json: .init( + .json: .content(.init( schema: .object( properties: [ "name": .string, "classification": .string(allowedValues: "big", "small") ] ) - ) + )) ] ) let successCreateResponse = OpenAPI.Response( description: "Created Widget", content: [ - .json: .init( + .json: .content(.init( schema: .object( properties: [ "id": .integer, @@ -1243,7 +1243,7 @@ final class ValidatorTests: XCTestCase { "classification": .string(allowedValues: "big", "small") ] ) - ) + )) ] ) @@ -1287,20 +1287,20 @@ final class ValidatorTests: XCTestCase { let requestBodyContainsName = Validation( check: unwrap( - \.content[.json]?.schema, + \.content[.json]?.contentValue?.schema, into: resourceContainsName ), - when: \OpenAPI.Request.content[.json]?.schema != nil + when: \OpenAPI.Request.content[.json]?.contentValue?.schema != nil ) let responseBodyContainsNameAndId = Validation( check: unwrap( - \.content[.json]?.schema, + \.content[.json]?.contentValue?.schema, into: resourceContainsName, responseResourceContainsId ), - when: \OpenAPI.Response.content[.json]?.schema != nil + when: \OpenAPI.Response.content[.json]?.contentValue?.schema != nil ) let successResponseBodyContainsNameAndId = Validation( diff --git a/documentation/migration_guides/v5_migration_guide.md b/documentation/migration_guides/v5_migration_guide.md index 57ae7732fa..8c2ced69d4 100644 --- a/documentation/migration_guides/v5_migration_guide.md +++ b/documentation/migration_guides/v5_migration_guide.md @@ -297,6 +297,52 @@ case .b(let positionalEncoding): } ``` +### Content Map (`OpenAPI.Content.Map`) +Content maps now reflect the specification correctly in allowing references to +Media Type Objects (`OpenAPI.Content`). This means that code that creates or +reads `OpenAPI.Content` values from a `Content.Map` needs to be updated to dig +one level deeper and account for the possibility that the `Content` is +referenced to instead of inlined. + +```swift +// BEFORE +let contentMap: OpenAPI.Content.Map = [ + .json: .init(schema: .string) +] +let jsonContent: OpenAPI.Content? = contentMap[.json] + +// AFTER +let contentMap: OpenAPI.Content.Map = [ + .json: .content(.init(schema: .string)), + .xml: .component(named: "xml") +] +let jsonContent: OpenAPI.Content? = contentMap[.json]?.contentValue +if case let .b(jsonContent) = contentMap[.json] { /*...*/ } + +let referenceToContent: OpenAPI.Reference? = contentMap[.xml]?.reference +if case let .a(referenceToContent) = contentMap[.xml] { /*...*/ } + +``` + +If you are constructing an `OpenAPI.Content.Map`, you have one other option for +migrating existing code: You can use the new `.direct()` convenience +constructor: +```swift +// BEFORE +let contentMap: OpenAPI.Content.Map = [ + .json: .init(schema: .string) +] + +// AFTER +let contentMap: OpenAPI.Content.Map = [ + .json: .content(.init(schema: .string)) +] +// OR +let contentMap: OpenAPI.Content.Map = .direct([ + .json: .init(schema: .string) +]) +``` + ### Security Scheme Object (`OpenAPI.SecurityScheme`) The `type` property's enumeration gains a new associated value on the `oauth2` case.