Skip to content

Commit eb34758

Browse files
Merge branch 'swift-oneof-sibling-properties'
2 parents 5f54c25 + 5b0994b commit eb34758

74 files changed

Lines changed: 5487 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
generatorName: swift5
2+
outputDir: samples/client/petstore/swift5/oneOfSharedProperties
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneOf_sharedProperties.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/swift5
5+
generateAliasAsModel: true
6+
additionalProperties:
7+
podAuthors: ""
8+
podSummary: PetstoreClient
9+
projectName: PetstoreClient
10+
podHomepage: https://github.com/openapitools/openapi-generator
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
generatorName: swift6
2+
outputDir: samples/client/petstore/swift6/oneOfSharedProperties
3+
inputSpec: modules/openapi-generator/src/test/resources/3_0/oneOf_sharedProperties.yaml
4+
templateDir: modules/openapi-generator/src/main/resources/swift6
5+
generateAliasAsModel: true
6+
additionalProperties:
7+
responseAs: ObjcBlock
8+
useSPMFileStructure: false
9+
podAuthors: ""
10+
podSummary: PetstoreClient
11+
projectName: PetstoreClient
12+
podHomepage: https://github.com/openapitools/openapi-generator

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift5ClientCodegen.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1177,6 +1177,13 @@ public ModelsMap postProcessModels(ModelsMap objs) {
11771177
return postProcessedModelsEnum;
11781178
}
11791179

1180+
@Override
1181+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
1182+
Map<String, ModelsMap> processed = super.postProcessAllModels(objs);
1183+
SwiftCodegenUtils.attachSharedAccessors(processed);
1184+
return processed;
1185+
}
1186+
11801187
@Override
11811188
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
11821189
super.postProcessModelProperty(model, property);

modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/Swift6ClientCodegen.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1240,6 +1240,13 @@ public ModelsMap postProcessModels(ModelsMap objs) {
12401240
return postProcessedModelsEnum;
12411241
}
12421242

1243+
@Override
1244+
public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs) {
1245+
Map<String, ModelsMap> processed = super.postProcessAllModels(objs);
1246+
SwiftCodegenUtils.attachSharedAccessors(processed);
1247+
return processed;
1248+
}
1249+
12431250
@Override
12441251
public void postProcessModelProperty(CodegenModel model, CodegenProperty property) {
12451252
super.postProcessModelProperty(model, property);
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
/*
2+
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
3+
* Copyright 2018 SmartBear Software
4+
*
5+
* Licensed under the Apache License, Version 2.0 (the "License");
6+
* you may not use this file except in compliance with the License.
7+
* You may obtain a copy of the License at
8+
*
9+
* https://www.apache.org/licenses/LICENSE-2.0
10+
*
11+
* Unless required by applicable law or agreed to in writing, software
12+
* distributed under the License is distributed on an "AS IS" BASIS,
13+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
* See the License for the specific language governing permissions and
15+
* limitations under the License.
16+
*/
17+
18+
package org.openapitools.codegen.languages;
19+
20+
import org.openapitools.codegen.CodegenConstants;
21+
import org.openapitools.codegen.CodegenModel;
22+
import org.openapitools.codegen.CodegenProperty;
23+
import org.openapitools.codegen.model.ModelMap;
24+
import org.openapitools.codegen.model.ModelsMap;
25+
import org.openapitools.codegen.utils.ModelUtils;
26+
import org.slf4j.Logger;
27+
import org.slf4j.LoggerFactory;
28+
29+
import java.util.ArrayList;
30+
import java.util.Collections;
31+
import java.util.HashMap;
32+
import java.util.List;
33+
import java.util.Map;
34+
import java.util.Objects;
35+
36+
final class SwiftCodegenUtils {
37+
38+
static final String X_SWIFT_SHARED_ACCESSORS = "x-swift-shared-accessors";
39+
40+
private static final Logger LOGGER = LoggerFactory.getLogger(SwiftCodegenUtils.class);
41+
42+
private SwiftCodegenUtils() {}
43+
44+
// A model declared as `type: object` with both sibling properties and `oneOf`
45+
// is rendered as a Swift enum with no stored properties. Without this pass
46+
// the parent's sibling properties are silently dropped from the output. Here
47+
// we surface properties that are present (with matching types) on every
48+
// variant as computed accessors on the enum.
49+
static void attachSharedAccessors(Map<String, ModelsMap> objs) {
50+
for (Map.Entry<String, ModelsMap> entry : objs.entrySet()) {
51+
CodegenModel cm = firstModel(entry.getValue());
52+
if (cm == null) continue;
53+
if (!Boolean.TRUE.equals(cm.vendorExtensions.get(CodegenConstants.X_IS_ONE_OF_INTERFACE))) continue;
54+
if (cm.vars == null || cm.vars.isEmpty()) continue;
55+
if (cm.oneOf == null || cm.oneOf.isEmpty()) continue;
56+
57+
List<Map<String, CodegenProperty>> variantProperties = resolveVariantProperties(cm, objs);
58+
if (variantProperties == null) continue;
59+
60+
List<CodegenProperty> shared = new ArrayList<>();
61+
for (CodegenProperty parentProp : cm.vars) {
62+
if (isSharedAcrossVariants(parentProp, variantProperties)) {
63+
shared.add(parentProp);
64+
}
65+
}
66+
if (!shared.isEmpty()) {
67+
cm.vendorExtensions.put(X_SWIFT_SHARED_ACCESSORS, shared);
68+
}
69+
}
70+
}
71+
72+
private static List<Map<String, CodegenProperty>> resolveVariantProperties(CodegenModel cm, Map<String, ModelsMap> objs) {
73+
List<Map<String, CodegenProperty>> result = new ArrayList<>(cm.oneOf.size());
74+
for (String variantName : cm.oneOf) {
75+
CodegenModel variant = ModelUtils.getModelByName(variantName, objs);
76+
if (variant == null) {
77+
LOGGER.info("Cannot resolve oneOf variant '{}' for '{}'; skipping shared-accessor pass.", variantName, cm.name);
78+
return null;
79+
}
80+
result.add(indexByBaseName(variant));
81+
}
82+
return result;
83+
}
84+
85+
private static Map<String, CodegenProperty> indexByBaseName(CodegenModel variant) {
86+
List<CodegenProperty> source = variant.allVars != null && !variant.allVars.isEmpty()
87+
? variant.allVars
88+
: variant.vars;
89+
if (source == null) return Collections.emptyMap();
90+
Map<String, CodegenProperty> index = new HashMap<>(source.size());
91+
for (CodegenProperty p : source) {
92+
index.putIfAbsent(p.baseName, p);
93+
}
94+
return index;
95+
}
96+
97+
private static boolean isSharedAcrossVariants(CodegenProperty parentProp, List<Map<String, CodegenProperty>> variantProperties) {
98+
CodegenProperty first = null;
99+
for (Map<String, CodegenProperty> index : variantProperties) {
100+
CodegenProperty match = index.get(parentProp.baseName);
101+
if (match == null) return false;
102+
if (first == null) {
103+
first = match;
104+
} else if (!Objects.equals(first.dataType, match.dataType)
105+
|| !Objects.equals(first.datatypeWithEnum, match.datatypeWithEnum)
106+
|| first.required != match.required
107+
|| first.isNullable != match.isNullable) {
108+
return false;
109+
}
110+
}
111+
return first != null;
112+
}
113+
114+
private static CodegenModel firstModel(ModelsMap mm) {
115+
if (mm == null) return null;
116+
List<ModelMap> list = mm.getModels();
117+
if (list == null) return null;
118+
for (ModelMap m : list) {
119+
CodegenModel model = m.getModel();
120+
if (model != null) return model;
121+
}
122+
return null;
123+
}
124+
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,17 @@
6767
{{/oneOfUnknownDefaultCase}}
6868
}
6969
{{/discriminator}} }
70+
{{#vendorExtensions.x-swift-shared-accessors}}
71+
72+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var {{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}} {
73+
switch self {
74+
{{#oneOf}}
75+
case .type{{.}}(let value): return value.{{{name}}}
76+
{{/oneOf}}
77+
{{#oneOfUnknownDefaultCase}}
78+
case .unknownDefaultOpenApi: return nil
79+
{{/oneOfUnknownDefaultCase}}
80+
}
81+
}
82+
{{/vendorExtensions.x-swift-shared-accessors}}
7083
}

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

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,4 +67,17 @@
6767
{{/oneOfUnknownDefaultCase}}
6868
}
6969
{{/discriminator}} }
70+
{{#vendorExtensions.x-swift-shared-accessors}}
71+
72+
{{#nonPublicApi}}internal{{/nonPublicApi}}{{^nonPublicApi}}public{{/nonPublicApi}} var {{{name}}}: {{{datatypeWithEnum}}}{{#required}}{{#isNullable}}?{{/isNullable}}{{/required}}{{^required}}?{{/required}} {
73+
switch self {
74+
{{#oneOf}}
75+
case .type{{#transformArrayType}}{{.}}{{/transformArrayType}}(let value): return value.{{{name}}}
76+
{{/oneOf}}
77+
{{#oneOfUnknownDefaultCase}}
78+
case .unknownDefaultOpenApi: return nil
79+
{{/oneOfUnknownDefaultCase}}
80+
}
81+
}
82+
{{/vendorExtensions.x-swift-shared-accessors}}
7083
}

modules/openapi-generator/src/test/java/org/openapitools/codegen/swift5/Swift5ClientCodegenTest.java

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,4 +355,52 @@ public void oneOfDiscriminatorFirstDecodingTest() throws IOException {
355355
}
356356
}
357357

358+
@Test(description = "oneOf model with sibling properties exposes shared accessors on the enum", enabled = true)
359+
public void oneOfSharedPropertyAccessorsTest() throws IOException {
360+
Path target = Files.createTempDirectory("test");
361+
File output = target.toFile();
362+
try {
363+
final CodegenConfigurator configurator = new CodegenConfigurator()
364+
.setGeneratorName("swift5")
365+
.setInputSpec("src/test/resources/3_0/oneOf_sharedProperties.yaml")
366+
.setOutputDir(target.toAbsolutePath().toString());
367+
368+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
369+
DefaultGenerator generator = new DefaultGenerator(false);
370+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
371+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
372+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
373+
374+
List<File> files = generator.opts(clientOptInput).generate();
375+
376+
File shapeFile = files.stream()
377+
.filter(f -> f.getName().equals("Shape.swift"))
378+
.findFirst()
379+
.orElseThrow(() -> new RuntimeException("Shape.swift not found"));
380+
381+
String content = Files.readString(shapeFile.toPath());
382+
383+
// The enum and its dispatch should still be generated
384+
Assert.assertTrue(content.contains("public enum Shape:"));
385+
Assert.assertTrue(content.contains("case typeCircle(Circle)"));
386+
Assert.assertTrue(content.contains("case typeSquare(Square)"));
387+
388+
// Properties shared by every variant should be exposed as computed accessors
389+
Assert.assertTrue(content.contains("public var id: String?"),
390+
"Expected computed accessor for shared property 'id'");
391+
Assert.assertTrue(content.contains("public var label: String?"),
392+
"Expected computed accessor for shared property 'label'");
393+
Assert.assertTrue(content.contains("public var kind: String?"),
394+
"Expected computed accessor for shared property 'kind'");
395+
Assert.assertTrue(content.contains("case .typeCircle(let value): return value.id"));
396+
Assert.assertTrue(content.contains("case .typeSquare(let value): return value.id"));
397+
398+
// 'onlyOnCircle' is on Shape and Circle but not Square -> must NOT be emitted
399+
Assert.assertFalse(content.contains("public var onlyOnCircle:"),
400+
"Property only present on a subset of variants must not be exposed (would not compile)");
401+
} finally {
402+
output.deleteOnExit();
403+
}
404+
}
405+
358406
}

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

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -450,4 +450,49 @@ public void testNullableMap() {
450450
Assert.assertEquals(notNullableMap.getDataType(), "[String: String]");
451451
Assert.assertEquals(defaultMap.getDataType(), "[String: String]");
452452
}
453+
454+
@Test(description = "oneOf model with sibling properties exposes shared accessors on the enum", enabled = true)
455+
public void oneOfSharedPropertyAccessorsTest() throws IOException {
456+
Path target = Files.createTempDirectory("test");
457+
File output = target.toFile();
458+
try {
459+
final CodegenConfigurator configurator = new CodegenConfigurator()
460+
.setGeneratorName("swift6")
461+
.setInputSpec("src/test/resources/3_0/oneOf_sharedProperties.yaml")
462+
.setOutputDir(target.toAbsolutePath().toString());
463+
464+
final ClientOptInput clientOptInput = configurator.toClientOptInput();
465+
DefaultGenerator generator = new DefaultGenerator(false);
466+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
467+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "false");
468+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "false");
469+
470+
List<File> files = generator.opts(clientOptInput).generate();
471+
472+
File shapeFile = files.stream()
473+
.filter(f -> f.getName().equals("Shape.swift"))
474+
.findFirst()
475+
.orElseThrow(() -> new RuntimeException("Shape.swift not found"));
476+
477+
String content = Files.readString(shapeFile.toPath());
478+
479+
Assert.assertTrue(content.contains("public enum Shape:"));
480+
Assert.assertTrue(content.contains("case typeCircle(Circle)"));
481+
Assert.assertTrue(content.contains("case typeSquare(Square)"));
482+
483+
Assert.assertTrue(content.contains("public var id: String?"),
484+
"Expected computed accessor for shared property 'id'");
485+
Assert.assertTrue(content.contains("public var label: String?"),
486+
"Expected computed accessor for shared property 'label'");
487+
Assert.assertTrue(content.contains("public var kind: String?"),
488+
"Expected computed accessor for shared property 'kind'");
489+
Assert.assertTrue(content.contains("case .typeCircle(let value): return value.id"));
490+
Assert.assertTrue(content.contains("case .typeSquare(let value): return value.id"));
491+
492+
Assert.assertFalse(content.contains("public var onlyOnCircle:"),
493+
"Property only present on a subset of variants must not be exposed (would not compile)");
494+
} finally {
495+
output.deleteOnExit();
496+
}
497+
}
453498
}
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
openapi: 3.0.1
2+
info:
3+
title: oneOf with shared properties
4+
version: 0.0.1
5+
paths:
6+
/shape:
7+
get:
8+
responses:
9+
'200':
10+
description: a shape
11+
content:
12+
application/json:
13+
schema:
14+
$ref: '#/components/schemas/Shape'
15+
components:
16+
schemas:
17+
Shape:
18+
type: object
19+
properties:
20+
id:
21+
type: string
22+
label:
23+
type: string
24+
kind:
25+
type: string
26+
onlyOnCircle:
27+
type: string
28+
oneOf:
29+
- $ref: '#/components/schemas/Circle'
30+
- $ref: '#/components/schemas/Square'
31+
discriminator:
32+
propertyName: kind
33+
mapping:
34+
circle: '#/components/schemas/Circle'
35+
square: '#/components/schemas/Square'
36+
Circle:
37+
type: object
38+
properties:
39+
id:
40+
type: string
41+
label:
42+
type: string
43+
kind:
44+
type: string
45+
onlyOnCircle:
46+
type: string
47+
radius:
48+
type: number
49+
Square:
50+
type: object
51+
properties:
52+
id:
53+
type: string
54+
label:
55+
type: string
56+
kind:
57+
type: string
58+
side:
59+
type: number

0 commit comments

Comments
 (0)