diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSServerCodegen.java new file mode 100644 index 00000000000..a0d279a82cd --- /dev/null +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/NodeJSServerCodegen.java @@ -0,0 +1,459 @@ +package org.openapitools.codegen.languages; + +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Lists; +import com.google.common.collect.Multimap; + +import org.openapitools.codegen.*; +import org.openapitools.codegen.utils.*; +import org.openapitools.codegen.mustache.*; +import io.swagger.v3.oas.models.security.SecurityScheme; +import io.swagger.v3.oas.models.*; +import io.swagger.v3.oas.models.media.*; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.oas.models.parameters.*; +import io.swagger.v3.oas.models.info.*; +import io.swagger.v3.oas.models.PathItem.*; +import io.swagger.v3.oas.models.Paths; +import io.swagger.v3.core.util.Yaml; +import io.swagger.v3.parser.util.SchemaTypeUtil; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.io.File; +import java.io.IOException; +import java.math.BigDecimal; +import java.net.URL; +import java.util.*; +import java.util.Map.Entry; +import java.util.regex.Pattern; +import org.apache.commons.lang3.StringUtils; + +public class NodeJSServerCodegen extends DefaultCodegen implements CodegenConfig { + + private static final Logger LOGGER = LoggerFactory.getLogger(NodeJSServerCodegen.class); + protected String implFolder = "service"; + public static final String GOOGLE_CLOUD_FUNCTIONS = "googleCloudFunctions"; + public static final String EXPORTED_NAME = "exportedName"; + public static final String SERVER_PORT = "serverPort"; + + protected String apiVersion = "1.0.0"; + protected String projectName = "swagger-server"; + protected String defaultServerPort = "8080"; + + protected boolean googleCloudFunctions; + protected String exportedName; + + public NodeJSServerCodegen() { + super(); + + // set the output folder here + outputFolder = "generated-code/nodejs"; + + /* + * Models. You can write model files using the modelTemplateFiles map. + * if you want to create one template for file, you can do so here. + * for multiple files for model, just put another entry in the `modelTemplateFiles` with + * a different extension + */ + modelTemplateFiles.clear(); + + /* + * Api classes. You can write classes for each Api file with the apiTemplateFiles map. + * as with models, add multiple entries with different extensions for multiple files per + * class + */ + apiTemplateFiles.put( + "controller.mustache", // the template to use + ".js"); // the extension for each file to write + + /* + * Template Location. This is the location which templates will be read from. The generator + * will use the resource stream to attempt to read the templates. + */ + embeddedTemplateDir = templateDir = "nodejs"; + + /* + * Reserved words. Override this with reserved words specific to your language + */ + setReservedWordsLowerCase( + Arrays.asList( + "break", "case", "class", "catch", "const", "continue", "debugger", + "default", "delete", "do", "else", "export", "extends", "finally", + "for", "function", "if", "import", "in", "instanceof", "let", "new", + "return", "super", "switch", "this", "throw", "try", "typeof", "var", + "void", "while", "with", "yield") + ); + + /* + * Additional Properties. These values can be passed to the templates and + * are available in models, apis, and supporting files + */ + additionalProperties.put("apiVersion", apiVersion); + additionalProperties.put("implFolder", implFolder); + + supportingFiles.add(new SupportingFile("writer.mustache", ("utils").replace(".", File.separator), "writer.js")); + + cliOptions.add(CliOption.newBoolean(GOOGLE_CLOUD_FUNCTIONS, + "When specified, it will generate the code which runs within Google Cloud Functions " + + "instead of standalone Node.JS server. See " + + "https://cloud.google.com/functions/docs/quickstart for the details of how to " + + "deploy the generated code.")); + cliOptions.add(new CliOption(EXPORTED_NAME, + "When the generated code will be deployed to Google Cloud Functions, this option can be " + + "used to update the name of the exported function. By default, it refers to the " + + "basePath. This does not affect normal standalone nodejs server code.")); + cliOptions.add(new CliOption(SERVER_PORT, + "TCP port to listen on.")); + } + + @Override + public String apiPackage() { + return "controllers"; + } + + /** + * Configures the type of generator. + * + * @return the CodegenType for this generator + * @see io.swagger.codegen.CodegenType + */ + @Override + public CodegenType getTag() { + return CodegenType.SERVER; + } + + /** + * Configures a friendly name for the generator. This will be used by the generator + * to select the library with the -l flag. + * + * @return the friendly name for the generator + */ + @Override + public String getName() { + return "nodejs-server"; + } + + /** + * Returns human-friendly help for the generator. Provide the consumer with help + * tips, parameters here + * + * @return A string value for the help message + */ + @Override + public String getHelp() { + return "Generates a nodejs server library using the swagger-tools project. By default, " + + "it will also generate service classes--which you can disable with the `-Dnoservice` environment variable."; + } + + @Override + public String toApiName(String name) { + if (name.length() == 0) { + return "DefaultController"; + } + return initialCaps(name); + } + + @Override + public String toApiFilename(String name) { + return toApiName(name); + } + + + @Override + public String apiFilename(String templateName, String tag) { + String result = super.apiFilename(templateName, tag); + + if (templateName.equals("service.mustache")) { + String stringToMatch = File.separator + "controllers" + File.separator; + String replacement = File.separator + implFolder + File.separator; + result = result.replaceAll(Pattern.quote(stringToMatch), replacement); + } + return result; + } + + private String implFileFolder(String output) { + return outputFolder + File.separator + output + File.separator + apiPackage().replace('.', File.separatorChar); + } + + /** + * Escapes a reserved word as defined in the `reservedWords` array. Handle escaping + * those terms here. This logic is only called if a variable matches the reserved words + * + * @return the escaped term + */ + @Override + public String escapeReservedWord(String name) { + if(this.reservedWordsMappings().containsKey(name)) { + return this.reservedWordsMappings().get(name); + } + return "_" + name; + } + + /** + * Location to write api files. You can use the apiPackage() as defined when the class is + * instantiated + */ + @Override + public String apiFileFolder() { + return outputFolder + File.separator + apiPackage().replace('.', File.separatorChar); + } + + public boolean getGoogleCloudFunctions() { + return googleCloudFunctions; + } + + public void setGoogleCloudFunctions(boolean value) { + googleCloudFunctions = value; + } + + public String getExportedName() { + return exportedName; + } + + public void setExportedName(String name) { + exportedName = name; + } + + @Override + public Map postProcessOperations(Map objs) { + @SuppressWarnings("unchecked") + Map objectMap = (Map) objs.get("operations"); + @SuppressWarnings("unchecked") + List operations = (List) objectMap.get("operation"); + for (CodegenOperation operation : operations) { + operation.httpMethod = operation.httpMethod.toLowerCase(); + + List params = operation.allParams; + if (params != null && params.size() == 0) { + operation.allParams = null; + } + List responses = operation.responses; + if (responses != null) { + for (CodegenResponse resp : responses) { + if ("0".equals(resp.code)) { + resp.code = "default"; + } + } + } + if (operation.examples != null && !operation.examples.isEmpty()) { + // Leave application/json* items only + for (Iterator> it = operation.examples.iterator(); it.hasNext(); ) { + final Map example = it.next(); + final String contentType = example.get("contentType"); + if (contentType == null || !contentType.startsWith("application/json")) { + it.remove(); + } + } + } + } + return objs; + } + + @SuppressWarnings("unchecked") + private static List> getOperations(Map objs) { + List> result = new ArrayList>(); + Map apiInfo = (Map) objs.get("apiInfo"); + List> apis = (List>) apiInfo.get("apis"); + for (Map api : apis) { + result.add((Map) api.get("operations")); + } + return result; + } + + private static List> sortOperationsByPath(List ops) { + Multimap opsByPath = ArrayListMultimap.create(); + + for (CodegenOperation op : ops) { + opsByPath.put(op.path, op); + } + + List> opsByPathList = new ArrayList>(); + for (Entry> entry : opsByPath.asMap().entrySet()) { + Map opsByPathEntry = new HashMap(); + opsByPathList.add(opsByPathEntry); + opsByPathEntry.put("path", entry.getKey()); + opsByPathEntry.put("operation", entry.getValue()); + List operationsForThisPath = Lists.newArrayList(entry.getValue()); + operationsForThisPath.get(operationsForThisPath.size() - 1).hasMore = false; + if (opsByPathList.size() < opsByPath.asMap().size()) { + opsByPathEntry.put("hasMore", "true"); + } + } + + return opsByPathList; + } + + @Override + public void processOpts() { + super.processOpts(); + + if (additionalProperties.containsKey(GOOGLE_CLOUD_FUNCTIONS)) { + setGoogleCloudFunctions( + Boolean.valueOf(additionalProperties.get(GOOGLE_CLOUD_FUNCTIONS).toString())); + } + + if (additionalProperties.containsKey(EXPORTED_NAME)) { + setExportedName((String)additionalProperties.get(EXPORTED_NAME)); + } + + /* + * Supporting Files. You can write single files for the generator with the + * entire object tree available. If the input file has a suffix of `.mustache + * it will be processed by the template engine. Otherwise, it will be copied + */ + // supportingFiles.add(new SupportingFile("controller.mustache", + // "controllers", + // "controller.js") + // ); + supportingFiles.add(new SupportingFile("swagger.mustache", + "api", + "swagger.yaml") + ); + if (getGoogleCloudFunctions()) { + writeOptional(outputFolder, new SupportingFile("index-gcf.mustache", "", "index.js")); + } else { + writeOptional(outputFolder, new SupportingFile("index.mustache", "", "index.js")); + } + writeOptional(outputFolder, new SupportingFile("package.mustache", "", "package.json")); + writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md")); + if (System.getProperty("noservice") == null) { + apiTemplateFiles.put( + "service.mustache", // the template to use + "Service.js"); // the extension for each file to write + } + } + + @Override + public void preprocessOpenAPI(OpenAPI openAPI) { + URL url = URLPathUtil.getServerURL(openAPI); + String host = URLPathUtil.LOCAL_HOST; + String port = defaultServerPort; + String basePath = null; + if (url != null) { + port = String.valueOf(url.getPort()); + host = url.getHost(); + basePath = url.getPath(); + } + + if (!StringUtils.isEmpty(host)) { + String[] parts = host.split(":"); + if (parts.length > 1) { + port = parts[1]; + } + } else { + // host is empty, default to https://localhost + host = "http://localhost"; + LOGGER.warn("'host' in the specification is empty or undefined. Default to http://localhost."); + } + + if (additionalProperties.containsKey(SERVER_PORT)) { + port = additionalProperties.get(SERVER_PORT).toString(); + } + this.additionalProperties.put(SERVER_PORT, port); + + if (openAPI.getInfo() != null) { + Info info = openAPI.getInfo(); + if (info.getTitle() != null) { + // when info.title is defined, use it for projectName + // used in package.json + projectName = info.getTitle() + .replaceAll("[^a-zA-Z0-9]", "-") + .replaceAll("^[-]*", "") + .replaceAll("[-]*$", "") + .replaceAll("[-]{2,}", "-") + .toLowerCase(); + this.additionalProperties.put("projectName", projectName); + } + } + + if (getGoogleCloudFunctions()) { + // Note that Cloud Functions don't allow customizing port name, simply checking host + // is good enough. + if (!host.endsWith(".cloudfunctions.net")) { + LOGGER.warn("Host " + host + " seems not matching with cloudfunctions.net URL."); + } + if (!additionalProperties.containsKey(EXPORTED_NAME)) { + if (basePath == null || basePath.equals("/")) { + LOGGER.warn("Cannot find the exported name properly. Using 'openapi' as the exported name"); + basePath = "/openapi"; + } + additionalProperties.put(EXPORTED_NAME, basePath.substring(1)); + } + } + + // need vendor extensions for x-swagger-router-controller + Paths paths = openAPI.getPaths(); + if (paths != null) { + for(String pathname : paths.keySet()) { + PathItem path = paths.get(pathname); + Map operationMap = path.readOperationsMap(); + if(operationMap != null) { + for(HttpMethod method : operationMap.keySet()) { + Operation operation = operationMap.get(method); + String tag = "default"; + if (operation.getTags() != null && operation.getTags().size() > 0) { + tag = toApiName(operation.getTags().get(0)); + } + if (operation.getOperationId() == null) { + operation.setOperationId(getOrGenerateOperationId(operation, pathname, method.toString())); + } + if (operation.getExtensions().get("x-openapi-router-controller") == null) { + operation.getExtensions().put("x-openapi-router-controller", sanitizeTag(tag)); + } + } + } + } + } + } + + @Override + public Map postProcessSupportingFileData(Map objs) { + OpenAPI openAPI = (OpenAPI)objs.get("openapi"); + if (openAPI != null) { + try { + SimpleModule module = new SimpleModule(); + module.addSerializer(Double.class, new JsonSerializer() { + @Override + public void serialize(Double val, JsonGenerator jgen, + SerializerProvider provider) throws IOException, JsonProcessingException { + jgen.writeNumber(new BigDecimal(val)); + } + }); + objs.put("swagger-yaml", Yaml.mapper().registerModule(module).writeValueAsString(openAPI)); + } catch (JsonProcessingException e) { + LOGGER.error(e.getMessage(), e); + } + } + for (Map operations : getOperations(objs)) { + @SuppressWarnings("unchecked") + List ops = (List) operations.get("operation"); + + List> opsByPathList = sortOperationsByPath(ops); + operations.put("operationsByPath", opsByPathList); + } + return super.postProcessSupportingFileData(objs); + } + + @Override + public String removeNonNameElementToCamelCase(String name) { + return removeNonNameElementToCamelCase(name, "[-:;#]"); + } + + @Override + public String escapeUnsafeCharacters(String input) { + return input.replace("*/", "*_/").replace("/*", "/_*"); + } + + @Override + public String escapeQuotationMark(String input) { + // remove " to avoid code injection + return input.replace("\"", ""); + } +} diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyOnRailsServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyOnRailsServerCodegen.java index 55ea5248836..35ad43913ff 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyOnRailsServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubyOnRailsServerCodegen.java @@ -177,12 +177,12 @@ public class RubyOnRailsServerCodegen extends DefaultCodegen implements CodegenC @Override public String getName() { - return "rails5"; + return "ruby-on-rails"; } @Override public String getHelp() { - return "Generates a Ruby on Rails server library."; + return "Generates a Ruby on Rails (v5) server library."; } @Override diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SinatraServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubySinatraServerCodegen.java similarity index 98% rename from modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SinatraServerCodegen.java rename to modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubySinatraServerCodegen.java index 9dfdb62828e..283094f30ca 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/SinatraServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/RubySinatraServerCodegen.java @@ -21,16 +21,16 @@ import org.apache.commons.lang3.StringUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; -public class SinatraServerCodegen extends DefaultCodegen implements CodegenConfig { +public class RubySinatraServerCodegen extends DefaultCodegen implements CodegenConfig { - private static final Logger LOGGER = LoggerFactory.getLogger(SinatraServerCodegen.class); + private static final Logger LOGGER = LoggerFactory.getLogger(RubySinatraServerCodegen.class); protected String gemName; protected String moduleName; protected String gemVersion = "1.0.0"; protected String libFolder = "lib"; - public SinatraServerCodegen() { + public RubySinatraServerCodegen() { super(); apiPackage = "lib"; outputFolder = "generated-code" + File.separator + "sinatra"; diff --git a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig index 7d78429b3ff..952b8bcae07 100644 --- a/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig +++ b/modules/openapi-generator/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -20,6 +20,7 @@ org.openapitools.codegen.languages.HaskellHttpClientCodegen org.openapitools.codegen.languages.HaskellServantCodegen org.openapitools.codegen.languages.JavascriptClientCodegen org.openapitools.codegen.languages.LuaClientCodegen +org.openapitools.codegen.languages.NodeJSServerCodegen org.openapitools.codegen.languages.ObjcClientCodegen org.openapitools.codegen.languages.PerlClientCodegen org.openapitools.codegen.languages.PhpClientCodegen @@ -32,10 +33,10 @@ org.openapitools.codegen.languages.PowerShellClientCodegen org.openapitools.codegen.languages.PythonClientCodegen org.openapitools.codegen.languages.PythonFlaskConnexionServerCodegen org.openapitools.codegen.languages.RClientCodegen -org.openapitools.codegen.languages.RubyOnRailsServerCodegen org.openapitools.codegen.languages.RubyClientCodegen +org.openapitools.codegen.languages.RubyOnRailsServerCodegen +org.openapitools.codegen.languages.RubySinatraServerCodegen org.openapitools.codegen.languages.ScalaClientCodegen -org.openapitools.codegen.languages.SinatraServerCodegen org.openapitools.codegen.languages.TizenClientCodegen org.openapitools.codegen.languages.TypeScriptAngularClientCodegen org.openapitools.codegen.languages.TypeScriptAngularJsClientCodegen diff --git a/modules/openapi-generator/src/test/java/org/openapitools/codegen/sinatra/SinatraServerOptionsTest.java b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubysinatra/RubySinatraServerOptionsTest.java similarity index 50% rename from modules/openapi-generator/src/test/java/org/openapitools/codegen/sinatra/SinatraServerOptionsTest.java rename to modules/openapi-generator/src/test/java/org/openapitools/codegen/rubysinatra/RubySinatraServerOptionsTest.java index 3f906bd8c3f..29f6a67dfca 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/codegen/sinatra/SinatraServerOptionsTest.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/codegen/rubysinatra/RubySinatraServerOptionsTest.java @@ -1,20 +1,20 @@ -package org.openapitools.codegen.sinatra; +package org.openapitools.codegen.rubysinatra; import org.openapitools.codegen.AbstractOptionsTest; import org.openapitools.codegen.CodegenConfig; -import org.openapitools.codegen.languages.SinatraServerCodegen; -import org.openapitools.codegen.options.SinatraServerOptionsProvider; +import org.openapitools.codegen.languages.RubySinatraServerCodegen; +import org.openapitools.codegen.options.RubySinatraServerOptionsProvider; import mockit.Expectations; import mockit.Tested; -public class SinatraServerOptionsTest extends AbstractOptionsTest { +public class RubySinatraServerOptionsTest extends AbstractOptionsTest { @Tested - private SinatraServerCodegen clientCodegen; + private RubySinatraServerCodegen clientCodegen; - public SinatraServerOptionsTest() { - super(new SinatraServerOptionsProvider()); + public RubySinatraServerOptionsTest() { + super(new RubySinatraServerOptionsProvider()); } @Override diff --git a/modules/openapi-generator/src/test/java/org/openapitools/options/SinatraServerOptionsProvider.java b/modules/openapi-generator/src/test/java/org/openapitools/options/RubySinatraServerOptionsProvider.java similarity index 86% rename from modules/openapi-generator/src/test/java/org/openapitools/options/SinatraServerOptionsProvider.java rename to modules/openapi-generator/src/test/java/org/openapitools/options/RubySinatraServerOptionsProvider.java index 51f1c2cef1d..e03e30e997b 100644 --- a/modules/openapi-generator/src/test/java/org/openapitools/options/SinatraServerOptionsProvider.java +++ b/modules/openapi-generator/src/test/java/org/openapitools/options/RubySinatraServerOptionsProvider.java @@ -4,7 +4,7 @@ import com.google.common.collect.ImmutableMap; import java.util.Map; -public class SinatraServerOptionsProvider implements OptionsProvider { +public class RubySinatraServerOptionsProvider implements OptionsProvider { @Override public String getLanguage() { return "ruby-sinatra";