Skip to content

Commit

Permalink
Merge pull request #397 from mattpolzin/feature/395/new-oas-versions
Browse files Browse the repository at this point in the history
Support future OAS versions without breaking enum changes
  • Loading branch information
mattpolzin authored Feb 17, 2025
2 parents 927cb61 + 70fcb70 commit 3c34a3f
Show file tree
Hide file tree
Showing 5 changed files with 250 additions and 13 deletions.
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

0 comments on commit 3c34a3f

Please sign in to comment.