Skip to content

Commit 3c34a3f

Browse files
authored
Merge pull request #397 from mattpolzin/feature/395/new-oas-versions
Support future OAS versions without breaking enum changes
2 parents 927cb61 + 70fcb70 commit 3c34a3f

File tree

5 files changed

+250
-13
lines changed

5 files changed

+250
-13
lines changed

Sources/OpenAPIKit/Document/Document.swift

+43-3
Original file line numberDiff line numberDiff line change
@@ -424,9 +424,49 @@ extension OpenAPI.Document {
424424
/// OpenAPIKit only explicitly supports versions that can be found in
425425
/// this enum. Other versions may or may not be decodable by
426426
/// OpenAPIKit to a certain extent.
427-
public enum Version: String, Codable {
428-
case v3_1_0 = "3.1.0"
429-
case v3_1_1 = "3.1.1"
427+
///
428+
///**IMPORTANT**: Although the `v3_1_x` case supports arbitrary
429+
/// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI
430+
/// specification releases a new patch version, OpenAPIKit will see a patch version release
431+
/// explicitly supports decoding documents of that new patch version before said version will
432+
/// succesfully decode as the `v3_1_x` case.
433+
public enum Version: RawRepresentable, Equatable, Codable {
434+
case v3_1_0
435+
case v3_1_1
436+
case v3_1_x(x: Int)
437+
438+
public init?(rawValue: String) {
439+
switch rawValue {
440+
case "3.1.0": self = .v3_1_0
441+
case "3.1.1": self = .v3_1_1
442+
default:
443+
let components = rawValue.split(separator: ".")
444+
guard components.count == 3 else {
445+
return nil
446+
}
447+
guard components[0] == "3", components[1] == "1" else {
448+
return nil
449+
}
450+
guard let patchVersion = Int(components[2], radix: 10) else {
451+
return nil
452+
}
453+
// to support newer versions released in the future without a breaking
454+
// change to the enumeration, bump the upper limit here to e.g. 2 or 3
455+
// or 6:
456+
guard patchVersion > 1 && patchVersion <= 1 else {
457+
return nil
458+
}
459+
self = .v3_1_x(x: patchVersion)
460+
}
461+
}
462+
463+
public var rawValue: String {
464+
switch self {
465+
case .v3_1_0: return "3.1.0"
466+
case .v3_1_1: return "3.1.1"
467+
case .v3_1_x(x: let x): return "3.1.\(x)"
468+
}
469+
}
430470
}
431471
}
432472

Sources/OpenAPIKit30/Document/Document.swift

+52-6
Original file line numberDiff line numberDiff line change
@@ -408,12 +408,58 @@ extension OpenAPI.Document {
408408
/// OpenAPIKit only explicitly supports versions that can be found in
409409
/// this enum. Other versions may or may not be decodable by
410410
/// OpenAPIKit to a certain extent.
411-
public enum Version: String, Codable {
412-
case v3_0_0 = "3.0.0"
413-
case v3_0_1 = "3.0.1"
414-
case v3_0_2 = "3.0.2"
415-
case v3_0_3 = "3.0.3"
416-
case v3_0_4 = "3.0.4"
411+
///
412+
///**IMPORTANT**: Although the `v3_0_x` case supports arbitrary
413+
/// patch versions, only _known_ patch versions are decodable. That is, if the OpenAPI
414+
/// specification releases a new patch version, OpenAPIKit will see a patch version release
415+
/// explicitly supports decoding documents of that new patch version before said version will
416+
/// succesfully decode as the `v3_0_x` case.
417+
public enum Version: RawRepresentable, Equatable, Codable {
418+
case v3_0_0
419+
case v3_0_1
420+
case v3_0_2
421+
case v3_0_3
422+
case v3_0_4
423+
case v3_0_x(x: Int)
424+
425+
public init?(rawValue: String) {
426+
switch rawValue {
427+
case "3.0.0": self = .v3_0_0
428+
case "3.0.1": self = .v3_0_1
429+
case "3.0.2": self = .v3_0_2
430+
case "3.0.3": self = .v3_0_3
431+
case "3.0.4": self = .v3_0_4
432+
default:
433+
let components = rawValue.split(separator: ".")
434+
guard components.count == 3 else {
435+
return nil
436+
}
437+
guard components[0] == "3", components[1] == "0" else {
438+
return nil
439+
}
440+
guard let patchVersion = Int(components[2], radix: 10) else {
441+
return nil
442+
}
443+
// to support newer versions released in the future without a breaking
444+
// change to the enumeration, bump the upper limit here to e.g. 5 or 6
445+
// or 9:
446+
guard patchVersion > 4 && patchVersion <= 4 else {
447+
return nil
448+
}
449+
self = .v3_0_x(x: patchVersion)
450+
}
451+
}
452+
453+
public var rawValue: String {
454+
switch self {
455+
case .v3_0_0: return "3.0.0"
456+
case .v3_0_1: return "3.0.1"
457+
case .v3_0_2: return "3.0.2"
458+
case .v3_0_3: return "3.0.3"
459+
case .v3_0_4: return "3.0.4"
460+
case .v3_0_x(x: let x): return "3.0.\(x)"
461+
}
462+
}
417463
}
418464
}
419465

Tests/OpenAPIKit30Tests/Document/DocumentTests.swift

+83
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,45 @@ final class DocumentTests: XCTestCase {
4141
)
4242
}
4343

44+
func test_initOASVersions() {
45+
let t1 = OpenAPI.Document.Version.v3_0_0
46+
XCTAssertEqual(t1.rawValue, "3.0.0")
47+
48+
let t2 = OpenAPI.Document.Version.v3_0_1
49+
XCTAssertEqual(t2.rawValue, "3.0.1")
50+
51+
let t3 = OpenAPI.Document.Version.v3_0_2
52+
XCTAssertEqual(t3.rawValue, "3.0.2")
53+
54+
let t4 = OpenAPI.Document.Version.v3_0_3
55+
XCTAssertEqual(t4.rawValue, "3.0.3")
56+
57+
let t5 = OpenAPI.Document.Version.v3_0_4
58+
XCTAssertEqual(t5.rawValue, "3.0.4")
59+
60+
let t6 = OpenAPI.Document.Version.v3_0_x(x: 8)
61+
XCTAssertEqual(t6.rawValue, "3.0.8")
62+
63+
let t7 = OpenAPI.Document.Version(rawValue: "3.0.0")
64+
XCTAssertEqual(t7, .v3_0_0)
65+
66+
let t8 = OpenAPI.Document.Version(rawValue: "3.0.1")
67+
XCTAssertEqual(t8, .v3_0_1)
68+
69+
let t9 = OpenAPI.Document.Version(rawValue: "3.0.2")
70+
XCTAssertEqual(t9, .v3_0_2)
71+
72+
let t10 = OpenAPI.Document.Version(rawValue: "3.0.3")
73+
XCTAssertEqual(t10, .v3_0_3)
74+
75+
let t11 = OpenAPI.Document.Version(rawValue: "3.0.4")
76+
XCTAssertEqual(t11, .v3_0_4)
77+
78+
// not a known version:
79+
let t12 = OpenAPI.Document.Version(rawValue: "3.0.8")
80+
XCTAssertNil(t12)
81+
}
82+
4483
func test_getRoutes() {
4584
let pi1 = OpenAPI.PathItem(
4685
parameters: [],
@@ -472,6 +511,33 @@ extension DocumentTests {
472511
)
473512
}
474513

514+
func test_specifyUknownOpenAPIVersion_encode() throws {
515+
let document = OpenAPI.Document(
516+
openAPIVersion: .v3_0_x(x: 9),
517+
info: .init(title: "API", version: "1.0"),
518+
servers: [],
519+
paths: [:],
520+
components: .noComponents
521+
)
522+
let encodedDocument = try orderUnstableTestStringFromEncoding(of: document)
523+
524+
assertJSONEquivalent(
525+
encodedDocument,
526+
"""
527+
{
528+
"info" : {
529+
"title" : "API",
530+
"version" : "1.0"
531+
},
532+
"openapi" : "3.0.9",
533+
"paths" : {
534+
535+
}
536+
}
537+
"""
538+
)
539+
}
540+
475541
func test_specifyOpenAPIVersion_decode() throws {
476542
let documentData =
477543
"""
@@ -500,6 +566,23 @@ extension DocumentTests {
500566
)
501567
}
502568

569+
func test_specifyUnknownOpenAPIVersion_decode() throws {
570+
let documentData =
571+
"""
572+
{
573+
"info" : {
574+
"title" : "API",
575+
"version" : "1.0"
576+
},
577+
"openapi" : "3.0.9",
578+
"paths" : {
579+
580+
}
581+
}
582+
""".data(using: .utf8)!
583+
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.") }
584+
}
585+
503586
func test_specifyServers_encode() throws {
504587
let document = OpenAPI.Document(
505588
info: .init(title: "API", version: "1.0"),

Tests/OpenAPIKitTests/Document/DocumentTests.swift

+62
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,27 @@ final class DocumentTests: XCTestCase {
4141
)
4242
}
4343

44+
func test_initOASVersions() {
45+
let t1 = OpenAPI.Document.Version.v3_1_0
46+
XCTAssertEqual(t1.rawValue, "3.1.0")
47+
48+
let t2 = OpenAPI.Document.Version.v3_1_1
49+
XCTAssertEqual(t2.rawValue, "3.1.1")
50+
51+
let t3 = OpenAPI.Document.Version.v3_1_x(x: 8)
52+
XCTAssertEqual(t3.rawValue, "3.1.8")
53+
54+
let t4 = OpenAPI.Document.Version(rawValue: "3.1.0")
55+
XCTAssertEqual(t4, .v3_1_0)
56+
57+
let t5 = OpenAPI.Document.Version(rawValue: "3.1.1")
58+
XCTAssertEqual(t5, .v3_1_1)
59+
60+
// not a known version:
61+
let t6 = OpenAPI.Document.Version(rawValue: "3.1.8")
62+
XCTAssertNil(t6)
63+
}
64+
4465
func test_getRoutes() {
4566
let pi1 = OpenAPI.PathItem(
4667
parameters: [],
@@ -492,6 +513,30 @@ extension DocumentTests {
492513
)
493514
}
494515

516+
func test_specifyUknownOpenAPIVersion_encode() throws {
517+
let document = OpenAPI.Document(
518+
openAPIVersion: .v3_1_x(x: 9),
519+
info: .init(title: "API", version: "1.0"),
520+
servers: [],
521+
paths: [:],
522+
components: .noComponents
523+
)
524+
let encodedDocument = try orderUnstableTestStringFromEncoding(of: document)
525+
526+
assertJSONEquivalent(
527+
encodedDocument,
528+
"""
529+
{
530+
"info" : {
531+
"title" : "API",
532+
"version" : "1.0"
533+
},
534+
"openapi" : "3.1.9"
535+
}
536+
"""
537+
)
538+
}
539+
495540
func test_specifyOpenAPIVersion_decode() throws {
496541
let documentData =
497542
"""
@@ -520,6 +565,23 @@ extension DocumentTests {
520565
)
521566
}
522567

568+
func test_specifyUnknownOpenAPIVersion_decode() throws {
569+
let documentData =
570+
"""
571+
{
572+
"info" : {
573+
"title" : "API",
574+
"version" : "1.0"
575+
},
576+
"openapi" : "3.1.9",
577+
"paths" : {
578+
579+
}
580+
}
581+
""".data(using: .utf8)!
582+
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.") }
583+
}
584+
523585
func test_specifyServers_encode() throws {
524586
let document = OpenAPI.Document(
525587
info: .init(title: "API", version: "1.0"),

documentation/v4_migration_guide.md

+10-4
Original file line numberDiff line numberDiff line change
@@ -20,10 +20,16 @@ is now required.
2020
Only relevant when compiling OpenAPIKit on macOS: Now v10_15+ is required.
2121

2222
### OpenAPI Specification Versions
23-
The `OpenAPIKit.Document.Version` enum gained `v3_1_1` and the
24-
`OpenAPIKit30.Document.Version` enum gained `v3_0_4`. If you have exhaustive
25-
switches over values of those types then your switch statements will need to be
26-
updated.
23+
The OpenAPIKit module's `OpenAPI.Document.Version` enum gained `v3_1_1` and the
24+
OpenAPIKit30 module's `OpenAPI.Document.Version` enum gained `v3_0_4`.
25+
26+
The `OpenAPI.Document.Version` enum in both modules gained a new case
27+
(`v3_0_x(x: Int)` and `v3_1_x(x: Int)` respectively) that represents future OAS
28+
versions not released at the time of the given OpenAPIKit release. This allows
29+
non-breaking addition of support for those new versions.
30+
31+
If you have exhaustive switches over values of those types then your switch
32+
statements will need to be updated.
2733

2834
### Typo corrections
2935
The following typo corrections were made to OpenAPIKit code. These amount to

0 commit comments

Comments
 (0)