[Crystal] Allow double colons in model name (#13736)

This commit is contained in:
Chao Yang 2022-10-18 21:55:59 -05:00 committed by GitHub
parent ad2169ea33
commit b2e8a15d9f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 208 additions and 46 deletions

View File

@ -84,27 +84,21 @@ public class CrystalClientCodegen extends DefaultCodegen {
SecurityFeature.BasicAuth,
SecurityFeature.BearerToken,
SecurityFeature.ApiKey,
SecurityFeature.OAuth2_Implicit
))
SecurityFeature.OAuth2_Implicit))
.excludeGlobalFeatures(
GlobalFeature.XMLStructureDefinitions,
GlobalFeature.Callbacks,
GlobalFeature.LinkObjects,
GlobalFeature.ParameterStyling,
GlobalFeature.ParameterizedServer,
GlobalFeature.MultiServer
)
GlobalFeature.MultiServer)
.includeSchemaSupportFeatures(
SchemaSupportFeature.Polymorphism
)
SchemaSupportFeature.Polymorphism)
.excludeParameterFeatures(
ParameterFeature.Cookie
)
ParameterFeature.Cookie)
.includeClientModificationFeatures(
ClientModificationFeature.BasePath,
ClientModificationFeature.UserAgent
)
);
ClientModificationFeature.UserAgent));
generatorMetadata = GeneratorMetadata.newBuilder(generatorMetadata)
.stability(Stability.BETA)
@ -128,13 +122,14 @@ public class CrystalClientCodegen extends DefaultCodegen {
apiTestTemplateFiles.put("api_test.mustache", ".cr");
// TODO support auto-generated doc
//modelDocTemplateFiles.put("model_doc.mustache", ".md");
//apiDocTemplateFiles.put("api_doc.mustache", ".md");
// modelDocTemplateFiles.put("model_doc.mustache", ".md");
// apiDocTemplateFiles.put("api_doc.mustache", ".md");
// default HIDE_GENERATION_TIMESTAMP to true
hideGenerationTimestamp = Boolean.TRUE;
// reserved word. Ref: https://github.com/crystal-lang/crystal/wiki/Crystal-for-Rubyists#available-keywords
// reserved word. Ref:
// https://github.com/crystal-lang/crystal/wiki/Crystal-for-Rubyists#available-keywords
reservedWords = new HashSet<>(
Arrays.asList(
"abstract", "annotation", "do", "if", "nil?", "select", "union",
@ -146,8 +141,7 @@ public class CrystalClientCodegen extends DefaultCodegen {
"break", "extend", "macro", "require", "true", "with",
"case", "false", "module", "rescue", "type", "yield",
"class", "for", "next", "responds_to?", "typeof",
"def", "fun", "nil", "return", "uninitialized")
);
"def", "fun", "nil", "return", "uninitialized"));
languageSpecificPrimitives.clear();
languageSpecificPrimitives.add("String");
@ -195,29 +189,25 @@ public class CrystalClientCodegen extends DefaultCodegen {
cliOptions.removeIf(opt -> CodegenConstants.MODEL_PACKAGE.equals(opt.getOpt()) ||
CodegenConstants.API_PACKAGE.equals(opt.getOpt()));
cliOptions.add(new CliOption(SHARD_NAME, "shard name (e.g. twitter_client").
defaultValue("openapi_client"));
cliOptions.add(new CliOption(SHARD_NAME, "shard name (e.g. twitter_client").defaultValue("openapi_client"));
cliOptions.add(new CliOption(MODULE_NAME, "module name (e.g. TwitterClient").
defaultValue("OpenAPIClient"));
cliOptions.add(new CliOption(MODULE_NAME, "module name (e.g. TwitterClient").defaultValue("OpenAPIClient"));
cliOptions.add(new CliOption(SHARD_VERSION, "shard version.").defaultValue("1.0.0"));
cliOptions.add(new CliOption(SHARD_LICENSE, "shard license.").
defaultValue("unlicense"));
cliOptions.add(new CliOption(SHARD_LICENSE, "shard license.").defaultValue("unlicense"));
cliOptions.add(new CliOption(SHARD_HOMEPAGE, "shard homepage.").
defaultValue("http://org.openapitools"));
cliOptions.add(new CliOption(SHARD_HOMEPAGE, "shard homepage.").defaultValue("http://org.openapitools"));
cliOptions.add(new CliOption(SHARD_DESCRIPTION, "shard description.").
defaultValue("This shard maps to a REST API"));
cliOptions.add(
new CliOption(SHARD_DESCRIPTION, "shard description.").defaultValue("This shard maps to a REST API"));
cliOptions.add(new CliOption(SHARD_AUTHOR, "shard author (only one is supported)."));
cliOptions.add(new CliOption(SHARD_AUTHOR_EMAIL, "shard author email (only one is supported)."));
cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP, CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC).
defaultValue(Boolean.TRUE.toString()));
cliOptions.add(new CliOption(CodegenConstants.HIDE_GENERATION_TIMESTAMP,
CodegenConstants.HIDE_GENERATION_TIMESTAMP_DESC).defaultValue(Boolean.TRUE.toString()));
}
@Override
@ -225,7 +215,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
super.processOpts();
if (StringUtils.isEmpty(System.getenv("CRYSTAL_POST_PROCESS_FILE"))) {
LOGGER.info("Hint: Environment variable 'CRYSTAL_POST_PROCESS_FILE' (optional) not defined. E.g. to format the source code, please try 'export CRYSTAL_POST_PROCESS_FILE=\"/usr/local/bin/crystal tool format\"' (Linux/Mac)");
LOGGER.info(
"Hint: Environment variable 'CRYSTAL_POST_PROCESS_FILE' (optional) not defined. E.g. to format the source code, please try 'export CRYSTAL_POST_PROCESS_FILE=\"/usr/local/bin/crystal tool format\"' (Linux/Mac)");
}
if (additionalProperties.containsKey(SHARD_NAME)) {
@ -314,12 +305,14 @@ public class CrystalClientCodegen extends DefaultCodegen {
@Override
public String apiFileFolder() {
return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator + apiPackage.replace("/", File.separator);
return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator
+ apiPackage.replace("/", File.separator);
}
@Override
public String modelFileFolder() {
return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator + modelPackage.replace("/", File.separator);
return outputFolder + File.separator + srcFolder + File.separator + shardName + File.separator
+ modelPackage.replace("/", File.separator);
}
@Override
@ -365,7 +358,7 @@ public class CrystalClientCodegen extends DefaultCodegen {
@Override
public String toModelName(final String name) {
String modelName;
modelName = sanitizeName(name);
modelName = sanitizeModelName(name);
if (!StringUtils.isEmpty(modelNamePrefix)) {
modelName = modelNamePrefix + "_" + modelName;
@ -394,6 +387,15 @@ public class CrystalClientCodegen extends DefaultCodegen {
return camelize(modelName);
}
public String sanitizeModelName(String modelName) {
String[] parts = modelName.split("::");
ArrayList<String> new_parts = new ArrayList<String>();
for (String part : parts) {
new_parts.add(sanitizeName(part));
}
return String.join("::", new_parts);
}
@Override
public String toModelFilename(String name) {
return underscore(toModelName(name));
@ -511,7 +513,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
// operationId starts with a number
if (operationId.matches("^\\d.*")) {
LOGGER.warn("{} (starting with a number) cannot be used as method name. Renamed to {}", operationId, underscore(sanitizeName("call_" + operationId)));
LOGGER.warn("{} (starting with a number) cannot be used as method name. Renamed to {}", operationId,
underscore(sanitizeName("call_" + operationId)));
operationId = "call_" + operationId;
}
@ -610,7 +613,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
return objs;
}
private String constructExampleCode(CodegenParameter codegenParameter, HashMap<String, CodegenModel> modelMaps, HashMap<String, Integer> processedModelMap) {
private String constructExampleCode(CodegenParameter codegenParameter, HashMap<String, CodegenModel> modelMaps,
HashMap<String, Integer> processedModelMap) {
if (codegenParameter.isArray) { // array
if (codegenParameter.items == null) {
return "[]";
@ -670,13 +674,15 @@ public class CrystalClientCodegen extends DefaultCodegen {
if (modelMaps.containsKey(codegenParameter.dataType)) {
return constructExampleCode(modelMaps.get(codegenParameter.dataType), modelMaps, processedModelMap);
} else {
//LOGGER.error("Error in constructing examples. Failed to look up the model " + codegenParameter.dataType);
// LOGGER.error("Error in constructing examples. Failed to look up the model " +
// codegenParameter.dataType);
return "TODO";
}
}
}
private String constructExampleCode(CodegenProperty codegenProperty, HashMap<String, CodegenModel> modelMaps, HashMap<String, Integer> processedModelMap) {
private String constructExampleCode(CodegenProperty codegenProperty, HashMap<String, CodegenModel> modelMaps,
HashMap<String, Integer> processedModelMap) {
if (codegenProperty.isArray) { // array
return "[" + constructExampleCode(codegenProperty.items, modelMaps, processedModelMap) + "]";
} else if (codegenProperty.isMap) {
@ -736,14 +742,17 @@ public class CrystalClientCodegen extends DefaultCodegen {
if (modelMaps.containsKey(codegenProperty.dataType)) {
return constructExampleCode(modelMaps.get(codegenProperty.dataType), modelMaps, processedModelMap);
} else {
//LOGGER.error("Error in constructing examples. Failed to look up the model " + codegenParameter.dataType);
// LOGGER.error("Error in constructing examples. Failed to look up the model " +
// codegenParameter.dataType);
return "TODO";
}
}
}
private String constructExampleCode(CodegenModel codegenModel, HashMap<String, CodegenModel> modelMaps, HashMap<String, Integer> processedModelMap) {
// break infinite recursion. Return, in case a model is already processed in the current context.
private String constructExampleCode(CodegenModel codegenModel, HashMap<String, CodegenModel> modelMaps,
HashMap<String, Integer> processedModelMap) {
// break infinite recursion. Return, in case a model is already processed in the
// current context.
String model = codegenModel.name;
if (processedModelMap.containsKey(model)) {
int count = processedModelMap.get(model);
@ -755,7 +764,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
throw new RuntimeException("Invalid count when constructing example: " + count);
}
} else if (codegenModel.isEnum) {
List<Map<String, String>> enumVars = (List<Map<String, String>>) codegenModel.allowableValues.get("enumVars");
List<Map<String, String>> enumVars = (List<Map<String, String>>) codegenModel.allowableValues
.get("enumVars");
return moduleName + "::" + codegenModel.classname + "::" + enumVars.get(0).get("name");
} else if (codegenModel.oneOf != null && !codegenModel.oneOf.isEmpty()) {
String subModel = (String) codegenModel.oneOf.toArray()[0];
@ -767,7 +777,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
List<String> propertyExamples = new ArrayList<>();
for (CodegenProperty codegenProperty : codegenModel.requiredVars) {
propertyExamples.add(codegenProperty.name + ": " + constructExampleCode(codegenProperty, modelMaps, processedModelMap));
propertyExamples.add(
codegenProperty.name + ": " + constructExampleCode(codegenProperty, modelMaps, processedModelMap));
}
String example = moduleName + "::" + toModelName(model) + ".new";
if (!propertyExamples.isEmpty()) {
@ -827,7 +838,8 @@ public class CrystalClientCodegen extends DefaultCodegen {
LocalDate localDate = date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate();
return "Date.parse(\"" + String.format(Locale.ROOT, localDate.toString(), "") + "\")";
} else if (p.getDefault() instanceof java.time.OffsetDateTime) {
return "Time.parse(\"" + String.format(Locale.ROOT, ((java.time.OffsetDateTime) p.getDefault()).atZoneSameInstant(ZoneId.systemDefault()).toString(), "") + "\")";
return "Time.parse(\"" + String.format(Locale.ROOT, ((java.time.OffsetDateTime) p.getDefault())
.atZoneSameInstant(ZoneId.systemDefault()).toString(), "") + "\")";
} else {
return "\"" + escapeText((String.valueOf(p.getDefault()))) + "\"";
}
@ -902,14 +914,16 @@ public class CrystalClientCodegen extends DefaultCodegen {
Process p = Runtime.getRuntime().exec(command);
int exitValue = p.waitFor();
if (exitValue != 0) {
try (InputStreamReader inputStreamReader = new InputStreamReader(p.getErrorStream(), StandardCharsets.UTF_8);
try (InputStreamReader inputStreamReader = new InputStreamReader(p.getErrorStream(),
StandardCharsets.UTF_8);
BufferedReader br = new BufferedReader(inputStreamReader)) {
StringBuilder sb = new StringBuilder();
String line;
while ((line = br.readLine()) != null) {
sb.append(line);
}
LOGGER.error("Error running the command ({}). Exit value: {}, Error output: {}", command, exitValue, sb);
LOGGER.error("Error running the command ({}). Exit value: {}, Error output: {}", command,
exitValue, sb);
}
} else {
LOGGER.info("Successfully executed: {}", command);
@ -923,5 +937,7 @@ public class CrystalClientCodegen extends DefaultCodegen {
}
@Override
public GeneratorLanguage generatorLanguage() { return GeneratorLanguage.CRYSTAL; }
public GeneratorLanguage generatorLanguage() {
return GeneratorLanguage.CRYSTAL;
}
}

View File

@ -0,0 +1,146 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.openapitools.codegen.crystal;
import io.swagger.v3.oas.models.OpenAPI;
import org.apache.commons.io.FileUtils;
import org.openapitools.codegen.*;
import org.openapitools.codegen.languages.CrystalClientCodegen;
import org.testng.Assert;
import org.testng.annotations.Test;
import java.io.File;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.util.*;
import static org.testng.Assert.assertTrue;
import static org.testng.Assert.fail;
/**
* Tests for CrystalClientCodegen-generated templates
*/
public class CrystalClientCodegenTest {
@Test
public void testGenerateCrystalClientWithHtmlEntity() throws Exception {
final File output = Files.createTempDirectory("test").toFile();
output.mkdirs();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/pathWithHtmlEntity.yaml");
CodegenConfig codegenConfig = new CrystalClientCodegen();
codegenConfig.setOutputDir(output.getAbsolutePath());
ClientOptInput clientOptInput = new ClientOptInput().openAPI(openAPI).config(codegenConfig);
DefaultGenerator generator = new DefaultGenerator();
List<File> files = generator.opts(clientOptInput).generate();
boolean apiFileGenerated = false;
for (File file : files) {
if (file.getName().equals("default_api.cr")) {
apiFileGenerated = true;
// Crystal client should set the path unescaped in the api file
assertTrue(FileUtils.readFileToString(file, StandardCharsets.UTF_8)
.contains("local_var_path = \"/foo=bar\""));
}
}
if (!apiFileGenerated) {
fail("Default api file is not generated!");
}
}
@Test
public void testInitialConfigValues() throws Exception {
final CrystalClientCodegen codegen = new CrystalClientCodegen();
codegen.processOpts();
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP),
Boolean.TRUE);
Assert.assertEquals(codegen.isHideGenerationTimestamp(), true);
Assert.assertEquals(codegen.modelPackage(), "models");
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.MODEL_PACKAGE), null);
Assert.assertEquals(codegen.apiPackage(), "api");
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.API_PACKAGE), null);
}
@Test
public void testSettersForConfigValues() throws Exception {
final CrystalClientCodegen codegen = new CrystalClientCodegen();
codegen.setHideGenerationTimestamp(false);
codegen.processOpts();
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP),
Boolean.FALSE);
Assert.assertEquals(codegen.isHideGenerationTimestamp(), false);
}
@Test
public void testAdditionalPropertiesPutForConfigValues() throws Exception {
final CrystalClientCodegen codegen = new CrystalClientCodegen();
codegen.additionalProperties().put(CodegenConstants.HIDE_GENERATION_TIMESTAMP, false);
codegen.additionalProperties().put(CodegenConstants.MODEL_PACKAGE, "crystal-models");
codegen.additionalProperties().put(CodegenConstants.API_PACKAGE, "crystal-api");
codegen.processOpts();
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.HIDE_GENERATION_TIMESTAMP),
Boolean.FALSE);
Assert.assertEquals(codegen.isHideGenerationTimestamp(), false);
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.MODEL_PACKAGE), "crystal-models");
Assert.assertEquals(codegen.additionalProperties().get(CodegenConstants.API_PACKAGE), "crystal-api");
}
@Test
public void testBooleanDefaultValue() throws Exception {
final File output = Files.createTempDirectory("test").toFile();
output.mkdirs();
output.deleteOnExit();
final OpenAPI openAPI = TestUtils.parseFlattenSpec("src/test/resources/2_0/npe1.yaml");
CodegenConfig codegenConfig = new CrystalClientCodegen();
codegenConfig.setOutputDir(output.getAbsolutePath());
ClientOptInput clientOptInput = new ClientOptInput().openAPI(openAPI).config(codegenConfig);
DefaultGenerator generator = new DefaultGenerator();
List<File> files = generator.opts(clientOptInput).generate();
boolean apiFileGenerated = false;
for (File file : files) {
if (file.getName().equals("default_api.cr")) {
apiFileGenerated = true;
// Crystal client should set the path unescaped in the api file
assertTrue(FileUtils.readFileToString(file, StandardCharsets.UTF_8)
.contains("local_var_path = \"/default/Resources/{id}\""));
}
}
if (!apiFileGenerated) {
fail("Default api file is not generated!");
}
}
@Test
public void testSanitizeModelName() throws Exception {
final CrystalClientCodegen codegen = new CrystalClientCodegen();
codegen.setHideGenerationTimestamp(false);
codegen.processOpts();
Assert.assertEquals(codegen.sanitizeModelName("JSON::Any"), "JSON::Any");
// Disallows single colons
Assert.assertEquals(codegen.sanitizeModelName("JSON:Any"), "JSONAny");
}
}