diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java index 26b91464f898..3f52424b6170 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/CodegenConfig.java @@ -174,6 +174,8 @@ public interface CodegenConfig { String toModelImport(String name); + Map toModelImportMap(String name); + String toApiImport(String name); void addOperationToGroup(String tag, String resourcePath, Operation operation, CodegenOperation co, Map> operations); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java index 92a3f76b7600..bba1b9825f9e 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultCodegen.java @@ -22,6 +22,7 @@ import com.github.benmanes.caffeine.cache.Ticker; import com.google.common.base.CaseFormat; import com.google.common.collect.ImmutableMap; +import com.google.common.collect.Maps; import com.samskivert.mustache.Mustache; import com.samskivert.mustache.Mustache.Compiler; import com.samskivert.mustache.Mustache.Lambda; @@ -1367,6 +1368,16 @@ public String toModelImport(String name) { } } + /** + * Returns the same content as [[toModelImport]] with key the fully-qualified Model name and value the initial input. + * In case of union types this method has a key for each separate model and import. + * @param name the name of the "Model" + * @return Map of fully-qualified models. + */ + public Map toModelImportMap(String name){ + return Collections.singletonMap(this.toModelImport(name),name); + } + /** * Return the fully-qualified "Api" name for import * diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java index ee7b50c05c73..c73e1f5fb131 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/DefaultGenerator.java @@ -58,6 +58,7 @@ import java.nio.file.Path; import java.time.ZonedDateTime; import java.util.*; +import java.util.stream.Collectors; import static org.openapitools.codegen.utils.OnceLogger.once; @@ -1077,26 +1078,11 @@ private Map processOperations(CodegenConfig config, String tag, allImports.addAll(op.imports); } - List> imports = new ArrayList<>(); - Set mappingSet = new TreeSet<>(); - for (String nextImport : allImports) { - Map im = new LinkedHashMap<>(); - String mapping = config.importMapping().get(nextImport); - if (mapping == null) { - mapping = config.toModelImport(nextImport); - } + Map mappings = getAllImportsMappings(allImports); + Set> imports = toImportsObjects(mappings); - if (mapping != null && !mappingSet.contains(mapping)) { // ensure import (mapping) is unique - mappingSet.add(mapping); - im.put("import", mapping); - im.put("classname", nextImport); - if (!imports.contains(im)) { // avoid duplicates - imports.add(im); - } - } - } - - operations.put("imports", imports); + //Some codegen implementations rely on a list interface for the imports + operations.put("imports", imports.stream().collect(Collectors.toList())); // add a flag to indicate whether there's any {{import}} if (imports.size() > 0) { @@ -1115,6 +1101,49 @@ private Map processOperations(CodegenConfig config, String tag, return operations; } + /** + * Transforms a set of imports to a map with key config.toModelImport(import) and value the import string. + * @param allImports - Set of imports + * @return Map of fully qualified import path and initial import. + */ + private Map getAllImportsMappings(Set allImports){ + Map result = new HashMap<>(); + allImports.forEach(nextImport->{ + String mapping = config.importMapping().get(nextImport); + if(mapping!= null){ + result.put(mapping,nextImport); + }else{ + result.putAll(config.toModelImportMap(nextImport)); + } + }); + return result; + } + + /** + * Using an import map created via {@link #getAllImportsMappings(Set)} to build a list import objects. + * The import objects have two keys: import and classname which hold the key and value of the initial map entry. + * + * @param mappedImports Map of fully qualified import and import + * @return The set of unique imports + */ + private Set> toImportsObjects(Map mappedImports){ + Set> result = new TreeSet>( + (Comparator>) (o1, o2) -> { + String s1 = o1.get("classname"); + String s2 = o2.get("classname"); + return s1.compareTo(s2); + } + ); + + mappedImports.entrySet().forEach(mapping->{ + Map im = new LinkedHashMap<>(); + im.put("import", mapping.getKey()); + im.put("classname", mapping.getValue()); + result.add(im); + }); + return result; + } + private Map processModels(CodegenConfig config, Map definitions) { Map objs = new HashMap<>(); objs.put("package", config.modelPackage()); diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java index bbc8e459fe3a..7cd75706b27d 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/AbstractTypeScriptClientCodegen.java @@ -17,6 +17,8 @@ package org.openapitools.codegen.languages; +import com.google.common.collect.Lists; +import com.google.common.collect.Maps; import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.media.ArraySchema; import io.swagger.v3.oas.models.media.ComposedSchema; @@ -35,6 +37,7 @@ import java.io.File; import java.text.SimpleDateFormat; import java.util.*; +import java.util.function.Function; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -224,6 +227,47 @@ public void processOpts() { } } + @Override + public String toModelImport( String name){ + if(isUnionType(name)){ + LOGGER.warn("The import is a union type. Consider using the toModelImportMap method."); + return toModelImportMap(name).values().stream().collect(Collectors.joining("|")); + } + return super.toModelImport(name); + } + + /** + * Maps the fully qualified model import to the initial given name. In case of union types the map will have multiple entries. + * For example for "classA | classB" the map will two entries have ["model.classA","classA"] and ["model.classB","classB"]. + * + * @param name the name of the "Model" + * @return Map between the fully qualified model import and the initial given name. + */ + @Override + public Map toModelImportMap( String name){ + if(isUnionType(name)){ + String[] names = splitUnionType(name); + return toImportMap(names); + } + return toImportMap(name); + } + + private boolean isUnionType(String name){ + return name.contains("|"); + } + + private String[] splitUnionType(String name){ + return name.replace(" ","").split("\\|"); + } + + private Map toImportMap(String... names){ + Map result = Maps.newHashMap(); + for(String name: names){ + result.put(toModelImport(name),name); + } + return result; + } + @Override public void preprocessOpenAPI(OpenAPI openAPI) { diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/SharedTypeScriptTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/SharedTypeScriptTest.java new file mode 100644 index 000000000000..9062d738689b --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/typescript/SharedTypeScriptTest.java @@ -0,0 +1,72 @@ +package org.openapitools.codegen.typescript; + +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang3.StringUtils; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.Generator; +import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.languages.TypeScriptAxiosClientCodegen; +import org.testng.Assert; +import org.testng.annotations.Test; + +import java.io.File; +import java.io.IOException; +import java.util.List; + +public class SharedTypeScriptTest { + @Test + public void typesInImportsAreSplittedTest() throws IOException { + CodegenConfigurator config = + new CodegenConfigurator() + .setInputSpec("src/test/resources/split-import.json") + .setModelPackage("model") + .setApiPackage("api") + .setOutputDir("src/test/resources/typesInImportsAreSplittedTest") + .addAdditionalProperty( + TypeScriptAxiosClientCodegen.SEPARATE_MODELS_AND_API, true); + + config.setGeneratorName("typescript-axios"); + checkAPIFile(getGenerator(config).generate(), "default-api.ts"); + + config.setGeneratorName("typescript-node"); + checkAPIFile(getGenerator(config).generate(), "defaultApi.ts"); + + config.setGeneratorName("typescript-angular"); + checkAPIFile(getGenerator(config).generate(), "default.service.ts"); + + FileUtils.deleteDirectory(new File("src/test/resources/typesInImportsAreSplittedTest")); + } + + private Generator getGenerator(CodegenConfigurator config) { + return new DefaultGenerator().opts(config.toClientOptInput()); + } + + private void checkAPIFile(List files, String apiFileName) throws IOException { + File apiFile = files.stream().filter(file->file.getName().contains(apiFileName)).findFirst().get(); + String apiFileContent = FileUtils.readFileToString(apiFile); + Assert.assertTrue(!apiFileContent.contains("import { OrganizationWrapper | PersonWrapper }")); + Assert.assertEquals(StringUtils.countMatches(apiFileContent,"import { PersonWrapper }"),1); + Assert.assertEquals(StringUtils.countMatches(apiFileContent,"import { OrganizationWrapper }"),1); + } + + @Test + public void oldImportsStillPresentTest() throws IOException { + CodegenConfigurator config = + new CodegenConfigurator() + .setInputSpec("petstore.json") + .setModelPackage("model") + .setApiPackage("api") + .setOutputDir("src/test/resources/oldImportsStillPresentTest/") + .addAdditionalProperty( + TypeScriptAxiosClientCodegen.SEPARATE_MODELS_AND_API, true); + + config.setGeneratorName("typescript-angular"); + final List files = getGenerator(config).generate(); + File pets = files.stream().filter(file->file.getName().contains("pet.ts")).findFirst().get(); + String apiFileContent = FileUtils.readFileToString(pets); + Assert.assertTrue(apiFileContent.contains("import { Category }")); + Assert.assertTrue(apiFileContent.contains("import { Tag }")); + + FileUtils.deleteDirectory(new File("src/test/resources/oldImportsStillPresentTest/")); + } +} \ No newline at end of file diff --git a/modules/openapi-generator/src/test/resources/split-import.json b/modules/openapi-generator/src/test/resources/split-import.json new file mode 100644 index 000000000000..736517af4635 --- /dev/null +++ b/modules/openapi-generator/src/test/resources/split-import.json @@ -0,0 +1,74 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "SAP Graph - Customers API", + "version": "0.1.0" + }, + "paths": { + "/Customer": { + "get": { + "operationId": "getCustomer", + "responses": { + "200": { + "$ref": "#/components/responses/CustomerResponse" + } + } + } + }, + "/Person": { + "get": { + "operationId": "getPerson", + "responses": { + "200": { + "$ref": "#/components/responses/PersonResponse" + } + } + } + } + }, + "components": { + "schemas": { + "OrganizationWrapper": { + "type": "object", + "properties": { + "id": {"type": "string"} + } + }, + "PersonWrapper": { + "type": "object", + "properties": { + "id": {"type": "string"} + } + } + }, + "responses": { + "CustomerResponse": { + "description": "Get Customer", + "content": { + "application/json": { + "schema": { + "oneOf": [ + { + "$ref": "#/components/schemas/OrganizationWrapper" + }, + { + "$ref": "#/components/schemas/PersonWrapper" + } + ] + } + } + } + }, + "PersonResponse": { + "description": "Get Person", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/PersonWrapper" + } + } + } + } + } + } +}