From dca99b7111e6c18a36ed6d0b4f773b03fa36fe36 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2025 14:05:54 -0600 Subject: [PATCH 1/4] make it possible to support additional future versions of OAS without a breaking change to an enumeration --- Sources/OpenAPIKit/Document/Document.swift | 34 ++++++++++++-- Sources/OpenAPIKit30/Document/Document.swift | 46 ++++++++++++++++--- .../Document/DocumentTests.swift | 38 +++++++++++++++ .../Document/DocumentTests.swift | 20 ++++++++ 4 files changed, 129 insertions(+), 9 deletions(-) diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 73513284e..378991237 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -424,9 +424,37 @@ 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" + 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 + } + 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)" + } + } } } diff --git a/Sources/OpenAPIKit30/Document/Document.swift b/Sources/OpenAPIKit30/Document/Document.swift index 672405f3a..c48e22399 100644 --- a/Sources/OpenAPIKit30/Document/Document.swift +++ b/Sources/OpenAPIKit30/Document/Document.swift @@ -408,12 +408,46 @@ 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" + 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 + } + 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)" + } + } } } diff --git a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift index d46d29b52..91bc21d3d 100644 --- a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift @@ -41,6 +41,44 @@ 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) + + let t12 = OpenAPI.Document.Version(rawValue: "3.0.8") + XCTAssertEqual(t12, .v3_0_x(x: 8)) + } + func test_getRoutes() { let pi1 = OpenAPI.PathItem( parameters: [], diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index cbeeb45ec..cb99e21d1 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -41,6 +41,26 @@ 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) + + let t6 = OpenAPI.Document.Version(rawValue: "3.1.8") + XCTAssertEqual(t6, .v3_1_x(x: 8)) + } + func test_getRoutes() { let pi1 = OpenAPI.PathItem( parameters: [], From ace1ef300d317547773fb4624a8c8e2fb6a1ca16 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2025 14:19:25 -0600 Subject: [PATCH 2/4] change implementation to not support decoding unknown new OAS versions --- Sources/OpenAPIKit/Document/Document.swift | 12 ++++++++++++ Sources/OpenAPIKit30/Document/Document.swift | 14 +++++++++++++- .../OpenAPIKit30Tests/Document/DocumentTests.swift | 3 ++- Tests/OpenAPIKitTests/Document/DocumentTests.swift | 3 ++- 4 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Sources/OpenAPIKit/Document/Document.swift b/Sources/OpenAPIKit/Document/Document.swift index 378991237..8e5ee58df 100644 --- a/Sources/OpenAPIKit/Document/Document.swift +++ b/Sources/OpenAPIKit/Document/Document.swift @@ -424,6 +424,12 @@ 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. + /// + ///**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 @@ -444,6 +450,12 @@ extension OpenAPI.Document { 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) } } diff --git a/Sources/OpenAPIKit30/Document/Document.swift b/Sources/OpenAPIKit30/Document/Document.swift index c48e22399..c4bc3a127 100644 --- a/Sources/OpenAPIKit30/Document/Document.swift +++ b/Sources/OpenAPIKit30/Document/Document.swift @@ -408,7 +408,13 @@ 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: RawRepresentable, Equatable, Codable { + /// + ///**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 @@ -434,6 +440,12 @@ extension OpenAPI.Document { 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) } } diff --git a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift index 91bc21d3d..b4b53607a 100644 --- a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift @@ -75,8 +75,9 @@ final class DocumentTests: XCTestCase { 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") - XCTAssertEqual(t12, .v3_0_x(x: 8)) + XCTAssertNil(t12) } func test_getRoutes() { diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index cb99e21d1..3c396377d 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -57,8 +57,9 @@ final class DocumentTests: XCTestCase { 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") - XCTAssertEqual(t6, .v3_1_x(x: 8)) + XCTAssertNil(t6) } func test_getRoutes() { From c7c7b8b7b5f35096077a984207551a9046f6564b Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2025 14:29:27 -0600 Subject: [PATCH 3/4] test cases --- .../Document/DocumentTests.swift | 44 +++++++++++++++++++ .../Document/DocumentTests.swift | 41 +++++++++++++++++ 2 files changed, 85 insertions(+) diff --git a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift index b4b53607a..53a890bd7 100644 --- a/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKit30Tests/Document/DocumentTests.swift @@ -511,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 = """ @@ -539,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"), diff --git a/Tests/OpenAPIKitTests/Document/DocumentTests.swift b/Tests/OpenAPIKitTests/Document/DocumentTests.swift index 3c396377d..1de075b87 100644 --- a/Tests/OpenAPIKitTests/Document/DocumentTests.swift +++ b/Tests/OpenAPIKitTests/Document/DocumentTests.swift @@ -513,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 = """ @@ -541,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"), From 70fcb701628080752b6f7a13f83ff56ab101b2a8 Mon Sep 17 00:00:00 2001 From: Mathew Polzin Date: Mon, 17 Feb 2025 14:35:17 -0600 Subject: [PATCH 4/4] update migration guide --- documentation/v4_migration_guide.md | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/documentation/v4_migration_guide.md b/documentation/v4_migration_guide.md index 0632ecb5f..d4b4a61a5 100644 --- a/documentation/v4_migration_guide.md +++ b/documentation/v4_migration_guide.md @@ -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