Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion Sources/OpenAPIKit/CodableVendorExtendable.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,11 @@ extension OpenAPI.Link: ComponentDictionaryLocatable {
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.links) }
}

extension OpenAPI.Content: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "mediaTypes" }
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .b(\.mediaTypes) }
}

extension OpenAPI.PathItem: ComponentDictionaryLocatable {
public static var openAPIComponentsKey: String { "pathItems" }
public static var openAPIComponentsKeyPath: Either<WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentDictionary<Self>>, WritableKeyPath<OpenAPI.Components, OpenAPI.ComponentReferenceDictionary<Self>>> { .a(\.pathItems) }
Expand Down
68 changes: 65 additions & 3 deletions Sources/OpenAPIKit/Components Object/Components.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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<JSONSchema>
public var responses: ComponentReferenceDictionary<Response>
Expand All @@ -56,6 +56,8 @@ extension OpenAPI {
public var securitySchemes: ComponentReferenceDictionary<SecurityScheme>
public var links: ComponentReferenceDictionary<Link>
public var callbacks: ComponentReferenceDictionary<Callbacks>
/// Media Type Objects (aka `OpenAPI.Content`)
public var mediaTypes: ComponentReferenceDictionary<Content>

public var pathItems: ComponentDictionary<PathItem>

Expand All @@ -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<JSONSchema> = [:],
responses: ComponentReferenceDictionary<Response> = [:],
Expand All @@ -76,6 +80,7 @@ extension OpenAPI {
securitySchemes: ComponentReferenceDictionary<SecurityScheme> = [:],
links: ComponentReferenceDictionary<Link> = [:],
callbacks: ComponentReferenceDictionary<Callbacks> = [:],
mediaTypes: ComponentReferenceDictionary<Content> = [:],
pathItems: ComponentDictionary<PathItem> = [:],
vendorExtensions: [String: AnyCodable] = [:]
) {
Expand All @@ -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
Expand All @@ -105,6 +115,7 @@ extension OpenAPI {
securitySchemes: ComponentDictionary<SecurityScheme> = [:],
links: ComponentDictionary<Link> = [:],
callbacks: ComponentDictionary<Callbacks> = [:],
mediaTypes: ComponentDictionary<Content> = [:],
pathItems: ComponentDictionary<PathItem> = [:],
vendorExtensions: [String: AnyCodable] = [:]
) -> Self {
Expand All @@ -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
)
Expand All @@ -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<T> = OrderedDictionary<ComponentKey, T>
Expand Down Expand Up @@ -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"))
}
Expand All @@ -185,6 +224,7 @@ extension OpenAPI.Components {
securitySchemes.sortKeys()
links.sortKeys()
callbacks.sortKeys()
mediaTypes.sortKeys()
pathItems.sortKeys()
}
}
Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -276,10 +320,16 @@ extension OpenAPI.Components: Decodable {
links = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Link>.self, forKey: .links) ?? [:]

callbacks = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Callbacks>.self, forKey: .callbacks) ?? [:]

mediaTypes = try container.decodeIfPresent(OpenAPI.ComponentReferenceDictionary<OpenAPI.Content>.self, forKey: .mediaTypes) ?? [:]

pathItems = try container.decodeIfPresent(OpenAPI.ComponentDictionary<OpenAPI.PathItem>.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)
Expand Down Expand Up @@ -310,6 +360,7 @@ extension OpenAPI.Components {
case securitySchemes
case links
case callbacks
case mediaTypes
case pathItems

case extended(String)
Expand All @@ -325,6 +376,7 @@ extension OpenAPI.Components {
.securitySchemes,
.links,
.callbacks,
.mediaTypes,
.pathItems
]
}
Expand Down Expand Up @@ -353,6 +405,8 @@ extension OpenAPI.Components {
self = .links
case "callbacks":
self = .callbacks
case "mediaTypes":
self = .mediaTypes
case "pathItems":
self = .pathItems
default:
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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.
Expand All @@ -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 }

Expand All @@ -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):
Expand Down
10 changes: 9 additions & 1 deletion Sources/OpenAPIKit/Content/Content.swift
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,15 @@ extension OpenAPI.Content {
}

extension OpenAPI.Content {
public typealias Map = OrderedDictionary<OpenAPI.ContentType, OpenAPI.Content>
public typealias Map = OrderedDictionary<OpenAPI.ContentType, Either<OpenAPI.Reference<OpenAPI.Content>, 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.ContentType, OpenAPI.Content>) -> OpenAPI.Content.Map {
map.mapValues { .b($0) }
}
}

extension OpenAPI.Content {
Expand Down
10 changes: 8 additions & 2 deletions Sources/OpenAPIKit/Content/DereferencedContent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@ public struct DereferencedContent: Equatable {
internal init(
_ content: OpenAPI.Content,
resolvingIn components: OpenAPI.Components,
following references: Set<AnyHashable>
following references: Set<AnyHashable>,
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)
Expand Down Expand Up @@ -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
}

Expand All @@ -85,7 +91,7 @@ extension OpenAPI.Content: LocallyDereferenceable {
following references: Set<AnyHashable>,
dereferencedFromComponentNamed name: String?
) throws -> DereferencedContent {
return try DereferencedContent(self, resolvingIn: components, following: references)
return try DereferencedContent(self, resolvingIn: components, following: references, dereferencedFromComponentNamed: name)
}
}

Expand Down
10 changes: 10 additions & 0 deletions Sources/OpenAPIKit/Either/Either+Convenience.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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 }
Expand Down Expand Up @@ -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) }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions Sources/OpenAPIKit/Header/DereferencedHeader.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
)
Expand Down
8 changes: 4 additions & 4 deletions Sources/OpenAPIKit/Parameter/DereferencedParameter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
}
)
Expand Down
8 changes: 5 additions & 3 deletions Sources/OpenAPIKit/Schema Object/JSONSchemaContext.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading