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