Skip to content

Commit e805233

Browse files
authored
Merge pull request #284 from mattpolzin/bugfix/280/path-item-references
Bugfix/280/path item references
2 parents 24939cf + 8e902c6 commit e805233

File tree

50 files changed

+1000
-263
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+1000
-263
lines changed

Sources/OpenAPIKit/Document/DereferencedDocument.swift

+111-5
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ public struct DereferencedDocument: Equatable {
3232
/// in the array.
3333
public let security: [DereferencedSecurityRequirement]
3434

35+
36+
3537
public subscript<T>(dynamicMember path: KeyPath<OpenAPI.Document, T>) -> T {
3638
return underlyingDocument[keyPath: path]
3739
}
@@ -49,11 +51,7 @@ public struct DereferencedDocument: Equatable {
4951
/// component in the same file that cannot be found in the Components Object.
5052
internal init(_ document: OpenAPI.Document) throws {
5153
self.paths = try document.paths.mapValues {
52-
try DereferencedPathItem(
53-
$0,
54-
resolvingIn: document.components,
55-
following: []
56-
)
54+
try $0._dereferenced(in: document.components, following: [])
5755
}
5856
self.security = try document.security.map {
5957
try DereferencedSecurityRequirement(
@@ -67,7 +65,13 @@ public struct DereferencedDocument: Equatable {
6765
}
6866
}
6967

68+
// MARK: - Dereferenced Helpers
7069
extension DereferencedDocument {
70+
// We override the following helpers defined on `Document`
71+
// because they utilize `PathItems` which might be references
72+
// on a `Document` but are guaranteed to be derefenced at this
73+
// point
74+
7175
/// The pairing of a path and the path item that describes the
7276
/// route at that path.
7377
public struct Route: Equatable {
@@ -89,8 +93,110 @@ extension DereferencedDocument {
8993
public var routes: [Route] {
9094
return paths.map { (path, pathItem) in .init(path: path, pathItem: pathItem) }
9195
}
96+
97+
/// Retrieve an array of all locally defined Operation Ids defined by
98+
/// this API. These Ids are guaranteed to be unique by
99+
/// the OpenAPI Specification.
100+
///
101+
/// The ordering is not necessarily significant, but it will
102+
/// be the order in which each operation is occurred within
103+
/// each path, traversed in the order the paths appear in
104+
/// the document.
105+
///
106+
/// See [Operation Object](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md#operation-object) in the specifcation.
107+
///
108+
public var allOperationIds: [String] {
109+
return paths.values
110+
.flatMap { $0.endpoints }
111+
.compactMap { $0.operation.operationId }
112+
}
113+
114+
/// All servers referenced anywhere in the whole document.
115+
///
116+
/// This property contains all servers defined at any level the document
117+
/// and therefore may or may not contain servers not found in the
118+
/// root servers array.
119+
///
120+
/// The `servers` property on `OpenAPI.Document`, by contrast, contains
121+
/// servers that are applicable to all paths and operations that
122+
/// do not define their own `serves` array to override the root array.
123+
///
124+
/// - Important: For the purposes of returning one of each `Server`,
125+
/// two servers are considered identical if they have the same `url`
126+
/// and `variables`. Differing `description` properties for
127+
/// otherwise identical servers are considered to be two ways to
128+
/// describe the same server. `vendorExtensions` are also
129+
/// ignored when determining server uniqueness.
130+
///
131+
/// The first `Server` encountered will be used, so if the only
132+
/// difference between a server at the root document level and
133+
/// one in an `Operation`'s override of the servers array is the
134+
/// description, the description of the `Server` returned by this
135+
/// property will be that of the root document definition.
136+
///
137+
public var allServers: [OpenAPI.Server] {
138+
// We hash `Variable` without its
139+
// `description` or `vendorExtensions`.
140+
func hash(variable: OpenAPI.Server.Variable, into hasher: inout Hasher) {
141+
hasher.combine(variable.enum)
142+
hasher.combine(variable.default)
143+
}
144+
145+
// We hash `Server` without its `description` or
146+
// `vendorExtensions`.
147+
func hash(server: OpenAPI.Server, into hasher: inout Hasher) {
148+
hasher.combine(server.urlTemplate)
149+
for (key, value) in server.variables {
150+
hasher.combine(key)
151+
hash(variable: value, into: &hasher)
152+
}
153+
}
154+
155+
func hash(for server: OpenAPI.Server) -> Int {
156+
var hasher = Hasher()
157+
hash(server: server, into: &hasher)
158+
return hasher.finalize()
159+
}
160+
161+
var collectedServers = underlyingDocument.servers
162+
var seenHashes = Set(underlyingDocument.servers.map(hash(for:)))
163+
164+
func insertUniquely(server: OpenAPI.Server) {
165+
let serverHash = hash(for: server)
166+
if !seenHashes.contains(serverHash) {
167+
seenHashes.insert(serverHash)
168+
collectedServers.append(server)
169+
}
170+
}
171+
172+
for pathItem in paths.values {
173+
let pathItemServers = pathItem.servers ?? []
174+
pathItemServers.forEach(insertUniquely)
175+
176+
let endpointServers: [OpenAPI.Server] =
177+
pathItem.endpoints
178+
.flatMap { $0.operation.servers ?? [] }
179+
endpointServers.forEach(insertUniquely)
180+
}
181+
182+
return collectedServers
183+
}
184+
185+
/// All Tags used anywhere in the document.
186+
///
187+
/// The tags stored in the `OpenAPI.Document.tags`
188+
/// property need not contain all tags used anywhere in
189+
/// the document. This property is comprehensive.
190+
public var allTags: Set<String> {
191+
return Set(
192+
(underlyingDocument.tags ?? []).map { $0.name }
193+
+ paths.values.flatMap { $0.endpoints }
194+
.flatMap { $0.operation.tags ?? [] }
195+
)
196+
}
92197
}
93198

199+
// MARK: - ResolvedDocument
94200
extension DereferencedDocument {
95201
/// Resolve the document's routes and endpoints.
96202
///

Sources/OpenAPIKit/Document/Document.swift

+26-9
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
import OpenAPIKitCore
99

1010
extension OpenAPI {
11-
/// The root of an OpenAPI 3.0 document.
11+
/// The root of an OpenAPI 3.1 document.
1212
///
1313
/// See [OpenAPI Specification](https://github.com/OAI/OpenAPI-Specification/blob/master/versions/3.1.0.md).
1414
///
@@ -204,18 +204,26 @@ extension OpenAPI.Document {
204204
}
205205
}
206206

207-
/// Get all routes for this document.
207+
/// Get all locally defined routes for this document. PathItems will be
208+
/// looked up in the components, but any remote references or path items
209+
/// missing from the components will be ignored.
208210
///
209211
/// - Returns: An Array of `Routes` with the path
210212
/// and the definition of the route.
213+
///
211214
public var routes: [Route] {
212-
return paths.map { (path, pathItem) in .init(path: path, pathItem: pathItem) }
215+
return paths.compactMap { (path, pathItemRef) in
216+
components[pathItemRef].map { .init(path: path, pathItem: $0) }
217+
}
213218
}
214219

215-
/// Retrieve an array of all Operation Ids defined by
220+
/// Retrieve an array of all locally defined Operation Ids defined by
216221
/// this API. These Ids are guaranteed to be unique by
217222
/// the OpenAPI Specification.
218223
///
224+
/// PathItems will be looked up in the components, but any remote references
225+
/// or path items missing from the components will be ignored.
226+
///
219227
/// The ordering is not necessarily significant, but it will
220228
/// be the order in which each operation is occurred within
221229
/// each path, traversed in the order the paths appear in
@@ -225,6 +233,7 @@ extension OpenAPI.Document {
225233
///
226234
public var allOperationIds: [String] {
227235
return paths.values
236+
.compactMap { components[$0] }
228237
.flatMap { $0.endpoints }
229238
.compactMap { $0.operation.operationId }
230239
}
@@ -288,11 +297,12 @@ extension OpenAPI.Document {
288297
}
289298

290299
for pathItem in paths.values {
291-
let pathItemServers = pathItem.servers ?? []
300+
let pathItemServers = components[pathItem]?.servers ?? []
292301
pathItemServers.forEach(insertUniquely)
293302

294-
let endpointServers = pathItem.endpoints.flatMap { $0.operation.servers ?? [] }
295-
endpointServers.forEach(insertUniquely)
303+
if let endpointServers = (components[pathItem]?.endpoints.flatMap { $0.operation.servers ?? [] }) {
304+
endpointServers.forEach(insertUniquely)
305+
}
296306
}
297307

298308
return collectedServers
@@ -303,10 +313,12 @@ extension OpenAPI.Document {
303313
/// The tags stored in the `OpenAPI.Document.tags`
304314
/// property need not contain all tags used anywhere in
305315
/// the document. This property is comprehensive.
316+
///
306317
public var allTags: Set<String> {
307318
return Set(
308319
(tags ?? []).map { $0.name }
309-
+ paths.values.flatMap { $0.endpoints }
320+
+ paths.values.compactMap { components[$0] }
321+
.flatMap { $0.endpoints }
310322
.flatMap { $0.operation.tags ?? [] }
311323
)
312324
}
@@ -434,6 +446,9 @@ extension OpenAPI.Document: Decodable {
434446
throw OpenAPI.Error.Decoding.Document(error)
435447
} catch let error as DecodingError {
436448

449+
throw OpenAPI.Error.Decoding.Document(error)
450+
} catch let error as EitherDecodeNoTypesMatchedError {
451+
437452
throw OpenAPI.Error.Decoding.Document(error)
438453
}
439454
}
@@ -580,7 +595,9 @@ internal func decodeSecurityRequirements<CodingKeys: CodingKey>(from container:
580595

581596
internal func validateSecurityRequirements(in paths: OpenAPI.PathItem.Map, against components: OpenAPI.Components) throws {
582597
for (path, pathItem) in paths {
583-
for endpoint in pathItem.endpoints {
598+
guard let pathItemValue = components[pathItem] else { continue }
599+
600+
for endpoint in pathItemValue.endpoints {
584601
if let securityRequirements = endpoint.operation.security {
585602
try validate(
586603
securityRequirements: securityRequirements,

Sources/OpenAPIKit/Either/Either+Convenience.swift

+44-10
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,11 @@ extension Either where A == DereferencedSchemaContext {
7171
}
7272
}
7373

74+
extension Either where B == OpenAPI.PathItem {
75+
/// Retrieve the path item if that is what this property contains.
76+
public var pathItemValue: B? { b }
77+
}
78+
7479
extension Either where B == OpenAPI.Parameter {
7580
/// Retrieve the parameter if that is what this property contains.
7681
public var parameterValue: B? { b }
@@ -126,11 +131,6 @@ extension Either where B == OpenAPI.Header {
126131
public var headerValue: B? { b }
127132
}
128133

129-
extension Either where B == OpenAPI.PathItem {
130-
/// Retrieve the path item if that is what this property contains.
131-
public var pathItemValue: B? { b }
132-
}
133-
134134
// MARK: - Convenience constructors
135135
extension Either where A == Bool {
136136
/// Construct a boolean value.
@@ -152,6 +152,45 @@ extension Either where B == JSONSchema {
152152
public static func schema(_ schema: JSONSchema) -> Self { .b(schema) }
153153
}
154154

155+
extension Either where B == OpenAPI.PathItem {
156+
/// Construct a path item value.
157+
public static func pathItem(_ pathItem: OpenAPI.PathItem) -> Self { .b(pathItem) }
158+
159+
public init(
160+
summary: String? = nil,
161+
description: String? = nil,
162+
servers: [OpenAPI.Server]? = nil,
163+
parameters: OpenAPI.Parameter.Array = [],
164+
get: OpenAPI.Operation? = nil,
165+
put: OpenAPI.Operation? = nil,
166+
post: OpenAPI.Operation? = nil,
167+
delete: OpenAPI.Operation? = nil,
168+
options: OpenAPI.Operation? = nil,
169+
head: OpenAPI.Operation? = nil,
170+
patch: OpenAPI.Operation? = nil,
171+
trace: OpenAPI.Operation? = nil,
172+
vendorExtensions: [String: AnyCodable] = [:]
173+
) {
174+
self = .b(
175+
.init(
176+
summary: summary,
177+
description: description,
178+
servers: servers,
179+
parameters: parameters,
180+
get: get,
181+
put: put,
182+
post: post,
183+
delete: delete,
184+
options: options,
185+
head: head,
186+
patch: patch,
187+
trace: trace,
188+
vendorExtensions: vendorExtensions
189+
)
190+
)
191+
}
192+
}
193+
155194
extension Either where B == OpenAPI.Parameter {
156195
/// Construct a parameter value.
157196
public static func parameter(_ parameter: OpenAPI.Parameter) -> Self { .b(parameter) }
@@ -181,8 +220,3 @@ extension Either where B == OpenAPI.Header {
181220
/// Construct a header value.
182221
public static func header(_ header: OpenAPI.Header) -> Self { .b(header) }
183222
}
184-
185-
extension Either where B == OpenAPI.PathItem {
186-
/// Construct a path item value.
187-
public static func pathItem(_ pathItem: OpenAPI.PathItem) -> Self { .b(pathItem) }
188-
}

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

+35-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extension OpenAPI.Error.Decoding {
1616
case path(Path)
1717
case inconsistency(InconsistencyError)
1818
case other(Swift.DecodingError)
19+
case neither(EitherDecodeNoTypesMatchedError)
1920
}
2021
}
2122
}
@@ -31,14 +32,17 @@ extension OpenAPI.Error.Decoding.Document {
3132

3233
case .inconsistency(let error):
3334
return error.subjectName
35+
36+
case .neither(let eitherError):
37+
return eitherError.subjectName
3438
}
3539
}
3640

3741
public var contextString: String {
3842
switch context {
3943
case .path(let pathError):
4044
return pathError.contextString
41-
case .other, .inconsistency:
45+
case .other, .inconsistency, .neither:
4246
return relativeCodingPathString.isEmpty
4347
? "in the root Document object"
4448
: "in Document\(relativeCodingPathString)"
@@ -53,6 +57,8 @@ extension OpenAPI.Error.Decoding.Document {
5357
return error.errorCategory
5458
case .inconsistency(let error):
5559
return .inconsistency(details: error.details)
60+
case .neither(let eitherError):
61+
return eitherError.errorCategory
5662
}
5763
}
5864

@@ -66,6 +72,8 @@ extension OpenAPI.Error.Decoding.Document {
6672
: error.codingPath.dropLast().stringValue
6773
case .path(let pathError):
6874
return pathError.relativeCodingPathString
75+
case .neither(let eitherError):
76+
return eitherError.codingPath.stringValue
6977
}
7078
}
7179

@@ -83,4 +91,30 @@ extension OpenAPI.Error.Decoding.Document {
8391
context = .path(error)
8492
codingPath = error.codingPath
8593
}
94+
95+
internal init(_ eitherError: EitherDecodeNoTypesMatchedError) {
96+
if let eitherBranchToDigInto = Self.eitherBranchToDigInto(eitherError) {
97+
self = Self(unwrapping: eitherBranchToDigInto)
98+
return
99+
}
100+
101+
context = .neither(eitherError)
102+
codingPath = eitherError.codingPath
103+
}
104+
}
105+
106+
extension OpenAPI.Error.Decoding.Document: DiggingError {
107+
public init(unwrapping error: Swift.DecodingError) {
108+
if let decodingError = error.underlyingError as? Swift.DecodingError {
109+
self = Self(unwrapping: decodingError)
110+
} else if let inconsistencyError = error.underlyingError as? InconsistencyError {
111+
self = Self(inconsistencyError)
112+
} else if let pathError = error.underlyingError as? OpenAPI.Error.Decoding.Path {
113+
self = Self(pathError)
114+
} else if let eitherError = error.underlyingError as? EitherDecodeNoTypesMatchedError {
115+
self = Self(eitherError)
116+
} else {
117+
self = Self(error)
118+
}
119+
}
86120
}

0 commit comments

Comments
 (0)