add nodejs server support

This commit is contained in:
wing328 2018-03-30 10:05:20 +08:00
parent af6312efa5
commit c2759b393a
6 changed files with 475 additions and 15 deletions

View File

@ -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<String, Object> postProcessOperations(Map<String, Object> objs) {
@SuppressWarnings("unchecked")
Map<String, Object> objectMap = (Map<String, Object>) objs.get("operations");
@SuppressWarnings("unchecked")
List<CodegenOperation> operations = (List<CodegenOperation>) objectMap.get("operation");
for (CodegenOperation operation : operations) {
operation.httpMethod = operation.httpMethod.toLowerCase();
List<CodegenParameter> params = operation.allParams;
if (params != null && params.size() == 0) {
operation.allParams = null;
}
List<CodegenResponse> 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<Map<String, String>> it = operation.examples.iterator(); it.hasNext(); ) {
final Map<String, String> 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<Map<String, Object>> getOperations(Map<String, Object> objs) {
List<Map<String, Object>> result = new ArrayList<Map<String, Object>>();
Map<String, Object> apiInfo = (Map<String, Object>) objs.get("apiInfo");
List<Map<String, Object>> apis = (List<Map<String, Object>>) apiInfo.get("apis");
for (Map<String, Object> api : apis) {
result.add((Map<String, Object>) api.get("operations"));
}
return result;
}
private static List<Map<String, Object>> sortOperationsByPath(List<CodegenOperation> ops) {
Multimap<String, CodegenOperation> opsByPath = ArrayListMultimap.create();
for (CodegenOperation op : ops) {
opsByPath.put(op.path, op);
}
List<Map<String, Object>> opsByPathList = new ArrayList<Map<String, Object>>();
for (Entry<String, Collection<CodegenOperation>> entry : opsByPath.asMap().entrySet()) {
Map<String, Object> opsByPathEntry = new HashMap<String, Object>();
opsByPathList.add(opsByPathEntry);
opsByPathEntry.put("path", entry.getKey());
opsByPathEntry.put("operation", entry.getValue());
List<CodegenOperation> 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<HttpMethod, Operation> 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<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
OpenAPI openAPI = (OpenAPI)objs.get("openapi");
if (openAPI != null) {
try {
SimpleModule module = new SimpleModule();
module.addSerializer(Double.class, new JsonSerializer<Double>() {
@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<String, Object> operations : getOperations(objs)) {
@SuppressWarnings("unchecked")
List<CodegenOperation> ops = (List<CodegenOperation>) operations.get("operation");
List<Map<String, Object>> 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("\"", "");
}
}

View File

@ -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

View File

@ -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";

View File

@ -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

View File

@ -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

View File

@ -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";