Skip to content

Commit 35e0fe6

Browse files
authored
Merge pull request #465 from mattpolzin/feature/449/media-types-components
Add OAS 3.2.0 mediaTypes to Components Object
2 parents 8079a49 + b4bce37 commit 35e0fe6

31 files changed

+431
-250
lines changed

Sources/OpenAPIKit/CodableVendorExtendable.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,7 +114,8 @@ extension CodableVendorExtendable {
114114
throw GenericError(
115115
subjectName: "Vendor Extension",
116116
details: "Found at least one vendor extension property that does not begin with the required 'x-' prefix. Invalid properties: \(invalidKeysList)",
117-
codingPath: decoder.codingPath
117+
codingPath: decoder.codingPath,
118+
pathIncludesSubject: false
118119
)
119120
}
120121

Sources/OpenAPIKit/Components Object/Components+Locatable.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ extension OpenAPI.Link: ComponentDictionaryLocatable {
6464
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.links) }
6565
}
6666

67+
extension OpenAPI.Content: ComponentDictionaryLocatable {
68+
public static var openAPIComponentsKey: String { "mediaTypes" }
69+
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.mediaTypes) }
70+
}
71+
6772
extension OpenAPI.PathItem: ComponentDictionaryLocatable {
6873
public static var openAPIComponentsKey: String { "pathItems" }
6974
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .a(\.pathItems) }

Sources/OpenAPIKit/Components Object/Components.swift

Lines changed: 65 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension OpenAPI {
4545
/// "my_param": .reference(.component(named: "my_direct_param"))
4646
/// ]
4747
/// )
48-
public struct Components: Equatable, CodableVendorExtendable, Sendable {
48+
public struct Components: HasConditionalWarnings, CodableVendorExtendable, Sendable {
4949

5050
public var schemas: ComponentDictionary<JSONSchema>
5151
public var responses: ComponentReferenceDictionary<Response>
@@ -56,6 +56,8 @@ extension OpenAPI {
5656
public var securitySchemes: ComponentReferenceDictionary<SecurityScheme>
5757
public var links: ComponentReferenceDictionary<Link>
5858
public var callbacks: ComponentReferenceDictionary<Callbacks>
59+
/// Media Type Objects (aka `OpenAPI.Content`)
60+
public var mediaTypes: ComponentReferenceDictionary<Content>
5961

6062
public var pathItems: ComponentDictionary<PathItem>
6163

@@ -66,6 +68,8 @@ extension OpenAPI {
6668
/// where the values are anything codable.
6769
public var vendorExtensions: [String: AnyCodable]
6870

71+
public let conditionalWarnings: [(any Condition, OpenAPI.Warning)]
72+
6973
public init(
7074
schemas: ComponentDictionary<JSONSchema> = [:],
7175
responses: ComponentReferenceDictionary<Response> = [:],
@@ -76,6 +80,7 @@ extension OpenAPI {
7680
securitySchemes: ComponentReferenceDictionary<SecurityScheme> = [:],
7781
links: ComponentReferenceDictionary<Link> = [:],
7882
callbacks: ComponentReferenceDictionary<Callbacks> = [:],
83+
mediaTypes: ComponentReferenceDictionary<Content> = [:],
7984
pathItems: ComponentDictionary<PathItem> = [:],
8085
vendorExtensions: [String: AnyCodable] = [:]
8186
) {
@@ -88,8 +93,13 @@ extension OpenAPI {
8893
self.securitySchemes = securitySchemes
8994
self.links = links
9095
self.callbacks = callbacks
96+
self.mediaTypes = mediaTypes
9197
self.pathItems = pathItems
9298
self.vendorExtensions = vendorExtensions
99+
100+
self.conditionalWarnings = [
101+
nonEmptyVersionWarning(fieldName: "mediaTypes", value: mediaTypes, minimumVersion: .v3_2_0)
102+
].compactMap { $0 }
93103
}
94104

95105
/// Construct components as "direct" entries (no references). When
@@ -105,6 +115,7 @@ extension OpenAPI {
105115
securitySchemes: ComponentDictionary<SecurityScheme> = [:],
106116
links: ComponentDictionary<Link> = [:],
107117
callbacks: ComponentDictionary<Callbacks> = [:],
118+
mediaTypes: ComponentDictionary<Content> = [:],
108119
pathItems: ComponentDictionary<PathItem> = [:],
109120
vendorExtensions: [String: AnyCodable] = [:]
110121
) -> Self {
@@ -118,6 +129,7 @@ extension OpenAPI {
118129
securitySchemes: securitySchemes.mapValues { .b($0) },
119130
links: links.mapValues { .b($0) },
120131
callbacks: callbacks.mapValues { .b($0) },
132+
mediaTypes: mediaTypes.mapValues { .b($0) },
121133
pathItems: pathItems,
122134
vendorExtensions: vendorExtensions
123135
)
@@ -132,6 +144,32 @@ extension OpenAPI {
132144
}
133145
}
134146

147+
extension OpenAPI.Components: Equatable {
148+
public static func == (lhs: Self, rhs: Self) -> Bool {
149+
lhs.schemas == rhs.schemas
150+
&& lhs.responses == rhs.responses
151+
&& lhs.parameters == rhs.parameters
152+
&& lhs.examples == rhs.examples
153+
&& lhs.requestBodies == rhs.requestBodies
154+
&& lhs.headers == rhs.headers
155+
&& lhs.securitySchemes == rhs.securitySchemes
156+
&& lhs.links == rhs.links
157+
&& lhs.callbacks == rhs.callbacks
158+
&& lhs.mediaTypes == rhs.mediaTypes
159+
&& lhs.pathItems == rhs.pathItems
160+
&& lhs.vendorExtensions == rhs.vendorExtensions
161+
}
162+
}
163+
164+
fileprivate func nonEmptyVersionWarning(fieldName: String, value: any Collection, minimumVersion: OpenAPI.Document.Version) -> (any Condition, OpenAPI.Warning)? {
165+
if value.isEmpty { return nil }
166+
167+
return OpenAPI.Document.ConditionalWarnings.version(
168+
lessThan: minimumVersion,
169+
doesNotSupport: "The Components \(fieldName) map"
170+
)
171+
}
172+
135173
extension OpenAPI {
136174

137175
public typealias ComponentDictionary<T> = OrderedDictionary<ComponentKey, T>
@@ -170,6 +208,7 @@ extension OpenAPI.Components {
170208
try securitySchemes.merge(other.securitySchemes, uniquingKeysWith: detectCollision(type: "securitySchemes"))
171209
try links.merge(other.links, uniquingKeysWith: detectCollision(type: "links"))
172210
try callbacks.merge(other.callbacks, uniquingKeysWith: detectCollision(type: "callbacks"))
211+
try mediaTypes.merge(other.mediaTypes, uniquingKeysWith: detectCollision(type: "mediaTypes"))
173212
try pathItems.merge(other.pathItems, uniquingKeysWith: detectCollision(type: "pathItems"))
174213
try vendorExtensions.merge(other.vendorExtensions, uniquingKeysWith: detectCollision(type: "vendorExtensions"))
175214
}
@@ -185,6 +224,7 @@ extension OpenAPI.Components {
185224
securitySchemes.sortKeys()
186225
links.sortKeys()
187226
callbacks.sortKeys()
227+
mediaTypes.sortKeys()
188228
pathItems.sortKeys()
189229
}
190230
}
@@ -237,6 +277,10 @@ extension OpenAPI.Components: Encodable {
237277
if !callbacks.isEmpty {
238278
try container.encode(callbacks, forKey: .callbacks)
239279
}
280+
281+
if !mediaTypes.isEmpty {
282+
try container.encode(mediaTypes, forKey: .mediaTypes)
283+
}
240284

241285
if !pathItems.isEmpty {
242286
try container.encode(pathItems, forKey: .pathItems)
@@ -276,10 +320,16 @@ extension OpenAPI.Components: Decodable {
276320
links = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Link>.self, forKey: .links) ?? [:]
277321

278322
callbacks = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Callbacks>.self, forKey: .callbacks) ?? [:]
323+
324+
mediaTypes = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Content>.self, forKey: .mediaTypes) ?? [:]
279325

280326
pathItems = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.PathItem>.self, forKey: .pathItems) ?? [:]
281327

282328
vendorExtensions = try Self.extensions(from: decoder)
329+
330+
conditionalWarnings = [
331+
nonEmptyVersionWarning(fieldName: "mediaTypes", value: mediaTypes, minimumVersion: .v3_2_0)
332+
].compactMap { $0 }
283333
} catch let error as EitherDecodeNoTypesMatchedError {
284334
if let underlyingError = OpenAPI.Error.Decoding.Document.eitherBranchToDigInto(error) {
285335
throw (underlyingError.underlyingError ?? underlyingError)
@@ -310,6 +360,7 @@ extension OpenAPI.Components {
310360
case securitySchemes
311361
case links
312362
case callbacks
363+
case mediaTypes
313364
case pathItems
314365

315366
case extended(String)
@@ -325,6 +376,7 @@ extension OpenAPI.Components {
325376
.securitySchemes,
326377
.links,
327378
.callbacks,
379+
.mediaTypes,
328380
.pathItems
329381
]
330382
}
@@ -353,6 +405,8 @@ extension OpenAPI.Components {
353405
self = .links
354406
case "callbacks":
355407
self = .callbacks
408+
case "mediaTypes":
409+
self = .mediaTypes
356410
case "pathItems":
357411
self = .pathItems
358412
default:
@@ -380,6 +434,8 @@ extension OpenAPI.Components {
380434
return "links"
381435
case .callbacks:
382436
return "callbacks"
437+
case .mediaTypes:
438+
return "mediaTypes"
383439
case .pathItems:
384440
return "pathItems"
385441
case .extended(let key):
@@ -409,6 +465,7 @@ extension OpenAPI.Components {
409465
let oldSecuritySchemes = securitySchemes
410466
let oldLinks = links
411467
let oldCallbacks = callbacks
468+
let oldMediaTypes = mediaTypes
412469
let oldPathItems = pathItems
413470

414471
async let (newSchemas, c1, m1) = oldSchemas.externallyDereferenced(with: loader)
@@ -420,7 +477,8 @@ extension OpenAPI.Components {
420477
async let (newSecuritySchemes, c7, m7) = oldSecuritySchemes.externallyDereferenced(with: loader)
421478
// async let (newLinks, c8, m8) = oldLinks.externallyDereferenced(with: loader)
422479
// async let (newCallbacks, c9, m9) = oldCallbacks.externallyDereferenced(with: loader)
423-
async let (newPathItems, c10, m10) = oldPathItems.externallyDereferenced(with: loader)
480+
async let (newMediaTypes, c10, m10) = oldMediaTypes.externallyDereferenced(with: loader)
481+
async let (newPathItems, c11, m11) = oldPathItems.externallyDereferenced(with: loader)
424482

425483
schemas = try await newSchemas
426484
responses = try await newResponses
@@ -431,6 +489,7 @@ extension OpenAPI.Components {
431489
securitySchemes = try await newSecuritySchemes
432490
// links = try await newLinks
433491
// callbacks = try await newCallbacks
492+
mediaTypes = try await newMediaTypes
434493
pathItems = try await newPathItems
435494

436495
let c1Resolved = try await c1
@@ -443,6 +502,7 @@ extension OpenAPI.Components {
443502
// let c8Resolved = try await c8
444503
// let c9Resolved = try await c9
445504
let c10Resolved = try await c10
505+
let c11Resolved = try await c11
446506

447507
// For Swift 5.10+ we can delete the following links and callbacks code and uncomment the
448508
// preferred code above.
@@ -464,8 +524,9 @@ extension OpenAPI.Components {
464524
&& c8Resolved.isEmpty
465525
&& c9Resolved.isEmpty
466526
&& c10Resolved.isEmpty
527+
&& c11Resolved.isEmpty
467528

468-
let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10
529+
let newMessages = try await context + m1 + m2 + m3 + m4 + m5 + m6 + m7 + m8 + m9 + m10 + m11
469530

470531
if noNewComponents { return newMessages }
471532

@@ -479,6 +540,7 @@ extension OpenAPI.Components {
479540
try merge(c8Resolved)
480541
try merge(c9Resolved)
481542
try merge(c10Resolved)
543+
try merge(c11Resolved)
482544

483545
switch depth {
484546
case .iterations(let number):

Sources/OpenAPIKit/Content/Content.swift

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -319,7 +319,15 @@ extension OpenAPI.Content {
319319
}
320320

321321
extension OpenAPI.Content {
322-
public typealias Map = OrderedDictionary<OpenAPI.ContentType, OpenAPI.Content>
322+
public typealias Map = OrderedDictionary<OpenAPI.ContentType, Either<OpenAPI.Reference<OpenAPI.Content>, OpenAPI.Content>>
323+
}
324+
325+
extension OpenAPI.Content.Map {
326+
/// Construct an OpenAPI.Content.Map for which none of the values are
327+
/// references (all values are OpenAPI.Content).
328+
public static func direct(_ map: OrderedDictionary<OpenAPI.ContentType, OpenAPI.Content>) -> OpenAPI.Content.Map {
329+
map.mapValues { .b($0) }
330+
}
323331
}
324332

325333
extension OpenAPI.Content {

Sources/OpenAPIKit/Content/DereferencedContent.swift

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,8 @@ public struct DereferencedContent: Equatable {
3737
internal init(
3838
_ content: OpenAPI.Content,
3939
resolvingIn components: OpenAPI.Components,
40-
following references: Set<AnyHashable>
40+
following references: Set<AnyHashable>,
41+
dereferencedFromComponentNamed name: String?
4142
) throws {
4243
self.schema = try content.schema?._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil)
4344
self.itemSchema = try content.itemSchema?._dereferenced(in: components, following: references, dereferencedFromComponentNamed: nil)
@@ -67,6 +68,11 @@ public struct DereferencedContent: Equatable {
6768
self.encoding = nil
6869
}
6970

71+
var content = content
72+
if let name {
73+
content.vendorExtensions[OpenAPI.Components.componentNameExtension] = .init(name)
74+
}
75+
7076
self.underlyingContent = content
7177
}
7278

@@ -85,7 +91,7 @@ extension OpenAPI.Content: LocallyDereferenceable {
8591
following references: Set<AnyHashable>,
8692
dereferencedFromComponentNamed name: String?
8793
) throws -> DereferencedContent {
88-
return try DereferencedContent(self, resolvingIn: components, following: references)
94+
return try DereferencedContent(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name)
8995
}
9096
}
9197

Sources/OpenAPIKit/Either/Either+Convenience.swift

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,11 @@ extension Either where B == OpenAPI.Link {
103103
public var linkValue: B? { b }
104104
}
105105

106+
extension Either where B == OpenAPI.Content {
107+
/// Retrieve the content if that is what this property contains.
108+
public var contentValue: B? { b }
109+
}
110+
106111
extension Either where B == OpenAPI.Content.Map {
107112
/// Retrieve the content map if that is what this property contains.
108113
public var contentValue: B? { b }
@@ -221,6 +226,11 @@ extension Either where B == OpenAPI.Parameter {
221226
public static func parameter(_ parameter: OpenAPI.Parameter) -> Self { .b(parameter) }
222227
}
223228

229+
extension Either where B == OpenAPI.Content {
230+
/// Construct content.
231+
public static func content(_ content: OpenAPI.Content) -> Self { .b(content) }
232+
}
233+
224234
extension Either where B == OpenAPI.Content.Map {
225235
/// Construct a content map.
226236
public static func content(_ map: OpenAPI.Content.Map) -> Self { .b(map) }

Sources/OpenAPIKit/Encoding and Decoding Errors/ResponseDecodingError.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ extension OpenAPI.Error.Decoding.Response {
7070

7171
internal init(_ error: GenericError) {
7272
var codingPath = Self.relativePath(from: error.codingPath)
73+
74+
if error.pathIncludesSubject {
75+
codingPath = codingPath.dropLast()
76+
}
77+
7378
let code = codingPath.removeFirst().stringValue.lowercased()
7479

7580
// this part of the coding path is structurally guaranteed to be a status code

Sources/OpenAPIKit/Header/DereferencedHeader.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,10 +47,10 @@ public struct DereferencedHeader: Equatable {
4747
case .b(let contentMap):
4848
self.schemaOrContent = .b(
4949
try contentMap.mapValues {
50-
try DereferencedContent(
51-
$0,
52-
resolvingIn: components,
53-
following: references
50+
try $0._dereferenced(
51+
in: components,
52+
following: references,
53+
dereferencedFromComponentNamed: nil
5454
)
5555
}
5656
)

Sources/OpenAPIKit/Parameter/DereferencedParameter.swift

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -49,10 +49,10 @@ public struct DereferencedParameter: Equatable {
4949
case .b(let contentMap):
5050
self.schemaOrContent = .b(
5151
try contentMap.mapValues {
52-
try DereferencedContent(
53-
$0,
54-
resolvingIn: components,
55-
following: references
52+
try $0._dereferenced(
53+
in: components,
54+
following: references,
55+
dereferencedFromComponentNamed: nil
5656
)
5757
}
5858
)

Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1173,11 +1173,13 @@ extension JSONSchema.IntegerContext: Decodable {
11731173
let value = try intAttempt
11741174
?? doubleAttempt.map { floatVal in
11751175
guard let integer = Int(exactly: floatVal) else {
1176+
let key = max ? CodingKeys.maximum : CodingKeys.minimum
1177+
let subject = key.rawValue
11761178
throw GenericError(
1177-
subjectName: max ? "maximum" : "minimum",
1179+
subjectName: subject,
11781180
details: "Expected an Integer literal but found a floating point value (\(String(describing: floatVal)))",
1179-
codingPath: decoder.codingPath,
1180-
pathIncludesSubject: false
1181+
codingPath: decoder.codingPath + [key],
1182+
pathIncludesSubject: true
11811183
)
11821184
}
11831185
return integer

0 commit comments

Comments
 (0)