Skip to content

Support future OAS versions without breaking enum changes #397

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 5 commits into from
Feb 17, 2025
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
46 changes: 43 additions & 3 deletions Sources/OpenAPIKit/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -424,9 +424,49 @@ extension OpenAPI.Document {
/// OpenAPIKit only explicitly supports versions that can be found in
/// this enum. Other versions may or may not be decodable by
/// OpenAPIKit to a certain extent.
public enum Version: String, Codable {
case v3_1_0 = "3.1.0"
case v3_1_1 = "3.1.1"
///
///**IMPORTANT**: Although the `v3_1_x` case supports arbitrary
/// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI
/// specification releases a new patch version, OpenAPIKit will see a patch version release
/// explicitly supports decoding documents of that new patch version before said version will
/// succesfully decode as the `v3_1_x` case.
public enum Version: RawRepresentable, Equatable, Codable {
case v3_1_0
case v3_1_1
case v3_1_x(x: Int)

public init?(rawValue: String) {
switch rawValue {
case "3.1.0": self = .v3_1_0
case "3.1.1": self = .v3_1_1
default:
let components = rawValue.split(separator: ".")
guard components.count == 3 else {
return nil
}
guard components[0] == "3", components[1] == "1" else {
return nil
}
guard let patchVersion = Int(components[2], radix: 10) else {
return nil
}
// to support newer versions released in the future without a breaking
// change to the enumeration, bump the upper limit here to e.g. 2 or 3
// or 6:
guard patchVersion > 1 && patchVersion <= 1 else {
return nil
}
self = .v3_1_x(x: patchVersion)
}
}

public var rawValue: String {
switch self {
case .v3_1_0: return "3.1.0"
case .v3_1_1: return "3.1.1"
case .v3_1_x(x: let x): return "3.1.\(x)"
}
}
}
}

Expand Down
58 changes: 52 additions & 6 deletions Sources/OpenAPIKit30/Document/Document.swift
Original file line number Diff line number Diff line change
Expand Up @@ -408,12 +408,58 @@ extension OpenAPI.Document {
/// OpenAPIKit only explicitly supports versions that can be found in
/// this enum. Other versions may or may not be decodable by
/// OpenAPIKit to a certain extent.
public enum Version: String, Codable {
case v3_0_0 = "3.0.0"
case v3_0_1 = "3.0.1"
case v3_0_2 = "3.0.2"
case v3_0_3 = "3.0.3"
case v3_0_4 = "3.0.4"
///
///**IMPORTANT**: Although the `v3_0_x` case supports arbitrary
/// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI
/// specification releases a new patch version, OpenAPIKit will see a patch version release
/// explicitly supports decoding documents of that new patch version before said version will
/// succesfully decode as the `v3_0_x` case.
public enum Version: RawRepresentable, Equatable, Codable {
case v3_0_0
case v3_0_1
case v3_0_2
case v3_0_3
case v3_0_4
case v3_0_x(x: Int)

public init?(rawValue: String) {
switch rawValue {
case "3.0.0": self = .v3_0_0
case "3.0.1": self = .v3_0_1
case "3.0.2": self = .v3_0_2
case "3.0.3": self = .v3_0_3
case "3.0.4": self = .v3_0_4
default:
let components = rawValue.split(separator: ".")
guard components.count == 3 else {
return nil
}
guard components[0] == "3", components[1] == "0" else {
return nil
}
guard let patchVersion = Int(components[2], radix: 10) else {
return nil
}
// to support newer versions released in the future without a breaking
// change to the enumeration, bump the upper limit here to e.g. 5 or 6
// or 9:
guard patchVersion > 4 && patchVersion <= 4 else {
return nil
}
self = .v3_0_x(x: patchVersion)
}
}

public var rawValue: String {
switch self {
case .v3_0_0: return "3.0.0"
case .v3_0_1: return "3.0.1"
case .v3_0_2: return "3.0.2"
case .v3_0_3: return "3.0.3"
case .v3_0_4: return "3.0.4"
case .v3_0_x(x: let x): return "3.0.\(x)"
}
}
}
}

Expand Down
83 changes: 83 additions & 0 deletions Tests/OpenAPIKit30Tests/Document/DocumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,45 @@ final class DocumentTests: XCTestCase {
)
}

func test_initOASVersions() {
let t1 = OpenAPI.Document.Version.v3_0_0
XCTAssertEqual(t1.rawValue, "3.0.0")

let t2 = OpenAPI.Document.Version.v3_0_1
XCTAssertEqual(t2.rawValue, "3.0.1")

let t3 = OpenAPI.Document.Version.v3_0_2
XCTAssertEqual(t3.rawValue, "3.0.2")

let t4 = OpenAPI.Document.Version.v3_0_3
XCTAssertEqual(t4.rawValue, "3.0.3")

let t5 = OpenAPI.Document.Version.v3_0_4
XCTAssertEqual(t5.rawValue, "3.0.4")

let t6 = OpenAPI.Document.Version.v3_0_x(x: 8)
XCTAssertEqual(t6.rawValue, "3.0.8")

let t7 = OpenAPI.Document.Version(rawValue: "3.0.0")
XCTAssertEqual(t7, .v3_0_0)

let t8 = OpenAPI.Document.Version(rawValue: "3.0.1")
XCTAssertEqual(t8, .v3_0_1)

let t9 = OpenAPI.Document.Version(rawValue: "3.0.2")
XCTAssertEqual(t9, .v3_0_2)

let t10 = OpenAPI.Document.Version(rawValue: "3.0.3")
XCTAssertEqual(t10, .v3_0_3)

let t11 = OpenAPI.Document.Version(rawValue: "3.0.4")
XCTAssertEqual(t11, .v3_0_4)

// not a known version:
let t12 = OpenAPI.Document.Version(rawValue: "3.0.8")
XCTAssertNil(t12)
}

func test_getRoutes() {
let pi1 = OpenAPI.PathItem(
parameters: [],
Expand Down Expand Up @@ -472,6 +511,33 @@ extension DocumentTests {
)
}

func test_specifyUknownOpenAPIVersion_encode() throws {
let document = OpenAPI.Document(
openAPIVersion: .v3_0_x(x: 9),
info: .init(title: "API", version: "1.0"),
servers: [],
paths: [:],
components: .noComponents
)
let encodedDocument = try orderUnstableTestStringFromEncoding(of: document)

assertJSONEquivalent(
encodedDocument,
"""
{
"info" : {
"title" : "API",
"version" : "1.0"
},
"openapi" : "3.0.9",
"paths" : {

}
}
"""
)
}

func test_specifyOpenAPIVersion_decode() throws {
let documentData =
"""
Expand Down Expand Up @@ -500,6 +566,23 @@ extension DocumentTests {
)
}

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

}
}
""".data(using: .utf8)!
XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value 3.0.9.") }
}

func test_specifyServers_encode() throws {
let document = OpenAPI.Document(
info: .init(title: "API", version: "1.0"),
Expand Down
62 changes: 62 additions & 0 deletions Tests/OpenAPIKitTests/Document/DocumentTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,27 @@ final class DocumentTests: XCTestCase {
)
}

func test_initOASVersions() {
let t1 = OpenAPI.Document.Version.v3_1_0
XCTAssertEqual(t1.rawValue, "3.1.0")

let t2 = OpenAPI.Document.Version.v3_1_1
XCTAssertEqual(t2.rawValue, "3.1.1")

let t3 = OpenAPI.Document.Version.v3_1_x(x: 8)
XCTAssertEqual(t3.rawValue, "3.1.8")

let t4 = OpenAPI.Document.Version(rawValue: "3.1.0")
XCTAssertEqual(t4, .v3_1_0)

let t5 = OpenAPI.Document.Version(rawValue: "3.1.1")
XCTAssertEqual(t5, .v3_1_1)

// not a known version:
let t6 = OpenAPI.Document.Version(rawValue: "3.1.8")
XCTAssertNil(t6)
}

func test_getRoutes() {
let pi1 = OpenAPI.PathItem(
parameters: [],
Expand Down Expand Up @@ -492,6 +513,30 @@ extension DocumentTests {
)
}

func test_specifyUknownOpenAPIVersion_encode() throws {
let document = OpenAPI.Document(
openAPIVersion: .v3_1_x(x: 9),
info: .init(title: "API", version: "1.0"),
servers: [],
paths: [:],
components: .noComponents
)
let encodedDocument = try orderUnstableTestStringFromEncoding(of: document)

assertJSONEquivalent(
encodedDocument,
"""
{
"info" : {
"title" : "API",
"version" : "1.0"
},
"openapi" : "3.1.9"
}
"""
)
}

func test_specifyOpenAPIVersion_decode() throws {
let documentData =
"""
Expand Down Expand Up @@ -520,6 +565,23 @@ extension DocumentTests {
)
}

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

}
}
""".data(using: .utf8)!
XCTAssertThrowsError(try orderUnstableDecode(OpenAPI.Document.self, from: documentData)) { error in XCTAssertEqual(OpenAPI.Error(from: error).localizedDescription, "Inconsistency encountered when parsing `openapi` in the root Document object: Cannot initialize Version from invalid String value 3.1.9.") }
}

func test_specifyServers_encode() throws {
let document = OpenAPI.Document(
info: .init(title: "API", version: "1.0"),
Expand Down
14 changes: 10 additions & 4 deletions documentation/v4_migration_guide.md
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,16 @@ is now required.
Only relevant when compiling OpenAPIKit on macOS: Now v10_15+ is required.

### OpenAPI Specification Versions
The `OpenAPIKit.Document.Version` enum gained `v3_1_1` and the
`OpenAPIKit30.Document.Version` enum gained `v3_0_4`. If you have exhaustive
switches over values of those types then your switch statements will need to be
updated.
The OpenAPIKit module's `OpenAPI.Document.Version` enum gained `v3_1_1` and the
OpenAPIKit30 module's `OpenAPI.Document.Version` enum gained `v3_0_4`.

The `OpenAPI.Document.Version` enum in both modules gained a new case
(`v3_0_x(x: Int)` and `v3_1_x(x: Int)` respectively) that represents future OAS
versions not released at the time of the given OpenAPIKit release. This allows
non-breaking addition of support for those new versions.

If you have exhaustive switches over values of those types then your switch
statements will need to be updated.

### Typo corrections
The following typo corrections were made to OpenAPIKit code. These amount to
Expand Down