Skip to content

Commit 2917ce8

Browse files
[swift5/swift6] Fix oneOf decoding when enumUnknownDefaultCase is enabled (#23496)
* Fix oneOf decoding when enumUnknownDefaultCase is enabled * Only add extension to types with enum properties * Skip array and map properties in the containsUnknownDefaultOpenApiCase check * Detect unknown default cases in array enum properties
1 parent 8a65919 commit 2917ce8

23 files changed

Lines changed: 302 additions & 6 deletions

File tree

modules/openapi-generator/src/main/resources/swift5/Models.mustache

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ extension CaseIterableDefaultsLast {
3636
}
3737
}
3838

39+
{{#enumUnknownDefaultCase}}
40+
/// Protocol for types used as oneOf variants, allowing the oneOf decoder to reject
41+
/// a variant that only decoded successfully because CaseIterableDefaultsLast
42+
/// silently accepted an unknown enum value.
43+
protocol UnknownCaseCheckable {
44+
var containsUnknownDefaultOpenApiCase: Bool { get }
45+
}
46+
47+
extension UnknownCaseCheckable {
48+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool { false }
49+
}
50+
51+
{{/enumUnknownDefaultCase}}
3952
/// A flexible type that can be encoded (`.encodeNull` or `.encodeValue`)
4053
/// or not encoded (`.encodeNothing`). Intended for request payloads.
4154
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} enum NullEncodable<Wrapped: Hashable>: Hashable {

modules/openapi-generator/src/main/resources/swift5/model.mustache

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,60 @@ extension {{projectName}}API {
3030
{{/swiftUseApiNamespace}}{{#models}}{{#model}}{{#vendorExtensions.x-swift-identifiable}}
3131
@available(iOS 13, tvOS 13, watchOS 6, macOS 10.15, *)
3232
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: Identifiable {}
33-
{{/vendorExtensions.x-swift-identifiable}}{{/model}}{{/models}}
33+
{{/vendorExtensions.x-swift-identifiable}}{{#enumUnknownDefaultCase}}{{^vendorExtensions.x-is-one-of-interface}}{{^isArray}}{{^isEnum}}{{#hasEnums}}
34+
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: UnknownCaseCheckable {
35+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool {
36+
{{#allVars}}
37+
{{#isEnum}}
38+
{{^isContainer}}
39+
{{#vendorExtensions.x-null-encodable}}
40+
if {{{name}}} == .encodeValue(.unknownDefaultOpenApi) { return true }
41+
{{/vendorExtensions.x-null-encodable}}
42+
{{^vendorExtensions.x-null-encodable}}
43+
if {{{name}}} == .unknownDefaultOpenApi { return true }
44+
{{/vendorExtensions.x-null-encodable}}
45+
{{/isContainer}}
46+
{{#isArray}}
47+
{{#required}}{{^isNullable}}
48+
if {{{name}}}.contains(.unknownDefaultOpenApi) { return true }
49+
{{/isNullable}}{{#isNullable}}
50+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
51+
{{/isNullable}}{{/required}}
52+
{{^required}}
53+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
54+
{{/required}}
55+
{{/isArray}}
56+
{{/isEnum}}
57+
{{^isEnum}}
58+
{{#isEnumRef}}
59+
{{^isContainer}}
60+
{{#vendorExtensions.x-null-encodable}}
61+
if {{{name}}} == .encodeValue(.unknownDefaultOpenApi) { return true }
62+
{{/vendorExtensions.x-null-encodable}}
63+
{{^vendorExtensions.x-null-encodable}}
64+
if {{{name}}} == .unknownDefaultOpenApi { return true }
65+
{{/vendorExtensions.x-null-encodable}}
66+
{{/isContainer}}
67+
{{#isArray}}
68+
{{#required}}{{^isNullable}}
69+
if {{{name}}}.contains(.unknownDefaultOpenApi) { return true }
70+
{{/isNullable}}{{#isNullable}}
71+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
72+
{{/isNullable}}{{/required}}
73+
{{^required}}
74+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
75+
{{/required}}
76+
{{/isArray}}
77+
{{/isEnumRef}}
78+
{{/isEnum}}
79+
{{/allVars}}
80+
return false
81+
}
82+
}
83+
{{/hasEnums}}{{/isEnum}}{{/isArray}}{{/vendorExtensions.x-is-one-of-interface}}{{#isEnum}}
84+
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: UnknownCaseCheckable {
85+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool {
86+
self == .unknownDefaultOpenApi
87+
}
88+
}
89+
{{/isEnum}}{{/enumUnknownDefaultCase}}{{/model}}{{/models}}

modules/openapi-generator/src/main/resources/swift5/modelOneOf.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@
5151
{{/discriminator}}{{^discriminator}} let container = try decoder.singleValueContainer()
5252
{{#oneOf}}
5353
{{#-first}}
54-
if let value = try? container.decode({{.}}.self) {
54+
if let value = try? container.decode({{.}}.self){{#enumUnknownDefaultCase}}, (value as? UnknownCaseCheckable)?.containsUnknownDefaultOpenApiCase != true{{/enumUnknownDefaultCase}} {
5555
{{/-first}}
5656
{{^-first}}
57-
} else if let value = try? container.decode({{.}}.self) {
57+
} else if let value = try? container.decode({{.}}.self){{#enumUnknownDefaultCase}}, (value as? UnknownCaseCheckable)?.containsUnknownDefaultOpenApiCase != true{{/enumUnknownDefaultCase}} {
5858
{{/-first}}
5959
self = .type{{.}}(value)
6060
{{/oneOf}}

modules/openapi-generator/src/main/resources/swift6/Models.mustache

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,19 @@ extension CaseIterableDefaultsLast {
3636
}
3737
}
3838

39+
{{#enumUnknownDefaultCase}}
40+
/// Protocol for types used as oneOf variants, allowing the oneOf decoder to reject
41+
/// a variant that only decoded successfully because CaseIterableDefaultsLast
42+
/// silently accepted an unknown enum value.
43+
protocol UnknownCaseCheckable {
44+
var containsUnknownDefaultOpenApiCase: Bool { get }
45+
}
46+
47+
extension UnknownCaseCheckable {
48+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool { false }
49+
}
50+
51+
{{/enumUnknownDefaultCase}}
3952
/// A flexible type that can be encoded (`.encodeNull` or `.encodeValue`)
4053
/// or not encoded (`.encodeNothing`). Intended for request payloads.
4154
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} enum NullEncodable<Wrapped> {

modules/openapi-generator/src/main/resources/swift6/model.mustache

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,60 @@ extension {{projectName}}API {
2626
}
2727
{{/swiftUseApiNamespace}}{{#models}}{{#model}}{{#vendorExtensions.x-swift-identifiable}}
2828
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: Identifiable {}
29-
{{/vendorExtensions.x-swift-identifiable}}{{/model}}{{/models}}
29+
{{/vendorExtensions.x-swift-identifiable}}{{#enumUnknownDefaultCase}}{{^vendorExtensions.x-is-one-of-interface}}{{^isArray}}{{^isEnum}}{{#hasEnums}}
30+
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: UnknownCaseCheckable {
31+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool {
32+
{{#allVars}}
33+
{{#isEnum}}
34+
{{^isContainer}}
35+
{{#vendorExtensions.x-null-encodable}}
36+
if {{{name}}} == .encodeValue(.unknownDefaultOpenApi) { return true }
37+
{{/vendorExtensions.x-null-encodable}}
38+
{{^vendorExtensions.x-null-encodable}}
39+
if {{{name}}} == .unknownDefaultOpenApi { return true }
40+
{{/vendorExtensions.x-null-encodable}}
41+
{{/isContainer}}
42+
{{#isArray}}
43+
{{#required}}{{^isNullable}}
44+
if {{{name}}}.contains(.unknownDefaultOpenApi) { return true }
45+
{{/isNullable}}{{#isNullable}}
46+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
47+
{{/isNullable}}{{/required}}
48+
{{^required}}
49+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
50+
{{/required}}
51+
{{/isArray}}
52+
{{/isEnum}}
53+
{{^isEnum}}
54+
{{#isEnumRef}}
55+
{{^isContainer}}
56+
{{#vendorExtensions.x-null-encodable}}
57+
if {{{name}}} == .encodeValue(.unknownDefaultOpenApi) { return true }
58+
{{/vendorExtensions.x-null-encodable}}
59+
{{^vendorExtensions.x-null-encodable}}
60+
if {{{name}}} == .unknownDefaultOpenApi { return true }
61+
{{/vendorExtensions.x-null-encodable}}
62+
{{/isContainer}}
63+
{{#isArray}}
64+
{{#required}}{{^isNullable}}
65+
if {{{name}}}.contains(.unknownDefaultOpenApi) { return true }
66+
{{/isNullable}}{{#isNullable}}
67+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
68+
{{/isNullable}}{{/required}}
69+
{{^required}}
70+
if {{{name}}}?.contains(.unknownDefaultOpenApi) == true { return true }
71+
{{/required}}
72+
{{/isArray}}
73+
{{/isEnumRef}}
74+
{{/isEnum}}
75+
{{/allVars}}
76+
return false
77+
}
78+
}
79+
{{/hasEnums}}{{/isEnum}}{{/isArray}}{{/vendorExtensions.x-is-one-of-interface}}{{#isEnum}}
80+
extension {{#swiftUseApiNamespace}}{{projectName}}API.{{/swiftUseApiNamespace}}{{{classname}}}: UnknownCaseCheckable {
81+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var containsUnknownDefaultOpenApiCase: Bool {
82+
self == .unknownDefaultOpenApi
83+
}
84+
}
85+
{{/isEnum}}{{/enumUnknownDefaultCase}}{{/model}}{{/models}}

modules/openapi-generator/src/main/resources/swift6/modelOneOf.mustache

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@
5151
{{/discriminator}}{{^discriminator}} let container = try decoder.singleValueContainer()
5252
{{#oneOf}}
5353
{{#-first}}
54-
if let value = try? container.decode({{.}}.self) {
54+
if let value = try? container.decode({{.}}.self){{#enumUnknownDefaultCase}}, (value as? UnknownCaseCheckable)?.containsUnknownDefaultOpenApiCase != true{{/enumUnknownDefaultCase}} {
5555
{{/-first}}
5656
{{^-first}}
57-
} else if let value = try? container.decode({{.}}.self) {
57+
} else if let value = try? container.decode({{.}}.self){{#enumUnknownDefaultCase}}, (value as? UnknownCaseCheckable)?.containsUnknownDefaultOpenApiCase != true{{/enumUnknownDefaultCase}} {
5858
{{/-first}}
5959
self = .type{{#transformArrayType}}{{.}}{{/transformArrayType}}(value)
6060
{{/oneOf}}

modules/openapi-generator/src/test/java/org/openapitools/codegen/swift6/Swift6ClientCodegenTest.java

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -365,6 +365,38 @@ public void oneOfArrayTypeNamesTest() throws IOException {
365365
}
366366
}
367367

368+
@Test(description = "test oneOf with enumUnknownDefaultCase generates UnknownCaseCheckable guard", enabled = true)
369+
public void oneOfEnumUnknownDefaultCaseGuardTest() throws IOException {
370+
Path target = Files.createTempDirectory("test");
371+
File output = target.toFile();
372+
try {
373+
final CodegenConfigurator configurator = new CodegenConfigurator()
374+
.setGeneratorName("swift6")
375+
.setInputSpec("src/test/resources/3_0/oneOf.yaml")
376+
.setOutputDir(target.toAbsolutePath().toString())
377+
.addAdditionalProperty("enumUnknownDefaultCase", true);
378+
379+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
380+
DefaultGenerator generator = new DefaultGenerator(false);
381+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
382+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
383+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true");
384+
385+
List<File> files = generator.opts(clientOptInput).generate();
386+
387+
String oneOfContent = Files.readString(files.stream()
388+
.filter(f -> f.getName().equals("Fruit.swift")).findFirst().get().toPath());
389+
Assert.assertTrue(oneOfContent.contains("as? UnknownCaseCheckable)?.containsUnknownDefaultOpenApiCase != true"),
390+
"oneOf decoder should guard against unknown default enum cases");
391+
392+
String modelsContent = Files.readString(files.stream()
393+
.filter(f -> f.getName().equals("Models.swift")).findFirst().get().toPath());
394+
Assert.assertTrue(modelsContent.contains("protocol UnknownCaseCheckable"));
395+
} finally {
396+
output.deleteOnExit();
397+
}
398+
}
399+
368400
@Test(description = "test oneOf with discriminator generates discriminator-first decoding", enabled = true)
369401
public void oneOfDiscriminatorFirstDecodingTest() throws IOException {
370402
Path target = Files.createTempDirectory("test");

samples/client/petstore/swift5/resultLibrary/PetstoreClient/Classes/OpenAPIs/Models.swift

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@ extension CaseIterableDefaultsLast {
3535
}
3636
}
3737

38+
/// Protocol for types used as oneOf variants, allowing the oneOf decoder to reject
39+
/// a variant that only decoded successfully because CaseIterableDefaultsLast
40+
/// silently accepted an unknown enum value.
41+
protocol UnknownCaseCheckable {
42+
var containsUnknownDefaultOpenApiCase: Bool { get }
43+
}
44+
45+
extension UnknownCaseCheckable {
46+
internal var containsUnknownDefaultOpenApiCase: Bool { false }
47+
}
48+
3849
/// A flexible type that can be encoded (`.encodeNull` or `.encodeValue`)
3950
/// or not encoded (`.encodeNothing`). Intended for request payloads.
4051
internal enum NullEncodable<Wrapped: Hashable>: Hashable {

samples/client/petstore/swift5/resultLibrary/PetstoreClient/Classes/OpenAPIs/Models/EnumArrays.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,3 +44,12 @@ internal struct EnumArrays: Codable, JSONEncodable {
4444
}
4545
}
4646

47+
48+
extension EnumArrays: UnknownCaseCheckable {
49+
internal var containsUnknownDefaultOpenApiCase: Bool {
50+
if justSymbol == .unknownDefaultOpenApi { return true }
51+
52+
if arrayEnum?.contains(.unknownDefaultOpenApi) == true { return true }
53+
return false
54+
}
55+
}

samples/client/petstore/swift5/resultLibrary/PetstoreClient/Classes/OpenAPIs/Models/EnumClass.swift

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,3 +16,9 @@ internal enum EnumClass: String, Codable, CaseIterable, CaseIterableDefaultsLast
1616
case xyz = "(xyz)"
1717
case unknownDefaultOpenApi = "unknown_default_open_api"
1818
}
19+
20+
extension EnumClass: UnknownCaseCheckable {
21+
internal var containsUnknownDefaultOpenApiCase: Bool {
22+
self == .unknownDefaultOpenApi
23+
}
24+
}

0 commit comments

Comments
 (0)