Skip to content

Commit

Permalink
[typescript-fetch] Support fileNaming option (#18283) (#18284)
Browse files Browse the repository at this point in the history
* [typescript-fetch] Support fileNaming option (#18283)

* [typescript-fetch] Support fileNaming option - add tests (#18283)

* [typescript-fetch] introduce constants
  • Loading branch information
En0s0und authored May 3, 2024
1 parent 67a504a commit 0e809d4
Show file tree
Hide file tree
Showing 14 changed files with 337 additions and 63 deletions.
1 change: 1 addition & 0 deletions docs/generators/typescript-fetch.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
|enumPropertyNaming|Naming convention for enum properties: 'camelCase', 'PascalCase', 'snake_case', 'UPPERCASE', and 'original'| |PascalCase|
|enumPropertyNamingReplaceSpecialChar|Set to true to replace '-' and '+' symbols with 'minus_' and 'plus_' in enum of type string| |false|
|enumUnknownDefaultCase|If the server adds new enum cases, that are unknown by an old spec/client, the client will fail to parse the network response.With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the server sends an enum case that is not known by the client/spec, they can safely fallback to this case.|<dl><dt>**false**</dt><dd>No changes to the enum's are made, this is the default option.</dd><dt>**true**</dt><dd>With this option enabled, each enum will have a new case, 'unknown_default_open_api', so that when the enum case sent by the server is not known by the client/spec, can safely be decoded to this case.</dd></dl>|false|
|fileNaming|Naming convention for the output files: 'PascalCase', 'camelCase', 'kebab-case'.| |PascalCase|
|importFileExtension|File extension to use with relative imports. Set it to '.js' or '.mjs' when using [ESM](https://nodejs.org/api/esm.html).| ||
|legacyDiscriminatorBehavior|Set to false for generators with better support for discriminators. (Python, Java, Go, PowerShell, C# have this enabled by default).|<dl><dt>**true**</dt><dd>The mapping in the discriminator includes descendent schemas that allOf inherit from self and the discriminator mapping schemas in the OAS document.</dd><dt>**false**</dt><dd>The mapping in the discriminator includes any descendent schemas that allOf inherit from self, any oneOf schemas, any anyOf schemas, any x-discriminator-values, and the discriminator mapping schemas in the OAS document AND Codegen validates that oneOf and anyOf schemas contain the required discriminator and throws an error if the discriminator is missing.</dd></dl>|true|
|modelPropertyNaming|Naming convention for the property: 'camelCase', 'PascalCase', 'snake_case' and 'original', which keeps the original name| |camelCase|
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
import io.swagger.v3.oas.models.responses.ApiResponse;
import io.swagger.v3.oas.models.servers.Server;
import io.swagger.v3.parser.util.SchemaTypeUtil;
import java.util.stream.Collectors;
import org.openapitools.codegen.*;
import org.openapitools.codegen.meta.features.DocumentationFeature;
import org.openapitools.codegen.meta.features.SecurityFeature;
Expand All @@ -38,6 +37,10 @@

import java.io.File;
import java.util.*;
import java.util.stream.Collectors;

import static org.openapitools.codegen.utils.CamelizeOption.LOWERCASE_FIRST_LETTER;
import static org.openapitools.codegen.utils.StringUtils.*;

public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodegen {
public static final String NPM_REPOSITORY = "npmRepository";
Expand All @@ -49,6 +52,10 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
public static final String STRING_ENUMS_DESC = "Generate string enums instead of objects for enum values.";
public static final String IMPORT_FILE_EXTENSION_SWITCH = "importFileExtension";
public static final String IMPORT_FILE_EXTENSION_SWITCH_DESC = "File extension to use with relative imports. Set it to '.js' or '.mjs' when using [ESM](https://nodejs.org/api/esm.html).";
public static final String FILE_NAMING = "fileNaming";
public static final String KEBAB_CASE = "kebab-case";
public static final String CAMEL_CASE = "camelCase";
public static final String PASCAL_CASE = "PascalCase";

protected String npmRepository = null;
protected String importFileExtension = "";
Expand All @@ -58,6 +65,7 @@ public class TypeScriptFetchClientCodegen extends AbstractTypeScriptClientCodege
protected boolean addedModelIndex = false;
protected boolean withoutRuntimeChecks = false;
protected boolean stringEnums = false;
protected String fileNaming = PASCAL_CASE;

// "Saga and Record" mode.
public static final String SAGAS_AND_RECORDS = "sagasAndRecords";
Expand Down Expand Up @@ -105,6 +113,33 @@ public TypeScriptFetchClientCodegen() {
this.cliOptions.add(new CliOption(SAGAS_AND_RECORDS, "Setting this property to true will generate additional files for use with redux-saga and immutablejs.", SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(STRING_ENUMS, STRING_ENUMS_DESC, SchemaTypeUtil.BOOLEAN_TYPE).defaultValue(Boolean.FALSE.toString()));
this.cliOptions.add(new CliOption(IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_SWITCH_DESC).defaultValue(""));
this.cliOptions.add(new CliOption(FILE_NAMING, "Naming convention for the output files: 'PascalCase', 'camelCase', 'kebab-case'.").defaultValue(this.fileNaming));
}

@Override
public String toApiFilename(String name) {
return convertUsingFileNamingConvention(super.toApiFilename(name));
}

@Override
public String toModelFilename(String name) {
return convertUsingFileNamingConvention(super.toModelFilename(name));
}

/**
* Converts the original name according to the current <code>fileNaming</code> strategy.
*
* @param originalName the original name to transform
* @return the transformed name
*/
private String convertUsingFileNamingConvention(String originalName) {
String name = originalName;
if (KEBAB_CASE.equals(fileNaming)) {
name = dashize(underscore(name));
} else if (CAMEL_CASE.equals(fileNaming)) {
name = camelize(name, LOWERCASE_FIRST_LETTER);
}
return name;
}

@Override
Expand Down Expand Up @@ -148,6 +183,20 @@ public void setStringEnums(Boolean stringEnums) {
this.stringEnums = stringEnums;
}

/**
* Set the file naming type.
*
* @param fileNaming the file naming to use
*/
public void setFileNaming(String fileNaming) {
if (PASCAL_CASE.equals(fileNaming) || CAMEL_CASE.equals(fileNaming) || KEBAB_CASE.equals(fileNaming)) {
this.fileNaming = fileNaming;
} else {
throw new IllegalArgumentException("Invalid file naming '" +
fileNaming + "'. Must be 'PascalCase', 'camelCase' or 'kebab-case'");
}
}

public Boolean getSagasAndRecords() {
return sagasAndRecords;
}
Expand Down Expand Up @@ -250,6 +299,10 @@ public void processOpts() {
this.setStringEnums(convertPropertyToBoolean(STRING_ENUMS));
}

if (additionalProperties.containsKey(FILE_NAMING)) {
this.setFileNaming(additionalProperties.get(FILE_NAMING).toString());
}

if (!withoutRuntimeChecks) {
this.modelTemplateFiles.put("models.mustache", ".ts");
typeMapping.put("date", "Date");
Expand Down Expand Up @@ -367,7 +420,7 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
for (ModelMap model : entry.getModels()) {
ExtendedCodegenModel codegenModel = (ExtendedCodegenModel) model.getModel();
model.put("hasImports", codegenModel.imports.size() > 0);

model.put("tsImports", toTsImports(codegenModel, parseImports(codegenModel)));
allModels.add(codegenModel);
if (codegenModel.isEntity) {
entityModelClassnames.add(codegenModel.classname);
Expand Down Expand Up @@ -401,6 +454,38 @@ public Map<String, ModelsMap> postProcessAllModels(Map<String, ModelsMap> objs)
return result;
}

/**
* Parse imports
*/
private Set<String> parseImports(CodegenModel cm) {
Set<String> newImports = new HashSet<>();
if (cm.imports.size() > 0) {
for (String name : cm.imports) {
if (name.indexOf(" | ") >= 0) {
String[] parts = name.split(" \\| ");
Collections.addAll(newImports, parts);
} else {
newImports.add(name);
}
}
}
return newImports;
}

private List<Map<String, String>> toTsImports(CodegenModel cm, Set<String> imports) {
List<Map<String, String>> tsImports = new ArrayList<>();
for (String im : imports) {
if (!im.equals(cm.classname)) {
HashMap<String, String> tsImport = new HashMap<>();
// TVG: This is used as class name in the import statements of the model file
tsImport.put("classname", im);
tsImport.put("filename", toModelFilename(im));
tsImports.add(tsImport);
}
}
return tsImports;
}

private void autoSetDefaultValueForProperty(ExtendedCodegenProperty var) {
if (var.isArray || var.isModel) {
var.defaultValue = var.dataTypeAlternate + "()";
Expand Down
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { mapValues } from '../runtime{{importFileExtension}}';
{{#hasImports}}
{{#imports}}
import type { {{{.}}} } from './{{.}}{{importFileExtension}}';
{{#tsImports}}
import type { {{{classname}}} } from './{{filename}}{{importFileExtension}}';
import {
{{.}}FromJSON,
{{.}}FromJSONTyped,
{{.}}ToJSON,
} from './{{.}}{{importFileExtension}}';
{{/imports}}
{{classname}}FromJSON,
{{classname}}FromJSONTyped,
{{classname}}ToJSON,
} from './{{filename}}{{importFileExtension}}';
{{/tsImports}}

{{/hasImports}}
{{#discriminator}}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,9 @@

import java.util.Map;

import static org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.CAMEL_CASE;
import static org.openapitools.codegen.languages.TypeScriptFetchClientCodegen.PASCAL_CASE;

public class TypeScriptFetchClientOptionsProvider implements OptionsProvider {
public static final String SORT_PARAMS_VALUE = "false";
public static final String SORT_MODEL_PROPERTIES_VALUE = "false";
Expand All @@ -32,9 +35,9 @@ public class TypeScriptFetchClientOptionsProvider implements OptionsProvider {
public static final String IMPORT_FILE_EXTENSION_VALUE = "";
public static final Boolean NULL_SAFE_ADDITIONAL_PROPS_VALUE = false;
public static final String ENUM_NAME_SUFFIX = "Enum";
public static final String MODEL_PROPERTY_NAMING_VALUE = "camelCase";
public static final String PARAM_NAMING_VALUE = "camelCase";
public static final String ENUM_PROPERTY_NAMING_VALUE = "PascalCase";
public static final String MODEL_PROPERTY_NAMING_VALUE = CAMEL_CASE;
public static final String PARAM_NAMING_VALUE = CAMEL_CASE;
public static final String ENUM_PROPERTY_NAMING_VALUE = PASCAL_CASE;
private static final String NMP_NAME = "npmName";
private static final String NMP_VERSION = "1.0.0";
private static final String NPM_REPOSITORY = "https://registry.npmjs.org";
Expand All @@ -44,6 +47,7 @@ public class TypeScriptFetchClientOptionsProvider implements OptionsProvider {
public static final String SAGAS_AND_RECORDS = "false";
public static final String ENUM_UNKNOWN_DEFAULT_CASE_VALUE = "false";
public static final String STRING_ENUMS = "false";
public static final String FILE_NAMING_VALUE = PASCAL_CASE;
public static final String ENUM_PROPERTY_NAMING_REPLACE_SPECIAL_CHAR_VALUE = "false";

@Override
Expand Down Expand Up @@ -74,6 +78,7 @@ public Map<String, String> createOptions() {
.put(TypeScriptFetchClientCodegen.WITHOUT_RUNTIME_CHECKS, WITHOUT_RUNTIME_CHECKS)
.put(TypeScriptFetchClientCodegen.SAGAS_AND_RECORDS, SAGAS_AND_RECORDS)
.put(TypeScriptFetchClientCodegen.IMPORT_FILE_EXTENSION_SWITCH, IMPORT_FILE_EXTENSION_VALUE)
.put(TypeScriptFetchClientCodegen.FILE_NAMING, FILE_NAMING_VALUE)
.put(CodegenConstants.ALLOW_UNICODE_IDENTIFIERS, ALLOW_UNICODE_IDENTIFIERS_VALUE)
.put(CodegenConstants.PREPEND_FORM_OR_BODY_PARAMETERS, PREPEND_FORM_OR_BODY_PARAMETERS_VALUE)
.put(CodegenConstants.LEGACY_DISCRIMINATOR_BEHAVIOR, "true")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import io.swagger.v3.oas.models.media.MapSchema;
import io.swagger.v3.oas.models.media.Schema;
import io.swagger.v3.oas.models.media.StringSchema;
import org.apache.commons.lang3.StringUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.config.CodegenConfigurator;
import org.openapitools.codegen.ClientOptInput;
import org.openapitools.codegen.CodegenConstants;
Expand All @@ -24,11 +26,11 @@
import java.io.File;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import static org.assertj.core.api.Assertions.assertThat;

@Test(groups = {TypeScriptGroups.TYPESCRIPT, TypeScriptGroups.TYPESCRIPT_FETCH})
Expand Down Expand Up @@ -221,4 +223,99 @@ public void doesNotContainESMTSConfigFileInCaseOfES5AndNPM() {
assertThat(codegen.supportingFiles()).contains(new SupportingFile("tsconfig.mustache", "", "tsconfig.json"));
assertThat(codegen.supportingFiles()).doesNotContain(new SupportingFile("tsconfig.esm.mustache", "", "tsconfig.esm.json"));
}

@Test(description = "Verify file name formatting from model name in PascalCase")
public void testModelFileNameInPascalCase() {
final TypeScriptFetchClientCodegen codegen = new TypeScriptFetchClientCodegen();
codegen.setFileNaming(TypeScriptFetchClientCodegen.PASCAL_CASE);
Assert.assertEquals("FirstSimpleModel", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNameSuffix("suffix");
Assert.assertEquals("FirstSimpleModelSuffix", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNamePrefix("prefix");
Assert.assertEquals("PrefixFirstSimpleModelSuffix", codegen.toModelFilename("FirstSimpleModel"));
}

@Test(description = "Verify file name formatting from model name in camelCase")
public void testModelFileNameInCamelCase() {
final TypeScriptFetchClientCodegen codegen = new TypeScriptFetchClientCodegen();
codegen.setFileNaming(TypeScriptFetchClientCodegen.CAMEL_CASE);
Assert.assertEquals("firstSimpleModel", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNameSuffix("suffix");
Assert.assertEquals("firstSimpleModelSuffix", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNamePrefix("prefix");
Assert.assertEquals("prefixFirstSimpleModelSuffix", codegen.toModelFilename("FirstSimpleModel"));
}

@Test(description = "Verify file name formatting from model name in kebab-case")
public void testModelFileNameInKebabCase() {
final TypeScriptFetchClientCodegen codegen = new TypeScriptFetchClientCodegen();
codegen.setFileNaming("kebab-case");
Assert.assertEquals("first-simple-model", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNameSuffix("suffix");
Assert.assertEquals("first-simple-model-suffix", codegen.toModelFilename("FirstSimpleModel"));
codegen.setModelNamePrefix("prefix");
Assert.assertEquals("prefix-first-simple-model-suffix", codegen.toModelFilename("FirstSimpleModel"));
}

@Test(description = "Verify file name formatting from api name in PascalCase, camelCase and kebab-case")
public void testApiFileNameInVariousFormat() {
final TypeScriptFetchClientCodegen codegen = new TypeScriptFetchClientCodegen();
codegen.setFileNaming(TypeScriptFetchClientCodegen.PASCAL_CASE);
String prefix = codegen.getApiNamePrefix() != null ? codegen.getApiNamePrefix() : "";
String suffix = codegen.getApiNameSuffix() != null ? codegen.getApiNameSuffix() : "";
Assert.assertEquals(StringUtils.capitalize(prefix + "FirstSimpleController") + StringUtils.capitalize(suffix),
codegen.toApiFilename("FirstSimpleController"));
codegen.setFileNaming(TypeScriptFetchClientCodegen.CAMEL_CASE);
Assert.assertEquals(StringUtils.uncapitalize(prefix + "FirstSimpleController") + StringUtils.capitalize(suffix),
codegen.toApiFilename("FirstSimpleController"));
codegen.setFileNaming(TypeScriptFetchClientCodegen.KEBAB_CASE);
Assert.assertEquals((prefix.isBlank() ? "" : (StringUtils.lowerCase(suffix) + "-")) + "first-simple-controller" + (suffix.isBlank() ? "" : ("-" + StringUtils.lowerCase(suffix))),
codegen.toApiFilename("FirstSimpleController"));
}

@Test(description = "Verify names of files generated in kebab-case and imports")
public void testGeneratedFilenamesInKebabCase() throws IOException {

Map<String, Object> properties = new HashMap<>();
properties.put("fileNaming", TypeScriptFetchClientCodegen.KEBAB_CASE);

File output = generate(properties);

Path pet = Paths.get(output + "/models/pet.ts");
TestUtils.assertFileExists(pet);
TestUtils.assertFileContains(pet, "} from './pet-category';");
TestUtils.assertFileExists(Paths.get(output + "/models/pet-category.ts"));
TestUtils.assertFileExists(Paths.get(output + "/apis/pet-controller-api.ts"));
}

@Test(description = "Verify names of files generated in camelCase and imports")
public void testGeneratedFilenamesInCamelCase() throws IOException {

Map<String, Object> properties = new HashMap<>();
properties.put("fileNaming", TypeScriptFetchClientCodegen.CAMEL_CASE);

File output = generate(properties);

Path pet = Paths.get(output + "/models/pet.ts");
TestUtils.assertFileExists(pet);
TestUtils.assertFileContains(pet, "} from './petCategory';");
TestUtils.assertFileExists(Paths.get(output + "/models/petCategory.ts"));
TestUtils.assertFileExists(Paths.get(output + "/apis/petControllerApi.ts"));
}

private static File generate(Map<String, Object> properties) throws IOException {
File output = Files.createTempDirectory("test").toFile();
output.deleteOnExit();

final CodegenConfigurator configurator = new CodegenConfigurator()
.setGeneratorName("typescript-fetch")
.setInputSpec("src/test/resources/3_0/typescript-fetch/example-for-file-naming-option.yaml")
.setAdditionalProperties(properties)
.setOutputDir(output.getAbsolutePath().replace("\\", "/"));

Generator generator = new DefaultGenerator();
List<File> files = generator.opts(configurator.toClientOptInput()).generate();
files.forEach(File::deleteOnExit);
return output;
}
}
Loading

0 comments on commit 0e809d4

Please sign in to comment.