Skip to content

Commit 343b2c1

Browse files
authored
Merge pull request #434 from mattpolzin/backport/oas-conversion-userinfo
Backport of #433 (Add support for mapping newer OAS versions to older ones)
2 parents c1dcd65 + cf8bc7a commit 343b2c1

File tree

5 files changed

+114
-5
lines changed

5 files changed

+114
-5
lines changed

README.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,28 @@ let decoder = ... // JSONDecoder() or YAMLDecoder()
8585
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
8686
```
8787

88+
#### Decoding Future Versions
89+
`OpenAPIKit` adds support for new OAS versions when it has support for most or
90+
all of the features of that OAS version. If you want to parse an OpenAPI
91+
Document that is written in a newer version than `OpenAPIKit` supports and you
92+
are asserting that the newer version is possible to parse as if it were the
93+
pre-existing version, you can tell `OpenAPIKit` to parse the newer version as if
94+
it were the older version.
95+
96+
You do this with `userInfo` passed into the `Decoder` you are using. For
97+
example, to decode a hypothetical document version of `"3.100.100"` as if it
98+
were version `"3.1.1"`, set your decoder up as follows:
99+
```swift
100+
let userInfo = [
101+
DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
102+
]
103+
104+
let decoder = ... // JSONDecoder() or YAMLDecoder()
105+
decoder.userInfo = userInfo
106+
107+
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
108+
```
109+
88110
#### Decoding Errors
89111
You can wrap any error you get back from a decoder in `OpenAPI.Error` to get a friendlier human-readable description from `localizedDescription`.
90112

Sources/OpenAPIKit/Document/Document.swift

Lines changed: 66 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension OpenAPI {
4545
///
4646
/// See the documentation on `DereferencedDocument.resolved()` for more.
4747
///
48-
public struct Document: Equatable, CodableVendorExtendable {
48+
public struct Document: HasWarnings, CodableVendorExtendable {
4949
/// OpenAPI Spec "openapi" field.
5050
///
5151
/// OpenAPIKit only explicitly supports versions that can be found in
@@ -141,6 +141,8 @@ extension OpenAPI {
141141
/// where the values are anything codable.
142142
public var vendorExtensions: [String: AnyCodable]
143143

144+
public let warnings: [Warning]
145+
144146
public init(
145147
openAPIVersion: Version = .v3_1_0,
146148
info: Info,
@@ -163,10 +165,27 @@ extension OpenAPI {
163165
self.tags = tags
164166
self.externalDocs = externalDocs
165167
self.vendorExtensions = vendorExtensions
168+
169+
self.warnings = []
166170
}
167171
}
168172
}
169173

174+
extension OpenAPI.Document: Equatable {
175+
public static func == (lhs: Self, rhs: Self) -> Bool {
176+
return lhs.openAPIVersion == rhs.openAPIVersion
177+
&& lhs.info == rhs.info
178+
&& lhs.servers == rhs.servers
179+
&& lhs.paths == rhs.paths
180+
&& lhs.components == rhs.components
181+
&& lhs.webhooks == rhs.webhooks
182+
&& lhs.security == rhs.security
183+
&& lhs.tags == rhs.tags
184+
&& lhs.externalDocs == rhs.externalDocs
185+
&& lhs.vendorExtensions == rhs.vendorExtensions
186+
}
187+
}
188+
170189
extension OpenAPI.Document {
171190
/// Create a new OpenAPI Document with
172191
/// all paths not passign the given predicate
@@ -380,6 +399,30 @@ extension OpenAPI.Document {
380399
}
381400
}
382401

402+
/// OpenAPIKit supports some additional Encoder/Decoder configuration above and beyond
403+
/// what the Encoder or Decoder support out of box.
404+
///
405+
/// To instruct OpenAPIKit to decode OpenAPI Standards versions it does not
406+
/// natively support, set `userInfo[DocumentConfiguration.versionMapKey] =
407+
/// ["3.5.0": OpenAPI.Document.Version.v3_1_1]`.
408+
///
409+
/// That will cause OpenAPIKit to accept OAS v3.5.0 on decode and treat it as
410+
/// the natively supported v3.1.1. This feature exists to allow OpenAPIKit to
411+
/// be configured to parse future versions of the OAS standard that are
412+
/// determined (by you) to be backwards compatible with a previous version
413+
/// prior to OpenAPIKit gaining official support for the new version and its
414+
/// features.
415+
public enum DocumentConfiguration {
416+
public static let versionMapKey = CodingUserInfoKey(rawValue: "document-version-map")!
417+
418+
internal static func version(for decoder: Decoder, versionString: String) -> OpenAPI.Document.Version? {
419+
guard let map = decoder.userInfo[versionMapKey] as? [String: OpenAPI.Document.Version]
420+
else { return nil }
421+
422+
return map[versionString]
423+
}
424+
}
425+
383426
// MARK: - Codable
384427

385428
extension OpenAPI.Document: Encodable {
@@ -424,7 +467,26 @@ extension OpenAPI.Document: Decodable {
424467
let container = try decoder.container(keyedBy: CodingKeys.self)
425468

426469
do {
427-
openAPIVersion = try container.decode(OpenAPI.Document.Version.self, forKey: .openAPIVersion)
470+
let decodedVersion = try container.decode(String.self, forKey: .openAPIVersion)
471+
472+
var warnings = [Warning]()
473+
474+
if let version = OpenAPI.Document.Version(rawValue: decodedVersion) {
475+
openAPIVersion = version
476+
} else if let version = DocumentConfiguration.version(for: decoder, versionString: decodedVersion) {
477+
openAPIVersion = version
478+
479+
warnings.append(.message(
480+
"Document Version \(decodedVersion) is being decoded as version \(version.rawValue). Not all features of OAS \(decodedVersion) will be supported"
481+
))
482+
} else {
483+
throw InconsistencyError(
484+
subjectName: OpenAPI.Document.CodingKeys.openAPIVersion.stringValue,
485+
details: "Failed to parse Document Version \(decodedVersion) as one of OpenAPIKit's supported options",
486+
codingPath: container.codingPath + [OpenAPI.Document.CodingKeys.openAPIVersion]
487+
)
488+
}
489+
428490
info = try container.decode(OpenAPI.Document.Info.self, forKey: .info)
429491
servers = try container.decodeIfPresent([OpenAPI.Server].self, forKey: .servers) ?? []
430492

@@ -443,6 +505,8 @@ extension OpenAPI.Document: Decodable {
443505
externalDocs = try container.decodeIfPresent(OpenAPI.ExternalDocumentation.self, forKey: .externalDocs)
444506
vendorExtensions = try Self.extensions(from: decoder)
445507

508+
self.warnings = warnings
509+
446510
} catch let error as OpenAPI.Error.Decoding.Path {
447511

448512
throw OpenAPI.Error.Decoding.Document(error)

Tests/OpenAPIKitErrorReportingTests/DocumentErrorTests.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ final class DocumentErrorTests: XCTestCase {
4444

4545
let openAPIError = OpenAPI.Error(from: error)
4646

47-
XCTAssertEqual(openAPIError.localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value null.")
47+
XCTAssertEqual(openAPIError.localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Failed to parse Document Version null as one of OpenAPIKit's supported options.")
4848
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
4949
"openapi"
5050
])

Tests/OpenAPIKitTests/Document/DocumentTests.swift

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -520,6 +520,27 @@ extension DocumentTests {
520520
)
521521
}
522522

523+
func test_unsupportedButMappedOpenAPIVersion_decode() throws {
524+
let documentData =
525+
"""
526+
{
527+
"info" : {
528+
"title" : "API",
529+
"version" : "1.0"
530+
},
531+
"openapi" : "3.100.100",
532+
"paths" : {
533+
534+
}
535+
}
536+
""".data(using: .utf8)!
537+
let userInfo = [
538+
DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
539+
]
540+
let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData, userInfo: userInfo)
541+
XCTAssertEqual(document.warnings.map { $0.localizedDescription }, ["Document Version 3.100.100 is being decoded as version 3.1.1. Not all features of OAS 3.100.100 will be supported"])
542+
}
543+
523544
func test_specifyServers_encode() throws {
524545
let document = OpenAPI.Document(
525546
info: .init(title: "API", version: "1.0"),

Tests/OpenAPIKitTests/TestHelpers.swift

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,8 +53,10 @@ fileprivate let foundationTestDecoder = { () -> JSONDecoder in
5353
return decoder
5454
}()
5555

56-
func orderUnstableDecode<T: Decodable>(_ type: T.Type, from data: Data) throws -> T {
57-
return try foundationTestDecoder.decode(T.self, from: data)
56+
func orderUnstableDecode<T: Decodable>(_ type: T.Type, from data: Data, userInfo: [CodingUserInfoKey: Any] = [:]) throws -> T {
57+
let decoder = foundationTestDecoder
58+
decoder.userInfo = userInfo
59+
return try decoder.decode(T.self, from: data)
5860
}
5961

6062
fileprivate let yamsTestDecoder = { () -> YAMLDecoder in

0 commit comments

Comments
 (0)