Skip to content

Commit 201586d

Browse files
authored
Merge pull request #88 from mattpolzin/dereferenced-components
Dereferenced and Resolved (canonical) components
2 parents 67e02f6 + 155d7c1 commit 201586d

File tree

88 files changed

+4943
-560
lines changed

Some content is hidden

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

88 files changed

+4943
-560
lines changed

README.md

+71-318
Large diffs are not rendered by default.

Sources/OpenAPIKit/CodableVendorExtendable.swift

-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Mathew Polzin on 10/6/19.
66
//
77

8-
import Foundation
9-
108
/// A `VendorExtendable` type is a type that supports arbitrary
119
/// additions as long as those additions are keyed by strings starting
1210
/// with "x-" (e.g. "x-customThing").

Sources/OpenAPIKit/Component Object/Components+JSONReference.swift

+11-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Mathew Polzin on 3/30/20.
66
//
77

8-
import Foundation
9-
108
extension OpenAPI.Components {
119
/// Check if the `Components` contains the given reference or not.
1210
///
@@ -66,6 +64,11 @@ extension OpenAPI.Components {
6664
///
6765
/// - Important: Dereferencing an external reference (i.e. one that points to another file)
6866
/// is not currently supported by OpenAPIKit and will therefore always throw an error.
67+
///
68+
/// - Throws: `ReferenceError.cannotLookupRemoteReference` or
69+
/// `MissingReferenceError.referenceMissingOnLookup(name:)` depending
70+
/// on whether the reference points to another file or just points to a component in
71+
/// the same file that cannot be found in the Components Object.
6972
public func forceDereference<ReferenceType: ComponentDictionaryLocatable>(_ maybeReference: Either<JSONReference<ReferenceType>, ReferenceType>) throws -> ReferenceType {
7073
switch maybeReference {
7174
case .a(let reference):
@@ -97,6 +100,11 @@ extension OpenAPI.Components {
97100
///
98101
/// - Important: Dereferencing an external reference (i.e. one that points to another file)
99102
/// is not currently supported by OpenAPIKit and will therefore always throw an error.
103+
///
104+
/// - Throws: `ReferenceError.cannotLookupRemoteReference` or
105+
/// `MissingReferenceError.referenceMissingOnLookup(name:)` depending
106+
/// on whether the reference points to another file or just points to a component in
107+
/// the same file that cannot be found in the Components Object.
100108
public func forceDereference<ReferenceType: ComponentDictionaryLocatable>(_ reference: JSONReference<ReferenceType>) throws -> ReferenceType {
101109
guard case .internal = reference else {
102110
throw ReferenceError.cannotLookupRemoteReference
@@ -109,7 +117,7 @@ extension OpenAPI.Components {
109117

110118
/// Create a `JSONReference`.
111119
///
112-
/// - throws: If the given name does not refer to an existing component of the given type.
120+
/// - Throws: If the given name does not refer to an existing component of the given type.
113121
public func reference<ReferenceType: ComponentDictionaryLocatable & Equatable>(named name: String, ofType: ReferenceType.Type) throws -> JSONReference<ReferenceType> {
114122
let internalReference = JSONReference<ReferenceType>.InternalReference.component(name: name)
115123
let reference = JSONReference<ReferenceType>.internal(internalReference)

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

-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Mathew Polzin on 3/30/20.
66
//
77

8-
import Foundation
9-
108
/// Anything conforming to ComponentDictionaryLocatable knows
119
/// where to find resources of its type in the Components Dictionary.
1210
public protocol ComponentDictionaryLocatable {

Sources/OpenAPIKit/Component Object/Components.swift

+7
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ extension OpenAPI {
6060
}
6161

6262
extension OpenAPI {
63+
/// A key for one of the component dictionaries.
64+
///
65+
/// These keys must match the regex
66+
/// `^[a-zA-Z0-9\.\-_]+$`.
6367
public struct ComponentKey: RawRepresentable, ExpressibleByStringLiteral, Codable, Equatable, Hashable, StringConvertibleHintProvider {
6468
public let rawValue: String
6569

@@ -68,6 +72,9 @@ extension OpenAPI {
6872
}
6973

7074
public init?(rawValue: String) {
75+
guard !rawValue.isEmpty else {
76+
return nil
77+
}
7178
var allowedCharacters = CharacterSet.alphanumerics
7279
allowedCharacters.insert(charactersIn: "-_.")
7380
guard CharacterSet(charactersIn: rawValue).isSubset(of: allowedCharacters) else {

Sources/OpenAPIKit/Content/Content.swift

+15-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Mathew Polzin on 7/4/19.
66
//
77

8-
import Foundation
9-
108
extension OpenAPI {
119
/// OpenAPI Spec "Media Type Object"
1210
///
@@ -97,12 +95,26 @@ extension OpenAPI.Content {
9795
}
9896

9997
extension OpenAPI.Content {
98+
/// Pulls the first (inlined, not referenced) example found
99+
/// in the example dictionary given.
100+
///
101+
/// Operates on a dictionary with values that may be either
102+
/// an Example or a reference to and example.
100103
internal static func firstExample(from exampleDict: OpenAPI.Example.Map) -> AnyCodable? {
101104
return exampleDict
102-
.sorted { $0.key < $1.key }
105+
.lazy
103106
.compactMap { $0.value.exampleValue?.value.codableValue }
104107
.first
105108
}
109+
110+
/// Pulls the first example found in the example dictionary
111+
/// given.
112+
internal static func firstExample(from exampleDict: OrderedDictionary<String, OpenAPI.Example>) -> AnyCodable? {
113+
return exampleDict
114+
.lazy
115+
.compactMap { $0.value.value.codableValue }
116+
.first
117+
}
106118
}
107119

108120
// MARK: - Codable
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
//
2+
// DereferencedContent.swift
3+
//
4+
//
5+
// Created by Mathew Polzin on 6/18/20.
6+
//
7+
8+
/// An `OpenAPI.Content` type that guarantees
9+
/// its `schema`, `encoding`, and `examples` are
10+
/// inlined instead of referenced.
11+
@dynamicMemberLookup
12+
public struct DereferencedContent: Equatable {
13+
public let underlyingContent: OpenAPI.Content
14+
public let schema: DereferencedJSONSchema
15+
public let examples: OrderedDictionary<String, OpenAPI.Example>?
16+
public let example: AnyCodable?
17+
public let encoding: OrderedDictionary<String, DereferencedContentEncoding>?
18+
19+
public subscript<T>(dynamicMember path: KeyPath<OpenAPI.Content, T>) -> T {
20+
return underlyingContent[keyPath: path]
21+
}
22+
23+
/// Create a `DereferencedContent` if all references in the
24+
/// content can be found in the given Components Object.
25+
///
26+
/// - Throws: `ReferenceError.cannotLookupRemoteReference` or
27+
/// `MissingReferenceError.referenceMissingOnLookup(name:)` depending
28+
/// on whether an unresolvable reference points to another file or just points to a
29+
/// component in the same file that cannot be found in the Components Object.
30+
public init(_ content: OpenAPI.Content, resolvingIn components: OpenAPI.Components) throws {
31+
self.schema = try DereferencedJSONSchema(
32+
try components.forceDereference(content.schema),
33+
resolvingIn: components
34+
)
35+
let examples = try content.examples?.mapValues { try components.forceDereference($0) }
36+
self.examples = examples
37+
38+
self.example = examples.flatMap(OpenAPI.Content.firstExample(from:))
39+
?? content.example
40+
41+
self.encoding = try content.encoding.map { encodingMap in
42+
try encodingMap.mapValues { encoding in
43+
try DereferencedContentEncoding(
44+
encoding,
45+
resolvingIn: components
46+
)
47+
}
48+
}
49+
50+
self.underlyingContent = content
51+
}
52+
53+
public typealias Map = OrderedDictionary<OpenAPI.ContentType, DereferencedContent>
54+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
//
2+
// DereferencedContentEncoding.swift
3+
//
4+
//
5+
// Created by Mathew Polzin on 6/18/20.
6+
//
7+
8+
/// An `OpenAPI.Content.Encoding` type that
9+
/// guarantees its `headers` are inlined instead
10+
/// of referenced
11+
@dynamicMemberLookup
12+
public struct DereferencedContentEncoding: Equatable {
13+
public let underlyingContentEncoding: OpenAPI.Content.Encoding
14+
public let headers: DereferencedHeader.Map?
15+
16+
public subscript<T>(dynamicMember path: KeyPath<OpenAPI.Content.Encoding, T>) -> T {
17+
return underlyingContentEncoding[keyPath: path]
18+
}
19+
20+
/// Create a `DereferencedContentEncoding` if all references in the
21+
/// content can be found in the given Components Object.
22+
///
23+
/// - Throws: `ReferenceError.cannotLookupRemoteReference` or
24+
/// `MissingReferenceError.referenceMissingOnLookup(name:)` depending
25+
/// on whether an unresolvable reference points to another file or just points to a
26+
/// component in the same file that cannot be found in the Components Object.
27+
public init(_ contentEncoding: OpenAPI.Content.Encoding, resolvingIn components: OpenAPI.Components) throws {
28+
self.headers = try contentEncoding.headers.map { headersMap in
29+
try headersMap.mapValues { header in
30+
try DereferencedHeader(
31+
try components.forceDereference(header),
32+
resolvingIn: components
33+
)
34+
}
35+
}
36+
37+
self.underlyingContentEncoding = contentEncoding
38+
}
39+
}

Sources/OpenAPIKit/Discriminator.swift

-2
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
// Created by Mathew Polzin on 10/6/19.
66
//
77

8-
import Foundation
9-
108
extension OpenAPI {
119
/// OpenAPI Spec "Disciminator Object"
1210
///
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
//
2+
// DereferencedDocument.swift
3+
//
4+
//
5+
// Created by Mathew Polzin on 6/19/20.
6+
//
7+
8+
/// An `OpenAPI.Document` type that guarantees
9+
/// its `paths` and `security` are inlined instead of
10+
/// referenced. You create a `DereferencedDocument`
11+
/// by calling the `locallyDereferenced()` method
12+
/// on an `OpenAPI.Document`.
13+
@dynamicMemberLookup
14+
public struct DereferencedDocument: Equatable {
15+
/// The original OpenAPI document prior to being
16+
/// dereferenced.
17+
public let underlyingDocument: OpenAPI.Document
18+
19+
/// This property maps the path of each route (`OpenAPI.Path`) to the
20+
/// documentation for that route (`DereferencedPathItem`).
21+
public let paths: DereferencedPathItem.Map
22+
23+
/// A declaration of which security mechanisms can be used across the API.
24+
///
25+
/// The list of values includes alternative security requirement objects that can
26+
/// be used. Only one of the security requirement objects need to be satisfied
27+
/// to authorize a request. Individual operations can override this definition.
28+
///
29+
/// To make security optional, an empty security requirement can be included
30+
/// in the array.
31+
public let security: [DereferencedSecurityRequirement]
32+
33+
public subscript<T>(dynamicMember path: KeyPath<OpenAPI.Document, T>) -> T {
34+
return underlyingDocument[keyPath: path]
35+
}
36+
37+
/// Create a `DereferencedDocument` if all references in the
38+
/// document can be found in its Components Object.
39+
///
40+
/// - Important: This only attempts to dereference components in the
41+
/// Components Object. Any references pointing to other files or other
42+
/// locations in the same file will `throw`.
43+
///
44+
/// - Throws: `ReferenceError.cannotLookupRemoteReference` or
45+
/// `MissingReferenceError.referenceMissingOnLookup(name:)` depending
46+
/// on whether an unresolvable reference points to another file or just points to a
47+
/// component in the same file that cannot be found in the Components Object.
48+
internal init(_ document: OpenAPI.Document) throws {
49+
self.paths = try document.paths.mapValues {
50+
try DereferencedPathItem(
51+
$0,
52+
resolvingIn: document.components
53+
)
54+
}
55+
self.security = try document.security.map {
56+
try DereferencedSecurityRequirement(
57+
$0,
58+
resolvingIn: document.components
59+
)
60+
}
61+
62+
self.underlyingDocument = document
63+
}
64+
}
65+
66+
extension DereferencedDocument {
67+
/// The pairing of a path and the path item that describes the
68+
/// route at that path.
69+
public struct Route: Equatable {
70+
public let path: OpenAPI.Path
71+
public let pathItem: DereferencedPathItem
72+
73+
public init(
74+
path: OpenAPI.Path,
75+
pathItem: DereferencedPathItem
76+
) {
77+
self.path = path
78+
self.pathItem = pathItem
79+
}
80+
}
81+
82+
/// Get an array of all routes in the document. A route is
83+
/// the pairing of a path and the path item that describes the
84+
/// route at that path.
85+
public var routes: [Route] {
86+
return paths.map { (path, pathItem) in .init(path: path, pathItem: pathItem) }
87+
}
88+
}
89+
90+
extension DereferencedDocument {
91+
/// Resolve the document's routes and endpoints.
92+
///
93+
/// OpenAPI allows routes and endpoints to take on things like
94+
/// servers, parameters, and security requirements from
95+
/// various different locations in the `OpenAPI.Document`. A
96+
/// `ResolvedDocument` offers access to canonical routes
97+
/// and endpoints that collect and self-contain all necessary
98+
/// information about the given component.
99+
///
100+
/// **Example**
101+
///
102+
/// A particular `GET` endpoint takes its security
103+
/// requirements from the root OpenAPI `security`
104+
/// array, it takes a path item parameter from the `PathItem` it
105+
/// resides within, and it defines an additional query parameter.
106+
///
107+
/// The `ResolvedEndpoint` exposed by the `ResolvedDocument`
108+
/// will have the inherited security in its `security` array and it will have
109+
/// both the path and query parameters in its `parameters` array.
110+
public func resolved() -> ResolvedDocument {
111+
return ResolvedDocument(dereferencedDocument: self)
112+
}
113+
}

0 commit comments

Comments
 (0)