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
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,28 @@ let decoder = ... // JSONDecoder() or YAMLDecoder()
let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
```

#### Decoding Future Versions
`OpenAPIKit` adds support for new OAS versions when it has support for most or
all of the features of that OAS version. If you want to parse an OpenAPI
Document that is written in a newer version than `OpenAPIKit` supports and you
are asserting that the newer version is possible to parse as if it were the
pre-existing version, you can tell `OpenAPIKit` to parse the newer version as if
it were the older version.

You do this with `userInfo` passed into the `Decoder` you are using. For
example, to decode a hypothetical document version of `"3.100.100"` as if it
were version `"3.1.1"`, set your decoder up as follows:
```swift
let userInfo = [
DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
]

let decoder = ... // JSONDecoder() or YAMLDecoder()
decoder.userInfo = userInfo

let openAPIDoc = try decoder.decode(OpenAPI.Document.self, from: ...)
```

#### Decoding Errors
You can wrap any error you get back from a decoder in `OpenAPI.Error` to get a friendlier human-readable description from `localizedDescription`.

Expand Down
68 changes: 66 additions & 2 deletions Sources/OpenAPIKit/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension OpenAPI {
///
/// See the documentation on `DereferencedDocument.resolved()` for more.
///
public struct Document: Equatable, CodableVendorExtendable {
public struct Document: HasWarnings, CodableVendorExtendable {
/// OpenAPI Spec "openapi" field.
///
/// OpenAPIKit only explicitly supports versions that can be found in
Expand Down Expand Up @@ -141,6 +141,8 @@ extension OpenAPI {
/// where the values are anything codable.
public var vendorExtensions: [String: AnyCodable]

public let warnings: [Warning]

public init(
openAPIVersion: Version = .v3_1_0,
info: Info,
Expand All @@ -163,10 +165,27 @@ extension OpenAPI {
self.tags = tags
self.externalDocs = externalDocs
self.vendorExtensions = vendorExtensions

self.warnings = []
}
}
}

extension OpenAPI.Document: Equatable {
public static func == (lhs: Self, rhs: Self) -> Bool {
return lhs.openAPIVersion == rhs.openAPIVersion
&& lhs.info == rhs.info
&& lhs.servers == rhs.servers
&& lhs.paths == rhs.paths
&& lhs.components == rhs.components
&& lhs.webhooks == rhs.webhooks
&& lhs.security == rhs.security
&& lhs.tags == rhs.tags
&& lhs.externalDocs == rhs.externalDocs
&& lhs.vendorExtensions == rhs.vendorExtensions
}
}

extension OpenAPI.Document {
/// Create a new OpenAPI Document with
/// all paths not passign the given predicate
Expand Down Expand Up @@ -380,6 +399,30 @@ extension OpenAPI.Document {
}
}

/// OpenAPIKit supports some additional Encoder/Decoder configuration above and beyond
/// what the Encoder or Decoder support out of box.
///
/// To instruct OpenAPIKit to decode OpenAPI Standards versions it does not
/// natively support, set `userInfo[DocumentConfiguration.versionMapKey] =
/// ["3.5.0": OpenAPI.Document.Version.v3_1_1]`.
///
/// That will cause OpenAPIKit to accept OAS v3.5.0 on decode and treat it as
/// the natively supported v3.1.1. This feature exists to allow OpenAPIKit to
/// be configured to parse future versions of the OAS standard that are
/// determined (by you) to be backwards compatible with a previous version
/// prior to OpenAPIKit gaining official support for the new version and its
/// features.
public enum DocumentConfiguration {
public static let versionMapKey = CodingUserInfoKey(rawValue: "document-version-map")!

internal static func version(for decoder: Decoder, versionString: String) -> OpenAPI.Document.Version? {
guard let map = decoder.userInfo[versionMapKey] as? [String: OpenAPI.Document.Version]
else { return nil }

return map[versionString]
}
}

// MARK: - Codable

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

do {
openAPIVersion = try container.decode(OpenAPI.Document.Version.self, forKey: .openAPIVersion)
let decodedVersion = try container.decode(String.self, forKey: .openAPIVersion)

var warnings = [Warning]()

if let version = OpenAPI.Document.Version(rawValue: decodedVersion) {
openAPIVersion = version
} else if let version = DocumentConfiguration.version(for: decoder, versionString: decodedVersion) {
openAPIVersion = version

warnings.append(.message(
"Document Version \(decodedVersion) is being decoded as version \(version.rawValue). Not all features of OAS \(decodedVersion) will be supported"
))
} else {
throw InconsistencyError(
subjectName: OpenAPI.Document.CodingKeys.openAPIVersion.stringValue,
details: "Failed to parse Document Version \(decodedVersion) as one of OpenAPIKit's supported options",
codingPath: container.codingPath + [OpenAPI.Document.CodingKeys.openAPIVersion]
)
}

info = try container.decode(OpenAPI.Document.Info.self, forKey: .info)
servers = try container.decodeIfPresent([OpenAPI.Server].self, forKey: .servers) ?? []

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

self.warnings = warnings

} catch let error as OpenAPI.Error.Decoding.Path {

throw OpenAPI.Error.Decoding.Document(error)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ final class DocumentErrorTests: XCTestCase {

let openAPIError = OpenAPI.Error(from: error)

XCTAssertEqual(openAPIError.localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value null.")
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.")
XCTAssertEqual(openAPIError.codingPath.map { $0.stringValue }, [
"openapi"
])
Expand Down
21 changes: 21 additions & 0 deletions Tests/OpenAPIKitTests/Document/DocumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -520,6 +520,27 @@ extension DocumentTests {
)
}

func test_unsupportedButMappedOpenAPIVersion_decode() throws {
let documentData =
"""
{
"info" : {
"title" : "API",
"version" : "1.0"
},
"openapi" : "3.100.100",
"paths" : {

}
}
""".data(using: .utf8)!
let userInfo = [
DocumentConfiguration.versionMapKey: ["3.100.100": OpenAPI.Document.Version.v3_1_1]
]
let document = try orderUnstableDecode(OpenAPI.Document.self, from: documentData, userInfo: userInfo)
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"])
}

func test_specifyServers_encode() throws {
let document = OpenAPI.Document(
info: .init(title: "API", version: "1.0"),
Expand Down
6 changes: 4 additions & 2 deletions Tests/OpenAPIKitTests/TestHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,10 @@ fileprivate let foundationTestDecoder = { () -> JSONDecoder in
return decoder
}()

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

fileprivate let yamsTestDecoder = { () -> YAMLDecoder in
Expand Down