Skip to content

Commit

Permalink
[Erlang] Add Erlang server generator (#3758)
Browse files Browse the repository at this point in the history
* Initial erlang generation

* Recfactor erlang codegen to make a minimal working example

* ft/erlang_codegen Separate handlers by resourse, add minor codegen fixes and refactoring

* Test commit

* ft/erlang_codegen Modify reouting generation

* ft/erlang_codegen Remove parsed request concept. Add minor refactoring and bugfixes

* ft/erlang_codegen Use swagger spec from an internal directory instead of a provided path

* ft/erlang_codegen Add basic response validation

* ft/erlang_codegen Moved all the req validators to a separate file for test needs

* ft/erlang_codegen Add basic param validation

* Add refactoring:
OperationIDs are atoms now
Fix schema validation
Add todo list

* CAPI-23 Add auth context to request handling (#2)

* CAPI-23 Fix routing to support different paths in one handler. Add auth context to request handling. Add an opportunity to pass custom middlewares to the server

* CAPI-31 Add enum validation and some minor fixes (#4)

* CAPI-31 Fix turbo fuck up with additional params (#5)

* Capi 23/fix/basic logging (#6)

* CAPI-23 Add understandable messages in case of bad requests. Add specs to shut up dialyzer and add some minor code refactoring

* CAPI-23 Fix missed bracket in auth module (#7)
  • Loading branch information
galaxie authored and wing328 committed Sep 9, 2016
1 parent c171ca5 commit f90626a
Show file tree
Hide file tree
Showing 14 changed files with 1,322 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,267 @@
package io.swagger.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 io.swagger.codegen.*;
import io.swagger.models.*;
import io.swagger.util.Json;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.File;
import java.io.IOException;
import java.math.BigDecimal;
import java.util.*;
import java.util.Map.Entry;
import org.apache.commons.lang3.StringUtils;

public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig {

private static final Logger LOGGER = LoggerFactory.getLogger(ErlangServerCodegen.class);

protected String apiVersion = "1.0.0";
protected String apiPath = "src";
protected String packageName = "swagger";

public ErlangServerCodegen() {
super();

// set the output folder here
outputFolder = "generated-code/erlang-server";

if (additionalProperties.containsKey(CodegenConstants.PACKAGE_NAME)) {
setPackageName((String) additionalProperties.get(CodegenConstants.PACKAGE_NAME));
} else {
additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
};

/**
* 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(
"handler.mustache", // the template to use
".erl"); // 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 = "erlang-server";

/**
* Reserved words. Override this with reserved words specific to your language
*/
setReservedWordsLowerCase(
Arrays.asList(
"after","and","andalso","band","begin","bnot","bor","bsl","bsr","bxor","case",
"catch","cond","div","end","fun","if","let","not","of","or","orelse","receive",
"rem","try","when","xor"
)
);

instantiationTypes.clear();

typeMapping.clear();
typeMapping.put("enum", "binary");
typeMapping.put("date", "date");
typeMapping.put("datetime", "datetime");
typeMapping.put("boolean", "boolean");
typeMapping.put("string", "binary");
typeMapping.put("integer", "integer");
typeMapping.put("int", "integer");
typeMapping.put("float", "integer");
typeMapping.put("long", "integer");
typeMapping.put("double", "float");
typeMapping.put("array", "list");
typeMapping.put("map", "map");
typeMapping.put("number", "integer");
typeMapping.put("bigdecimal", "float");
typeMapping.put("List", "list");
typeMapping.put("object", "object");
typeMapping.put("file", "file");
typeMapping.put("binary", "binary");
typeMapping.put("bytearray", "binary");
typeMapping.put("byte", "binary");
typeMapping.put("uuid", "binary");
typeMapping.put("password", "binary");

cliOptions.clear();
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Erlang package name (convention: lowercase).")
.defaultValue(this.packageName));
/**
* 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("apiPath", apiPath);
/**
* 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("rebar.config.mustache","", "rebar.config"));
supportingFiles.add(new SupportingFile("app.src.mustache", "", "src" + File.separator + this.packageName + ".app.src"));
supportingFiles.add(new SupportingFile("router.mustache", "", toSourceFilePath("router", "erl")));
supportingFiles.add(new SupportingFile("api.mustache", "", toSourceFilePath("api", "erl")));
supportingFiles.add(new SupportingFile("server.mustache", "", toSourceFilePath("server", "erl")));
supportingFiles.add(new SupportingFile("utils.mustache", "", toSourceFilePath("utils", "erl")));
supportingFiles.add(new SupportingFile("auth.mustache", "", toSourceFilePath("auth", "erl")));
supportingFiles.add(new SupportingFile("swagger.mustache", "", toPrivFilePath("swagger", "json")));
supportingFiles.add(new SupportingFile("default_logic_handler.mustache", "", toSourceFilePath("default_logic_handler", "erl")));
supportingFiles.add(new SupportingFile("logic_handler.mustache", "", toSourceFilePath("logic_handler", "erl")));
writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md"));
}

@Override
public String apiPackage() {
return apiPath;
}

/**
* 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 "erlang-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 an Erlang 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 this.packageName + "_default_handler";
}
return this.packageName + "_" + underscore(name) + "_handler";
}

/**
* 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 reseved words
*
* @return the escaped term
*/
@Override
public String escapeReservedWord(String name) {
return name + "_"; // add an underscore to the 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);
}

@Override
public String toModelName(String name) {
return camelize(toModelFilename(name));
}

@Override
public String toOperationId(String operationId) {
// method name cannot use reserved keyword, e.g. return
if (isReservedWord(operationId)) {
LOGGER.warn(operationId + " (reserved word) cannot be used as method name. Renamed to " + camelize(sanitizeName("call_" + operationId)));
operationId = "call_" + operationId;
}

return camelize(operationId);
}

@Override
public String toApiFilename(String name) {
return toHandlerName(name);
}

@Override
public Map<String, Object> postProcessOperations(Map<String, Object> objs) {
Map<String, Object> operations = (Map<String, Object>) objs.get("operations");
List<CodegenOperation> operationList = (List<CodegenOperation>) operations.get("operation");
for (CodegenOperation op : operationList) {
if (op.path != null) {
op.path = op.path.replaceAll("\\{(.*?)\\}", ":$1");
}
}
return objs;
}

@Override
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
Swagger swagger = (Swagger)objs.get("swagger");
if(swagger != null) {
try {
objs.put("swagger-json", Json.mapper().writeValueAsString(swagger));
} catch (JsonProcessingException e) {
LOGGER.error(e.getMessage(), e);
}
}
return super.postProcessSupportingFileData(objs);
}

public void setPackageName(String packageName) {
this.packageName = packageName;
}

protected String toHandlerName(String name) {
return toModuleName(name) + "_handler";
}

protected String toModuleName(String name) {
return this.packageName + "_" + underscore(name.replaceAll("-", "_"));
}

protected String toSourceFilePath(String name, String extension) {
return "src" + File.separator + toModuleName(name) + "." + extension;
}

protected String toIncludeFilePath(String name, String extension) {
return "include" + File.separator + toModuleName(name) + "." + extension;
}

protected String toPrivFilePath(String name, String extension) {
return "priv" + File.separator + name + "." + extension;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -51,3 +51,4 @@ io.swagger.codegen.languages.ClojureClientCodegen
io.swagger.codegen.languages.HaskellServantCodegen
io.swagger.codegen.languages.LumenServerCodegen
io.swagger.codegen.languages.GoServerCodegen
io.swagger.codegen.languages.ErlangServerCodegen
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Swagger rest server library for Erlang

## TODO
- [ ] Write specs for static exported fuctions
- [ ] Write specs for generated exported fuctions
- [ ] Add datetime/date validation
- [ ] Separate gigantic `api` to submodules and refactor the routing
- [ ] Add tests for the validators
- [ ] Add integrational test for the whole cycle
- [ ] Add validations of definitions with inheritance
- [ ] Add proper response validation (this `list` hack is so weird)
- [ ] Fix enum validation. It doesn't work correctly when the parans is in qs/header
Loading

0 comments on commit f90626a

Please sign in to comment.