Skip to content

Commit 8e20f7d

Browse files
authored
Merge pull request #433 from mattpolzin/feature/oas-conversion-userinfo
Add support for mapping newer OAS versions to older ones
2 parents 6a4bcdc + 454e0e9 commit 8e20f7d

File tree

5 files changed

+113
-6
lines changed

5 files changed

+113
-6
lines changed

README.md

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

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

Sources/OpenAPIKit/CodableVendorExtendable.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,11 +29,11 @@ public protocol VendorExtendable {
2929
public enum VendorExtensionsConfiguration {
3030
public static let enabledKey: CodingUserInfoKey = .init(rawValue: "vendor-extensions-enabled")!
3131

32-
static func isEnabled(for decoder: Decoder) -> Bool {
32+
internal static func isEnabled(for decoder: Decoder) -> Bool {
3333
decoder.userInfo[enabledKey] as? Bool ?? true
3434
}
3535

36-
static func isEnabled(for encoder: Encoder) -> Bool {
36+
internal static func isEnabled(for encoder: Encoder) -> Bool {
3737
encoder.userInfo[enabledKey] as? Bool ?? true
3838
}
3939
}

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, Sendable {
48+
public struct Document: HasWarnings, CodableVendorExtendable, Sendable {
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_1,
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+
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
@@ -470,6 +489,30 @@ extension OpenAPI.Document {
470489
}
471490
}
472491

492+
/// OpenAPIKit supports some additional Encoder/Decoder configuration above and beyond
493+
/// what the Encoder or Decoder support out of box.
494+
///
495+
/// To instruct OpenAPIKit to decode OpenAPI Standards versions it does not
496+
/// natively support, set `userInfo[DocumentConfiguration.versionMapKey] =
497+
/// ["3.5.0": OpenAPI.Document.Version.v3_1_1]`.
498+
///
499+
/// That will cause OpenAPIKit to accept OAS v3.5.0 on decode and treat it as
500+
/// the natively supported v3.1.1. This feature exists to allow OpenAPIKit to
501+
/// be configured to parse future versions of the OAS standard that are
502+
/// determined (by you) to be backwards compatible with a previous version
503+
/// prior to OpenAPIKit gaining official support for the new version and its
504+
/// features.
505+
public enum DocumentConfiguration {
506+
public static let versionMapKey: CodingUserInfoKey = .init(rawValue: "document-version-map")!
507+
508+
internal static func version(for decoder: Decoder, versionString: String) -> OpenAPI.Document.Version? {
509+
guard let map = decoder.userInfo[versionMapKey] as? [String: OpenAPI.Document.Version]
510+
else { return nil }
511+
512+
return map[versionString]
513+
}
514+
}
515+
473516
// MARK: - Codable
474517

475518
extension OpenAPI.Document: Encodable {
@@ -516,7 +559,26 @@ extension OpenAPI.Document: Decodable {
516559
let container = try decoder.container(keyedBy: CodingKeys.self)
517560

518561
do {
519-
openAPIVersion = try container.decode(OpenAPI.Document.Version.self, forKey: .openAPIVersion)
562+
let decodedVersion = try container.decode(String.self, forKey: .openAPIVersion)
563+
564+
var warnings = [Warning]()
565+
566+
if let version = OpenAPI.Document.Version(rawValue: decodedVersion) {
567+
openAPIVersion = version
568+
} else if let version = DocumentConfiguration.version(for: decoder, versionString: decodedVersion) {
569+
openAPIVersion = version
570+
571+
warnings.append(.message(
572+
"Document Version \(decodedVersion) is being decoded as version \(version.rawValue). Not all features of OAS \(decodedVersion) will be supported"
573+
))
574+
} else {
575+
throw GenericError(
576+
subjectName: OpenAPI.Document.CodingKeys.openAPIVersion.stringValue,
577+
details: "Failed to parse Document Version \(decodedVersion) as one of OpenAPIKit's supported options",
578+
codingPath: container.codingPath + [OpenAPI.Document.CodingKeys.openAPIVersion]
579+
)
580+
}
581+
520582
info = try container.decode(OpenAPI.Document.Info.self, forKey: .info)
521583
servers = try container.decodeIfPresent([OpenAPI.Server].self, forKey: .servers) ?? []
522584

@@ -535,6 +597,8 @@ extension OpenAPI.Document: Decodable {
535597
externalDocs = try container.decodeIfPresent(OpenAPI.ExternalDocumentation.self, forKey: .externalDocs)
536598
vendorExtensions = try Self.extensions(from: decoder)
537599

600+
self.warnings = warnings
601+
538602
} catch let error as OpenAPI.Error.Decoding.Path {
539603

540604
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, "Problem encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value null.")
47+
XCTAssertEqual(openAPIError.localizedDescription, "Problem 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: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -585,7 +585,28 @@ extension DocumentTests {
585585
}
586586
}
587587
""".data(using: .utf8)!
588-
XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Problem encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value 3.1.9.") }
588+
XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Problem encountered when parsing `openapi` in the root Document object: Failed to parse Document Version 3.1.9 as one of OpenAPIKit's supported options.") }
589+
}
590+
591+
func test_unsupportedButMappedOpenAPIVersion_decode() throws {
592+
let documentData =
593+
"""
594+
{
595+
"info" : {
596+
"title" : "API",
597+
"version" : "1.0"
598+
},
599+
"openapi" : "3.100.100",
600+
"paths" : {
601+
602+
}
603+
}
604+
""".data(using: .utf8)!
605+
let userInfo = [
606+
DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
607+
]
608+
let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData, userInfo: userInfo)
609+
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"])
589610
}
590611

591612
func test_specifyServers_encode() throws {

0 commit comments

Comments
 (0)