diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java index 2d1e3c46537..68358071449 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/CrystalClientCodegen.java @@ -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 new_parts = new ArrayList(); + 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 modelMaps, HashMap processedModelMap) { + private String constructExampleCode(CodegenParameter codegenParameter, HashMap modelMaps, + HashMap 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 modelMaps, HashMap processedModelMap) { + private String constructExampleCode(CodegenProperty codegenProperty, HashMap modelMaps, + HashMap 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 modelMaps, HashMap processedModelMap) { - // break infinite recursion. Return, in case a model is already processed in the current context. + private String constructExampleCode(CodegenModel codegenModel, HashMap modelMaps, + HashMap 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> enumVars = (List>) codegenModel.allowableValues.get("enumVars"); + List> enumVars = (List>) 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 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); - BufferedReader br = new BufferedReader(inputStreamReader)) { + 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; + } } diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/crystal/CrystalClientCodegenTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/crystal/CrystalClientCodegenTest.java new file mode 100644 index 00000000000..98f2385d5c5 --- /dev/null +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/crystal/CrystalClientCodegenTest.java @@ -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 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 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"); + } +}