Skip to content

Commit 33617ee

Browse files
Improve generation of selected models with dependent models (#18462)
* Issue-18444: recursively trace variables and support of new option * Issue-18444: suppoting inheritance, but interfaces * Issue-18444: build project instructions executed * code review from wing328: tab-spaces removed * code review by wing328: added a line of comment for the private method
1 parent 9b0ca06 commit 33617ee

5 files changed

Lines changed: 429 additions & 22 deletions

File tree

modules/openapi-generator-maven-plugin/src/main/java/org/openapitools/codegen/plugin/CodeGenMojo.java

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -443,6 +443,12 @@ public class CodeGenMojo extends AbstractMojo {
443443
@Parameter(name = "generateModels", property = "openapi.generator.maven.plugin.generateModels")
444444
private Boolean generateModels = true;
445445

446+
/**
447+
* Generate the models recursively if models should generate selectively (see modelsToGenerate) and all dependent models are to generate
448+
*/
449+
@Parameter(name = "generateRecursiveDependentModels", property = "openapi.generator.maven.plugin.generateRecursiveDependentModels")
450+
private Boolean generateRecursiveDependentModels = false;
451+
446452
/**
447453
* A comma separated list of models to generate. All models is the default.
448454
*/
@@ -795,6 +801,7 @@ public void execute() throws MojoExecutionException {
795801
GlobalSettings.setProperty(CodegenConstants.API_TESTS, generateApiTests.toString());
796802
GlobalSettings.setProperty(CodegenConstants.API_DOCS, generateApiDocumentation.toString());
797803
GlobalSettings.setProperty(CodegenConstants.WITH_XML, withXml.toString());
804+
GlobalSettings.setProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, generateRecursiveDependentModels.toString());
798805

799806
if (configOptions != null) {
800807
// Retained for backwards-compatibility with configOptions -> instantiation-types

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ public class CodegenConstants {
2525
// NOTE: We may want to move these to a separate class to avoid confusion or modification.
2626
public static final String APIS = "apis";
2727
public static final String MODELS = "models";
28+
public static final String GENERATE_RECURSIVE_DEPENDENT_MODELS = "generateRecursiveDependentModels";
2829
public static final String SUPPORTING_FILES = "supportingFiles";
2930
public static final String MODEL_TESTS = "modelTests";
3031
public static final String MODEL_DOCS = "modelDocs";

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

Lines changed: 94 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
import io.swagger.v3.oas.models.parameters.Parameter;
3030
import io.swagger.v3.oas.models.security.*;
3131
import io.swagger.v3.oas.models.tags.Tag;
32+
3233
import org.apache.commons.io.FilenameUtils;
3334
import org.apache.commons.io.comparator.PathFileComparator;
3435
import org.apache.commons.lang3.ObjectUtils;
@@ -65,6 +66,7 @@
6566
import java.util.*;
6667
import java.util.concurrent.ConcurrentSkipListSet;
6768
import java.util.function.Function;
69+
import java.util.function.Supplier;
6870
import java.util.stream.Collectors;
6971

7072
import static org.apache.commons.lang3.StringUtils.removeStart;
@@ -81,6 +83,7 @@ public class DefaultGenerator implements Generator {
8183
protected CodegenIgnoreProcessor ignoreProcessor;
8284
private Boolean generateApis = null;
8385
private Boolean generateModels = null;
86+
private Boolean generateRecursiveDependentModels = null;
8487
private Boolean generateSupportingFiles = null;
8588
private Boolean generateWebhooks = null;
8689
private Boolean generateApiTests = null;
@@ -232,6 +235,7 @@ void configureGeneratorProperties() {
232235
generateModelDocumentation = GlobalSettings.getProperty(CodegenConstants.MODEL_DOCS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.MODEL_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.MODEL_DOCS, true);
233236
generateApiTests = GlobalSettings.getProperty(CodegenConstants.API_TESTS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.API_TESTS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_TESTS, true);
234237
generateApiDocumentation = GlobalSettings.getProperty(CodegenConstants.API_DOCS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.API_DOCS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.API_DOCS, true);
238+
generateRecursiveDependentModels = GlobalSettings.getProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS) != null ? Boolean.valueOf(GlobalSettings.getProperty(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS)) : getGeneratorPropertyDefaultSwitch(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, false);
235239

236240
// Additional properties added for tests to exclude references in project related files
237241
config.additionalProperties().put(CodegenConstants.GENERATE_API_TESTS, generateApiTests);
@@ -243,6 +247,7 @@ void configureGeneratorProperties() {
243247
config.additionalProperties().put(CodegenConstants.GENERATE_APIS, generateApis);
244248
config.additionalProperties().put(CodegenConstants.GENERATE_MODELS, generateModels);
245249
config.additionalProperties().put(CodegenConstants.GENERATE_WEBHOOKS, generateWebhooks);
250+
config.additionalProperties().put(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, generateRecursiveDependentModels);
246251

247252
if (!generateApiTests && !generateModelTests) {
248253
config.additionalProperties().put(CodegenConstants.EXCLUDE_TESTS, true);
@@ -443,36 +448,21 @@ private void generateModel(List<File> files, Map<String, Object> models, String
443448
}
444449

445450
void generateModels(List<File> files, List<ModelMap> allModels, List<String> unusedModels, List<ModelMap> aliasModels) {
451+
generateModels(files, allModels, unusedModels, aliasModels, new ArrayList<>(), DefaultGenerator.this::modelKeys);
452+
}
453+
454+
void generateModels(List<File> files, List<ModelMap> allModels, List<String> unusedModels, List<ModelMap> aliasModels, List<String> processedModels, Supplier<Set<String>> modelKeysSupplier) {
446455
if (!generateModels) {
447456
// TODO: Process these anyway and add to dryRun info
448457
LOGGER.info("Skipping generation of models.");
449458
return;
450459
}
451460

452-
final Map<String, Schema> schemas = ModelUtils.getSchemas(this.openAPI);
453-
if (schemas == null) {
454-
LOGGER.warn("Skipping generation of models because specification document has no schemas.");
461+
Set<String> modelKeys = modelKeysSupplier.get();
462+
if(modelKeys.isEmpty()) {
455463
return;
456464
}
457465

458-
String modelNames = GlobalSettings.getProperty("models");
459-
Set<String> modelsToGenerate = null;
460-
if (modelNames != null && !modelNames.isEmpty()) {
461-
modelsToGenerate = new HashSet<>(Arrays.asList(modelNames.split(",")));
462-
}
463-
464-
Set<String> modelKeys = schemas.keySet();
465-
if (modelsToGenerate != null && !modelsToGenerate.isEmpty()) {
466-
Set<String> updatedKeys = new HashSet<>();
467-
for (String m : modelKeys) {
468-
if (modelsToGenerate.contains(m)) {
469-
updatedKeys.add(m);
470-
}
471-
}
472-
473-
modelKeys = updatedKeys;
474-
}
475-
476466
// store all processed models
477467
Map<String, ModelsMap> allProcessedModels = new TreeMap<>((o1, o2) -> ObjectUtils.compare(config.toModelName(o1), config.toModelName(o2)));
478468

@@ -508,7 +498,7 @@ void generateModels(List<File> files, List<ModelMap> allModels, List<String> unu
508498
}
509499
}
510500

511-
Schema schema = schemas.get(name);
501+
Schema schema = ModelUtils.getSchemas(this.openAPI).get(name);
512502

513503
if (schema.getExtensions() != null && Boolean.TRUE.equals(schema.getExtensions().get("x-internal"))) {
514504
LOGGER.info("Model {} not generated since x-internal is set to true", name);
@@ -549,6 +539,24 @@ void generateModels(List<File> files, List<ModelMap> allModels, List<String> unu
549539
// post process all processed models
550540
allProcessedModels = config.postProcessAllModels(allProcessedModels);
551541

542+
if (generateRecursiveDependentModels) {
543+
for(ModelsMap modelsMap : allProcessedModels.values()) {
544+
for(ModelMap mm: modelsMap.getModels()) {
545+
CodegenModel cm = mm.getModel();
546+
if (cm != null) {
547+
for(CodegenProperty variable : cm.getVars()) {
548+
generateModelsForVariable(files, allModels, unusedModels, aliasModels, processedModels, variable);
549+
}
550+
//TODO: handle interfaces
551+
String parentSchema = cm.getParentSchema();
552+
if (parentSchema != null && !processedModels.contains(parentSchema) && ModelUtils.getSchemas(this.openAPI).containsKey(parentSchema)) {
553+
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(parentSchema));
554+
}
555+
}
556+
}
557+
}
558+
}
559+
552560
// generate files based on processed models
553561
for (String modelName : allProcessedModels.keySet()) {
554562
ModelsMap models = allProcessedModels.get(modelName);
@@ -592,7 +600,71 @@ void generateModels(List<File> files, List<ModelMap> allModels, List<String> unu
592600
LOGGER.info("############ Model info ############");
593601
Json.prettyPrint(allModels);
594602
}
603+
}
604+
605+
/**
606+
* this method guesses the schema type of in parent model used variable and if the schema type is available it let the generate the model for the type of this variable
607+
*/
608+
private void generateModelsForVariable(List<File> files, List<ModelMap> allModels, List<String> unusedModels, List<ModelMap> aliasModels, List<String> processedModels, CodegenProperty variable) {
609+
if (variable == null) {
610+
return;
611+
}
612+
613+
final String schemaKey = calculateModelKey(variable.getOpenApiType(), variable.getRef());
614+
Map<String, Schema> allSchemas = ModelUtils.getSchemas(this.openAPI);
615+
if (!processedModels.contains(schemaKey) && allSchemas.containsKey(schemaKey)) {
616+
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(schemaKey));
617+
} else if (variable.getComplexType() != null && variable.getComposedSchemas() == null) {
618+
String ref = variable.getHasItems() ? variable.getItems().getRef() : variable.getRef();
619+
final String key = calculateModelKey(variable.getComplexType(), ref);
620+
if (allSchemas.containsKey(key)) {
621+
generateModels(files, allModels, unusedModels, aliasModels, processedModels, () -> Set.of(key));
622+
} else {
623+
LOGGER.info("Type " + variable.getComplexType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
624+
}
625+
} else {
626+
LOGGER.info("Type " + variable.getOpenApiType()+" of variable " + variable.getName() + " could not be resolve because it is not declared as a model.");
627+
}
628+
}
629+
630+
private String calculateModelKey(String type, String ref) {
631+
Map<String, Schema> schemaMap = ModelUtils.getSchemas(this.openAPI);
632+
Set<String> keys = schemaMap.keySet();
633+
String simpleRef;
634+
if(keys.contains(type)) {
635+
return type;
636+
} else if (keys.contains(simpleRef = ModelUtils.getSimpleRef(ref))) {
637+
return simpleRef;
638+
} else {
639+
return type;
640+
}
641+
}
642+
643+
private Set<String> modelKeys() {
644+
final Map<String, Schema> schemas = ModelUtils.getSchemas(this.openAPI);
645+
if (schemas == null) {
646+
LOGGER.warn("Skipping generation of models because specification document has no schemas.");
647+
return Collections.emptySet();
648+
}
595649

650+
String modelNames = GlobalSettings.getProperty("models");
651+
Set<String> modelsToGenerate = null;
652+
if (modelNames != null && !modelNames.isEmpty()) {
653+
modelsToGenerate = new HashSet<>(Arrays.asList(modelNames.split(",")));
654+
}
655+
656+
Set<String> modelKeys = schemas.keySet();
657+
if (modelsToGenerate != null && !modelsToGenerate.isEmpty()) {
658+
Set<String> updatedKeys = new HashSet<>();
659+
for (String m : modelKeys) {
660+
if (modelsToGenerate.contains(m)) {
661+
updatedKeys.add(m);
662+
}
663+
}
664+
665+
modelKeys = updatedKeys;
666+
}
667+
return modelKeys;
596668
}
597669

598670
@SuppressWarnings("unchecked")

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

Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -775,4 +775,137 @@ public void testRecursionBug4650() {
775775
generator.generateModels(files, allModels, filteredSchemas, aliasModels);
776776
// all fine, we have passed
777777
}
778+
779+
780+
private DefaultGenerator generatorGenerateRecursiveDependentModelsBackwardCompatibility(String recursively) throws IOException {
781+
DefaultGenerator generator = new DefaultGenerator(false);
782+
generator.setGeneratorPropertyDefault(CodegenConstants.MODELS, "true");
783+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_TESTS, "true");
784+
generator.setGeneratorPropertyDefault(CodegenConstants.MODEL_DOCS, "false");
785+
generator.setGeneratorPropertyDefault(CodegenConstants.APIS, "true");
786+
generator.setGeneratorPropertyDefault(CodegenConstants.SUPPORTING_FILES, "true");
787+
generator.setGeneratorPropertyDefault(CodegenConstants.API_DOCS, "false");
788+
generator.setGeneratorPropertyDefault(CodegenConstants.API_TESTS, "false");
789+
generator.setGeneratorPropertyDefault(CodegenConstants.GENERATE_RECURSIVE_DEPENDENT_MODELS, recursively);
790+
return generator;
791+
}
792+
793+
private ClientOptInput createOptInputIssue18444(Path target) {
794+
final CodegenConfigurator configurator = new CodegenConfigurator()
795+
.setGeneratorName("spring")
796+
.setInputSpec("src/test/resources/bugs/issue_18444.json")
797+
.setOutputDir(target.toAbsolutePath().toString());
798+
return configurator.toClientOptInput();
799+
}
800+
801+
@Test
802+
public void testGenerateRecursiveDependentModelsBackwardCompatibilityIssue18444() throws IOException {
803+
Path target = Files.createTempDirectory("test");
804+
File output = target.toFile();
805+
String oldModelsProp = GlobalSettings.getProperty("models");
806+
807+
try {
808+
DefaultGenerator generator = generatorGenerateRecursiveDependentModelsBackwardCompatibility("false");
809+
GlobalSettings.setProperty("models", "RQ1,RS1");
810+
ClientOptInput clientOptInput = createOptInputIssue18444(target);
811+
List<File> files = generator.opts(clientOptInput ).generate();
812+
Assert.assertEquals(files.size(), 17);
813+
814+
// Check expected generated files
815+
// api sanity check
816+
String apiJavaFileName = "src/main/java/org/openapitools/api/ApiApi.java";
817+
TestUtils.ensureContainsFile(files, output, apiJavaFileName);
818+
Assert.assertTrue(new File(output, apiJavaFileName).exists());
819+
820+
// model sanity check
821+
String rq1FileName = "src/main/java/org/openapitools/model/RQ1.java";
822+
TestUtils.ensureContainsFile(files, output, rq1FileName);
823+
Assert.assertTrue(new File(output, rq1FileName).exists());
824+
825+
String rs1FileName = "src/main/java/org/openapitools/model/RS1.java";
826+
TestUtils.ensureContainsFile(files, output, rs1FileName );
827+
Assert.assertTrue(new File(output, rs1FileName).exists());
828+
829+
// Check not generated cause backwards compatibility files
830+
String ft1FileName = "src/main/java/org/openapitools/model/FT1.java";
831+
TestUtils.ensureDoesNotContainsFile(files, output, ft1FileName);
832+
Assert.assertFalse(new File(output, ft1FileName).exists());
833+
834+
String ft2FileName = "src/main/java/org/openapitools/model/FT2.java";
835+
TestUtils.ensureDoesNotContainsFile(files, output, ft2FileName);
836+
Assert.assertFalse(new File(output, ft2FileName).exists());
837+
838+
String ft3FileName = "src/main/java/org/openapitools/model/FT3.java";
839+
TestUtils.ensureDoesNotContainsFile(files, output, ft3FileName);
840+
Assert.assertFalse(new File(output, ft3FileName).exists());
841+
842+
String bttFileName = "src/main/java/org/openapitools/model/BTT.java";
843+
TestUtils.ensureDoesNotContainsFile(files, output, bttFileName);
844+
Assert.assertFalse(new File(output, bttFileName).exists());
845+
846+
} finally {
847+
output.deleteOnExit();
848+
if (oldModelsProp != null) {
849+
GlobalSettings.setProperty("models", oldModelsProp);
850+
} else {
851+
GlobalSettings.clearProperty("models");
852+
}
853+
}
854+
}
855+
856+
@Test
857+
public void testGenerateRecursiveDependentModelsIssue18444() throws IOException {
858+
Path target = Files.createTempDirectory("test");
859+
File output = target.toFile();
860+
String oldModelsProp = GlobalSettings.getProperty("models");
861+
862+
try {
863+
DefaultGenerator generator = generatorGenerateRecursiveDependentModelsBackwardCompatibility("true");
864+
GlobalSettings.setProperty("models", "RQ1,RS1");
865+
ClientOptInput clientOptInput = createOptInputIssue18444(target);
866+
List<File> files = generator.opts(clientOptInput ).generate();
867+
Assert.assertEquals(files.size(), 21);
868+
869+
// Check expected generated files
870+
// api sanity check
871+
String apiJavaFileName = "src/main/java/org/openapitools/api/ApiApi.java";
872+
TestUtils.ensureContainsFile(files, output, apiJavaFileName);
873+
Assert.assertTrue(new File(output, apiJavaFileName).exists());
874+
875+
// model sanity check
876+
String rq1FileName = "src/main/java/org/openapitools/model/RQ1.java";
877+
TestUtils.ensureContainsFile(files, output, rq1FileName);
878+
Assert.assertTrue(new File(output, rq1FileName).exists());
879+
880+
String rs1FileName = "src/main/java/org/openapitools/model/RS1.java";
881+
TestUtils.ensureContainsFile(files, output, rs1FileName );
882+
Assert.assertTrue(new File(output, rs1FileName).exists());
883+
884+
// Check generated cause RQ1 and RS1 dependents of FT1,FT2,FT3 files
885+
String ft1FileName = "src/main/java/org/openapitools/model/FT1.java";
886+
TestUtils.ensureContainsFile(files, output, ft1FileName);
887+
Assert.assertTrue(new File(output, ft1FileName).exists());
888+
889+
String ft2FileName = "src/main/java/org/openapitools/model/FT2.java";
890+
TestUtils.ensureContainsFile(files, output, ft2FileName);
891+
Assert.assertTrue(new File(output, ft2FileName).exists());
892+
893+
String ft3FileName = "src/main/java/org/openapitools/model/FT3.java";
894+
TestUtils.ensureContainsFile(files, output, ft3FileName);
895+
Assert.assertTrue(new File(output, ft3FileName).exists());
896+
897+
String bttFileName = "src/main/java/org/openapitools/model/BTT.java";
898+
TestUtils.ensureContainsFile(files, output, bttFileName);
899+
Assert.assertTrue(new File(output, bttFileName).exists());
900+
901+
} finally {
902+
output.deleteOnExit();
903+
if (oldModelsProp != null) {
904+
GlobalSettings.setProperty("models", oldModelsProp);
905+
} else {
906+
GlobalSettings.clearProperty("models");
907+
}
908+
}
909+
}
910+
778911
}

0 commit comments

Comments
 (0)