forked from loafle/openapi-generator-original
[Erlang] Add Erlang server generator (#3758)
* 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)
This commit is contained in:
parent
c171ca5f57
commit
f90626ac81
@ -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;
|
||||
}
|
||||
}
|
@ -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
|
||||
|
@ -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
|
@ -0,0 +1,345 @@
|
||||
-module({{packageName}}_api).
|
||||
|
||||
-export([request_params/1]).
|
||||
-export([request_param_info/2]).
|
||||
-export([populate_request/3]).
|
||||
-export([validate_response/4]).
|
||||
|
||||
-type operation_id() :: atom().
|
||||
-type request_param() :: atom().
|
||||
|
||||
-export_type([operation_id/0]).
|
||||
|
||||
-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()].
|
||||
{{#apiInfo}}{{#apis}}
|
||||
{{#operations}}{{#operation}}
|
||||
request_params('{{operationId}}') ->
|
||||
[{{#allParams}}{{^isBodyParam}}
|
||||
'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}
|
||||
'{{dataType}}'{{/isBodyParam}}{{#hasMore}},{{/hasMore}}{{/allParams}}
|
||||
];
|
||||
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
request_params(_) ->
|
||||
error(unknown_operation).
|
||||
|
||||
-type rule() ::
|
||||
{type, 'binary'} |
|
||||
{type, 'integer'} |
|
||||
{type, 'float'} |
|
||||
{type, 'binary'} |
|
||||
{type, 'boolean'} |
|
||||
{type, 'date'} |
|
||||
{type, 'datetime'} |
|
||||
{enum, [atom()]} |
|
||||
{max, Max :: number()} |
|
||||
{exclusive_max, Max :: number()} |
|
||||
{min, Min :: number()} |
|
||||
{exclusive_min, Min :: number()} |
|
||||
{max_length, MaxLength :: integer()} |
|
||||
{min_length, MaxLength :: integer()} |
|
||||
{pattern, Pattern :: string()} |
|
||||
schema |
|
||||
required |
|
||||
not_required.
|
||||
|
||||
-spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> #{
|
||||
source => qs_val | binding | header | body,
|
||||
rules => [rule()]
|
||||
}.
|
||||
|
||||
{{#apiInfo}}{{#apis}}
|
||||
{{#operations}}{{#operation}}{{#allParams}}
|
||||
request_param_info('{{operationId}}', {{^isBodyParam}}'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}'{{dataType}}'{{/isBodyParam}}) ->
|
||||
#{
|
||||
source => {{#isQueryParam}}qs_val{{/isQueryParam}} {{#isPathParam}}binding{{/isPathParam}} {{#isHeaderParam}}header{{/isHeaderParam}}{{#isBodyParam}}body{{/isBodyParam}},
|
||||
rules => [{{#isString}}
|
||||
{type, 'binary'},{{/isString}}{{#isInteger}}
|
||||
{type, 'integer'},{{/isInteger}}{{#isLong}}
|
||||
{type, 'integer'},{{/isLong}}{{#isFloat}}
|
||||
{type, 'float'},{{/isFloat}}{{#isDouble}}
|
||||
{type, 'float'},{{/isDouble}}{{#isByteArray}}
|
||||
{type, 'binary'},{{/isByteArray}}{{#isBinary}}
|
||||
{type, 'binary'},{{/isBinary}}{{#isBoolean}}
|
||||
{type, 'boolean'},{{/isBoolean}}{{#isDate}}
|
||||
{type, 'date'},{{/isDate}}{{#isDateTime}}
|
||||
{type, 'datetime'},{{/isDateTime}}{{#isEnum}}
|
||||
{enum, [{{#allowableValues}}{{#values}}'{{.}}'{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}] },{{/isEnum}}{{#maximum}}
|
||||
{max, {{maximum}} }, {{/maximum}}{{#exclusiveMaximum}}
|
||||
{exclusive_max, {{exclusiveMaximum}} },{{/exclusiveMaximum}}{{#minimum}}
|
||||
{min, {{minimum}} },{{/minimum}}{{#exclusiveMinimum}}
|
||||
{exclusive_min, {{exclusiveMinimum}} },{{/exclusiveMinimum}}{{#maxLength}}
|
||||
{max_length, {{maxLength}} },{{/maxLength}}{{#minLength}}
|
||||
{min_length, {{minLength}} },{{/minLength}}{{#pattern}}
|
||||
{pattern, "{{pattern}}" },{{/pattern}}{{#isBodyParam}}
|
||||
schema,{{/isBodyParam}}{{#required}}
|
||||
required{{/required}}{{^required}}
|
||||
not_required{{/required}}
|
||||
]
|
||||
};
|
||||
{{/allParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
request_param_info(OperationID, Name) ->
|
||||
error({unknown_param, OperationID, Name}).
|
||||
|
||||
-spec populate_request(
|
||||
OperationID :: operation_id(),
|
||||
Req :: cowboy_req:req(),
|
||||
ValidatorState :: jesse_state:state()
|
||||
) ->
|
||||
{ok, Model :: #{}, Req :: cowboy_req:req()} |
|
||||
{error, Reason :: any(), Req :: cowboy_req:req()}.
|
||||
|
||||
populate_request(OperationID, Req, ValidatorState) ->
|
||||
Params = request_params(OperationID),
|
||||
populate_request_params(OperationID, Params, Req, ValidatorState, #{}).
|
||||
|
||||
populate_request_params(_, [], Req, _, Model) ->
|
||||
{ok, Model, Req};
|
||||
|
||||
populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) ->
|
||||
case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of
|
||||
{ok, K, V, Req} ->
|
||||
populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model));
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
populate_request_param(OperationID, Name, Req0, ValidatorState) ->
|
||||
#{rules := Rules, source := Source} = request_param_info(OperationID, Name),
|
||||
{Value, Req} = get_value(Source, Name, Req0),
|
||||
case prepare_param(Rules, Name, Value, ValidatorState) of
|
||||
{ok, Result} -> {ok, Name, Result, Req};
|
||||
{error, Reason} ->
|
||||
{error, Reason, Req}
|
||||
end.
|
||||
|
||||
-spec validate_response(
|
||||
OperationID :: operation_id(),
|
||||
Code :: 200..599,
|
||||
Body :: jesse:json_term(),
|
||||
ValidatorState :: jesse_state:state()
|
||||
) -> ok | no_return().
|
||||
{{#apiInfo}}{{#apis}}
|
||||
{{#operations}}{{#operation}}
|
||||
{{#responses}}
|
||||
validate_response('{{operationId}}', {{code}}, Body, ValidatorState) ->
|
||||
validate_response_body('{{dataType}}', '{{baseType}}', Body, ValidatorState);
|
||||
{{/responses}}
|
||||
{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
|
||||
validate_response(_OperationID, _Code, _Body, _ValidatorState) ->
|
||||
ok.
|
||||
|
||||
validate_response_body('list', ReturnBaseType, Body, ValidatorState) ->
|
||||
[
|
||||
validate(schema, ReturnBaseType, Item, ValidatorState)
|
||||
|| Item <- Body];
|
||||
|
||||
validate_response_body(_, ReturnBaseType, Body, ValidatorState) ->
|
||||
validate(schema, ReturnBaseType, Body, ValidatorState).
|
||||
|
||||
%%%
|
||||
validate(Rule = required, Name, Value, _ValidatorState) ->
|
||||
case Value of
|
||||
undefined -> validation_error(Rule, Name);
|
||||
_ -> ok
|
||||
end;
|
||||
|
||||
validate(not_required, _Name, _Value, _ValidatorState) ->
|
||||
ok;
|
||||
|
||||
validate(_, _Name, undefined, _ValidatorState) ->
|
||||
ok;
|
||||
|
||||
validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) ->
|
||||
try
|
||||
{ok, {{packageName}}_utils:to_int(Value)}
|
||||
catch
|
||||
error:badarg ->
|
||||
validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) ->
|
||||
try
|
||||
{ok, {{packageName}}_utils:to_float(Value)}
|
||||
catch
|
||||
error:badarg ->
|
||||
validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) ->
|
||||
case is_binary(Value) of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(_Rule = {type, 'boolean'}, _Name, Value, _ValidatorState) when is_boolean(Value) ->
|
||||
{ok, Value};
|
||||
|
||||
validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) ->
|
||||
V = binary_to_lower(Value),
|
||||
try
|
||||
case binary_to_existing_atom(V, utf8) of
|
||||
B when is_boolean(B) -> {ok, B};
|
||||
_ -> validation_error(Rule, Name)
|
||||
end
|
||||
catch
|
||||
error:badarg ->
|
||||
validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) ->
|
||||
case is_binary(Value) of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {type, 'datetime'}, Name, Value, _ValidatorState) ->
|
||||
case is_binary(Value) of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {enum, Values}, Name, Value, _ValidatorState) ->
|
||||
try
|
||||
FormattedValue = erlang:binary_to_existing_atom(Value, utf8),
|
||||
case lists:member(FormattedValue, Values) of
|
||||
true -> {ok, FormattedValue};
|
||||
false -> validation_error(Rule, Name)
|
||||
end
|
||||
catch
|
||||
error:badarg ->
|
||||
validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {max, Max}, Name, Value, _ValidatorState) ->
|
||||
case Value >= Max of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) ->
|
||||
case Value > ExclusiveMax of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {min, Min}, Name, Value, _ValidatorState) ->
|
||||
case Value =< Min of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) ->
|
||||
case Value =< ExclusiveMin of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) ->
|
||||
case size(Value) =< MaxLength of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) ->
|
||||
case size(Value) >= MinLength of
|
||||
true -> ok;
|
||||
false -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) ->
|
||||
{ok, MP} = re:compile(Pattern),
|
||||
case re:run(Value, MP) of
|
||||
{match, _} -> ok;
|
||||
_ -> validation_error(Rule, Name)
|
||||
end;
|
||||
|
||||
validate(Rule = schema, Name, Value, ValidatorState) ->
|
||||
Definition = list_to_binary("#/definitions/" ++ {{packageName}}_utils:to_list(Name)),
|
||||
try
|
||||
_ = validate_with_schema(Value, Definition, ValidatorState),
|
||||
ok
|
||||
catch
|
||||
throw:[{schema_invalid, _, Error} | _] ->
|
||||
Info = #{
|
||||
type => schema_invalid,
|
||||
error => Error
|
||||
},
|
||||
validation_error(Rule, Name, Info);
|
||||
throw:[{data_invalid, Schema, Error, _, Path} | _] ->
|
||||
Info = #{
|
||||
type => data_invalid,
|
||||
error => Error,
|
||||
schema => Schema,
|
||||
path => Path
|
||||
},
|
||||
validation_error(Rule, Name, Info)
|
||||
end;
|
||||
|
||||
validate(Rule, Name, _Value, _ValidatorState) ->
|
||||
error_logger:info_msg("Can't validate ~p with ~p", [Name, Rule]),
|
||||
error({unknown_validation_rule, Rule}).
|
||||
|
||||
-spec validation_error(Rule :: any(), Name :: any()) -> no_return().
|
||||
|
||||
validation_error(ViolatedRule, Name) ->
|
||||
validation_error(ViolatedRule, Name, #{}).
|
||||
|
||||
-spec validation_error(Rule :: any(), Name :: any(), Info :: #{}) -> no_return().
|
||||
|
||||
validation_error(ViolatedRule, Name, Info) ->
|
||||
throw({wrong_param, Name, ViolatedRule, Info}).
|
||||
|
||||
get_value(body, _Name, Req0) ->
|
||||
{ok, Body, Req} = cowboy_req:body(Req0),
|
||||
Value = prepare_body(Body),
|
||||
{Value, Req};
|
||||
|
||||
get_value(qs_val, Name, Req0) ->
|
||||
{QS, Req} = cowboy_req:qs_vals(Req0),
|
||||
Value = {{packageName}}_utils:get_opt({{packageName}}_utils:to_qs(Name), QS),
|
||||
{Value, Req};
|
||||
|
||||
get_value(header, Name, Req0) ->
|
||||
{Headers, Req} = cowboy_req:headers(Req0),
|
||||
Value = {{packageName}}_utils:get_opt({{packageName}}_utils:to_header(Name), Headers),
|
||||
{Value, Req};
|
||||
|
||||
get_value(binding, Name, Req0) ->
|
||||
{Bindings, Req} = cowboy_req:bindings(Req0),
|
||||
Value = {{packageName}}_utils:get_opt({{packageName}}_utils:to_binding(Name), Bindings),
|
||||
{Value, Req}.
|
||||
|
||||
prepare_body(Body) ->
|
||||
case Body of
|
||||
<<"">> -> <<"">>;
|
||||
_ -> jsx:decode(Body, [return_maps])
|
||||
end.
|
||||
|
||||
validate_with_schema(Body, Definition, ValidatorState) ->
|
||||
jesse_schema_validator:validate_with_state(
|
||||
[{<<"$ref">>, Definition}],
|
||||
Body,
|
||||
ValidatorState
|
||||
).
|
||||
|
||||
prepare_param(Rules, Name, Value, ValidatorState) ->
|
||||
try
|
||||
Result = lists:foldl(
|
||||
fun(Rule, Acc) ->
|
||||
case validate(Rule, Name, Acc, ValidatorState) of
|
||||
ok -> Acc;
|
||||
{ok, Prepared} -> Prepared
|
||||
end
|
||||
end,
|
||||
Value,
|
||||
Rules
|
||||
),
|
||||
{ok, Result}
|
||||
catch
|
||||
throw:Reason ->
|
||||
{error, Reason}
|
||||
end.
|
||||
|
||||
binary_to_lower(V) when is_binary(V) ->
|
||||
list_to_binary(string:to_lower({{packageName}}_utils:to_list(V))).
|
@ -0,0 +1,19 @@
|
||||
{application, {{packageName}}, [
|
||||
{description, {{#appDescription}}"{{appDescription}}"{{/appDescription}}{{^appDescription}}"Swagger rest server library"{{/appDescription}}},
|
||||
{vsn, "{{apiVersion}}"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
ssl,
|
||||
inets,
|
||||
jsx,
|
||||
jesse,
|
||||
cowboy
|
||||
]},
|
||||
{env, [
|
||||
]},
|
||||
{modules, []},
|
||||
{licenses, [{{#licenseInfo}}"{{licenseInfo}}"{{/licenseInfo}}]},
|
||||
{links, [{{#infoUrl}}"{{infoUrl}}"{{/infoUrl}}]}
|
||||
]}.
|
@ -0,0 +1,50 @@
|
||||
-module({{packageName}}_auth).
|
||||
|
||||
-export([authorize_api_key/5]).
|
||||
|
||||
-spec authorize_api_key(
|
||||
LogicHandler :: atom(),
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
From :: header | qs_val,
|
||||
KeyParam :: iodata() | atom(),
|
||||
Req ::cowboy_req:req()
|
||||
)-> {true, Context :: #{binary() => any()}, Req ::cowboy_req:req()} |
|
||||
{false, AuthHeader :: binary(), Req ::cowboy_req:req()}.
|
||||
|
||||
authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) ->
|
||||
{ApiKey, Req} = get_api_key(From, KeyParam, Req0),
|
||||
case ApiKey of
|
||||
undefined ->
|
||||
AuthHeader = <<"">>,
|
||||
{false, AuthHeader, Req};
|
||||
_ ->
|
||||
Result = {{packageName}}_logic_handler:authorize_api_key(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
ApiKey
|
||||
),
|
||||
case Result of
|
||||
{true, Context} ->
|
||||
{true, Context, Req};
|
||||
false ->
|
||||
AuthHeader = <<"">>,
|
||||
{false, AuthHeader, Req}
|
||||
end
|
||||
end.
|
||||
|
||||
get_api_key(header, KeyParam, Req0) ->
|
||||
{Headers, Req} = cowboy_req:headers(Req0),
|
||||
{
|
||||
swagger_utils:get_opt(
|
||||
{{packageName}}_utils:to_header(KeyParam),
|
||||
Headers
|
||||
),
|
||||
Req
|
||||
};
|
||||
|
||||
get_api_key(qs_val, KeyParam, Req0) ->
|
||||
{QS, Req} = cowboy_req:qs_vals(Req0),
|
||||
{ {{packageName}}_utils:get_opt(KeyParam, QS), Req}.
|
||||
|
||||
|
||||
|
@ -0,0 +1,32 @@
|
||||
-module({{packageName}}_default_logic_handler).
|
||||
|
||||
-behaviour({{packageName}}_logic_handler).
|
||||
|
||||
-export([handle_request/3]).
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-export([authorize_api_key/2]).
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-spec authorize_api_key(OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) -> {true, #{}}.
|
||||
|
||||
authorize_api_key(_, _) -> {true, #{}}.
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
|
||||
-spec handle_request(
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
Req :: cowboy_req:req(),
|
||||
Context :: #{}
|
||||
) ->
|
||||
{Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: #{}}.
|
||||
|
||||
handle_request(OperationID, Req, Context) ->
|
||||
error_logger:error_msg(
|
||||
"Got not implemented request to process: ~p~n",
|
||||
[{OperationID, Req, Context}]
|
||||
),
|
||||
{501, [], #{}}.
|
@ -0,0 +1,230 @@
|
||||
%% basic handler
|
||||
-module({{classname}}).
|
||||
|
||||
%% Cowboy REST callbacks
|
||||
-export([allowed_methods/2]).
|
||||
-export([init/3]).
|
||||
-export([rest_init/2]).
|
||||
-export([allow_missing_post/2]).
|
||||
-export([content_types_accepted/2]).
|
||||
-export([content_types_provided/2]).
|
||||
-export([delete_resource/2]).
|
||||
-export([is_authorized/2]).
|
||||
-export([known_content_type/2]).
|
||||
-export([malformed_request/2]).
|
||||
-export([valid_content_headers/2]).
|
||||
-export([valid_entity_length/2]).
|
||||
|
||||
%% Handlers
|
||||
-export([handle_request_json/2]).
|
||||
|
||||
-record(state, {
|
||||
operation_id :: {{packageName}}_api:operation_id(),
|
||||
logic_handler :: atom(),
|
||||
validator_state :: jesse_state:state(),
|
||||
context=#{} :: #{}
|
||||
}).
|
||||
|
||||
-type state() :: state().
|
||||
|
||||
-spec init(TransportName :: atom(), Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) ->
|
||||
{upgrade, protocol, cowboy_rest, Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()}.
|
||||
|
||||
init(_Transport, Req, Opts) ->
|
||||
{upgrade, protocol, cowboy_rest, Req, Opts}.
|
||||
|
||||
-spec rest_init(Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) ->
|
||||
{ok, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
rest_init(Req0, {Operations, LogicHandler, ValidatorState}) ->
|
||||
{Method, Req} = cowboy_req:method(Req0),
|
||||
OperationID = maps:get(Method, Operations, undefined),
|
||||
|
||||
error_logger:info_msg("Attempt to process operation: ~p", [OperationID]),
|
||||
|
||||
State = #state{
|
||||
operation_id = OperationID,
|
||||
logic_handler = LogicHandler,
|
||||
validator_state = ValidatorState
|
||||
},
|
||||
{ok, Req, State}.
|
||||
|
||||
-spec allowed_methods(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: [binary()], Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
{{#operations}}{{#operation}}
|
||||
allowed_methods(
|
||||
Req,
|
||||
State = #state{
|
||||
operation_id = '{{operationId}}'
|
||||
}
|
||||
) ->
|
||||
{[<<"{{httpMethod}}">>], Req, State};
|
||||
{{/operation}}{{/operations}}
|
||||
allowed_methods(Req, State) ->
|
||||
{[], Req, State}.
|
||||
|
||||
-spec is_authorized(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: true | {false, AuthHeader :: iodata()},
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
{{#operations}}{{#operation}}
|
||||
is_authorized(
|
||||
Req0,
|
||||
State = #state{
|
||||
operation_id = '{{operationId}}' = OperationID,
|
||||
logic_handler = LogicHandler
|
||||
}
|
||||
) ->
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
From = {{#isKeyInQuery}}qs_val{{/isKeyInQuery}}{{#isKeyInHeader}}header{{/isKeyInHeader}},
|
||||
Result = {{packageName}}_auth:authorize_api_key(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
From,
|
||||
"{{keyParamName}}",
|
||||
Req0
|
||||
),
|
||||
case Result of
|
||||
{true, Context, Req} -> {true, Req, State#state{context = Context}};
|
||||
{false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State}
|
||||
end;
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
{{/operation}}{{/operations}}
|
||||
is_authorized(Req, State) ->
|
||||
{{false, <<"">>}, Req, State}.
|
||||
|
||||
-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: [{binary(), AcceptResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
|
||||
content_types_accepted(Req, State) ->
|
||||
{[
|
||||
{<<"application/json">>, handle_request_json}
|
||||
], Req, State}.
|
||||
|
||||
-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: boolean(), Req :: cowboy_req:req(), State :: state()}.
|
||||
{{#operations}}{{#operation}}
|
||||
valid_content_headers(
|
||||
Req0,
|
||||
State = #state{
|
||||
operation_id = '{{operationId}}'
|
||||
}
|
||||
) ->
|
||||
Headers = [{{#headerParams}}"{{baseName}}"{{#hasMore}},{{/hasMore}}{{/headerParams}}],
|
||||
{Result, Req} = validate_headers(Headers, Req0),
|
||||
{Result, Req, State};
|
||||
{{/operation}}{{/operations}}
|
||||
valid_content_headers(Req, State) ->
|
||||
{false, Req, State}.
|
||||
|
||||
-spec content_types_provided(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: [{binary(), ProvideResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
|
||||
content_types_provided(Req, State) ->
|
||||
{[
|
||||
{<<"application/json">>, handle_request_json}
|
||||
], Req, State}.
|
||||
|
||||
-spec malformed_request(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: false, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
malformed_request(Req, State) ->
|
||||
{false, Req, State}.
|
||||
|
||||
-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: false, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
allow_missing_post(Req, State) ->
|
||||
{false, Req, State}.
|
||||
|
||||
-spec delete_resource(Req :: cowboy_req:req(), State :: state()) ->
|
||||
processed_response().
|
||||
|
||||
delete_resource(Req, State) ->
|
||||
handle_request_json(Req, State).
|
||||
|
||||
-spec known_content_type(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: true, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
known_content_type(Req, State) ->
|
||||
{true, Req, State}.
|
||||
|
||||
-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: true, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
valid_entity_length(Req, State) ->
|
||||
%% @TODO check the length
|
||||
{true, Req, State}.
|
||||
|
||||
%%%%
|
||||
|
||||
-type result_ok() :: {
|
||||
ok,
|
||||
{Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()}
|
||||
}.
|
||||
|
||||
-type result_error() :: {error, Reason :: any()}.
|
||||
|
||||
-type processed_response() :: {halt, cowboy_req:req(), state()}.
|
||||
|
||||
-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) ->
|
||||
processed_response().
|
||||
|
||||
process_response(Response, Req0, State = #state{operation_id = OperationID}) ->
|
||||
case Response of
|
||||
{ok, {Code, Headers, Body}} ->
|
||||
{ok, Req} = cowboy_req:reply(Code, Headers, Body, Req0),
|
||||
{halt, Req, State};
|
||||
{error, Message} ->
|
||||
error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]),
|
||||
|
||||
{ok, Req} = cowboy_req:reply(400, Req0),
|
||||
{halt, Req, State}
|
||||
end.
|
||||
|
||||
-spec handle_request_json(cowboy_req:req(), state()) -> {halt, cowboy_req:req(), state()}.
|
||||
|
||||
handle_request_json(
|
||||
Req0,
|
||||
State = #state{
|
||||
operation_id = OperationID,
|
||||
logic_handler = LogicHandler,
|
||||
validator_state = ValidatorState,
|
||||
context = Context
|
||||
}
|
||||
) ->
|
||||
case {{packageName}}_api:populate_request(OperationID, Req0, ValidatorState) of
|
||||
{ok, Populated, Req1} ->
|
||||
{Code, Headers, Body} = {{packageName}}_logic_handler:handle_request(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
Populated,
|
||||
Context
|
||||
),
|
||||
_ = {{packageName}}_api:validate_response(
|
||||
OperationID,
|
||||
Code,
|
||||
Body,
|
||||
ValidatorState
|
||||
),
|
||||
PreparedBody = jsx:encode(Body),
|
||||
Response = {ok, {Code, Headers, PreparedBody}},
|
||||
process_response(Response, Req1, State);
|
||||
{error, Reason, Req1} ->
|
||||
process_response({error, Reason}, Req1, State)
|
||||
end.
|
||||
|
||||
validate_headers(_, Req) -> {true, Req}.
|
@ -0,0 +1,50 @@
|
||||
-module({{packageName}}_logic_handler).
|
||||
|
||||
-export([handle_request/4]).
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-export([authorize_api_key/3]).
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
-type context() :: #{binary() => any()}.
|
||||
-type handler_response() ::{
|
||||
Status :: cowboy:http_status(),
|
||||
Headers :: cowboy:http_headers(),
|
||||
Body :: #{}
|
||||
}.
|
||||
|
||||
-export_type([handler_response/0]).
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-callback authorize_api_key(
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
ApiKey :: binary()
|
||||
) ->
|
||||
Result :: boolean() | {boolean(), context()}.
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
|
||||
|
||||
-callback handle_request(OperationID :: {{packageName}}_api:operation_id(), Request :: any(), Context :: context()) ->
|
||||
handler_response().
|
||||
|
||||
-spec handle_request(
|
||||
Handler :: atom(),
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
Request :: any(),
|
||||
Context :: context()
|
||||
) ->
|
||||
handler_response().
|
||||
|
||||
handle_request(Handler, OperationID, Req, Context) ->
|
||||
Handler:handle_request(OperationID, Req, Context).
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-spec authorize_api_key(Handler :: atom(), OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) ->
|
||||
Result :: false | {true, context()}.
|
||||
authorize_api_key(Handler, OperationID, ApiKey) ->
|
||||
Handler:authorize_api_key(OperationID, ApiKey).
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
@ -0,0 +1,4 @@
|
||||
{deps, [
|
||||
{jsx, {git, "https://github.com/talentdeficit/jsx.git", {branch, "v2.8.0"}}},
|
||||
{jesse, {git, "https://github.com/for-GET/jesse.git", {tag, "1.4.0"}}}
|
||||
]}.
|
@ -0,0 +1,74 @@
|
||||
-module({{packageName}}_router).
|
||||
|
||||
-export([get_paths/1]).
|
||||
|
||||
-type operations() :: #{
|
||||
Method :: binary() => {{packageName}}_api:operation_id()
|
||||
}.
|
||||
|
||||
-type init_opts() :: {
|
||||
Operations :: operations(),
|
||||
LogicHandler :: atom(),
|
||||
ValidatorState :: jesse_state:state()
|
||||
}.
|
||||
|
||||
-export_type([init_opts/0]).
|
||||
|
||||
-spec get_paths(LogicHandler :: atom()) -> [{'_',[{
|
||||
Path :: string(),
|
||||
Handler :: atom(),
|
||||
InitOpts :: init_opts()
|
||||
}]}].
|
||||
|
||||
get_paths(LogicHandler) ->
|
||||
ValidatorState = prepare_validator(),
|
||||
PreparedPaths = maps:fold(
|
||||
fun(Path, #{operations := Operations, handler := Handler}, Acc) ->
|
||||
[{Path, Handler, Operations} | Acc]
|
||||
end,
|
||||
[],
|
||||
group_paths()
|
||||
),
|
||||
[
|
||||
{'_',
|
||||
[{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths]
|
||||
}
|
||||
].
|
||||
|
||||
group_paths() ->
|
||||
maps:fold(
|
||||
fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) ->
|
||||
case maps:find(Path, Acc) of
|
||||
{ok, PathInfo0 = #{operations := Operations0}} ->
|
||||
Operations = Operations0#{Method => OperationID},
|
||||
PathInfo = PathInfo0#{operations => Operations},
|
||||
Acc#{Path => PathInfo};
|
||||
error ->
|
||||
Operations = #{Method => OperationID},
|
||||
PathInfo = #{handler => Handler, operations => Operations},
|
||||
Acc#{Path => PathInfo}
|
||||
end
|
||||
end,
|
||||
#{},
|
||||
get_operations()
|
||||
).
|
||||
|
||||
get_operations() ->
|
||||
#{ {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}
|
||||
'{{operationId}}' => #{
|
||||
path => "{{basePathWithoutHost}}{{path}}",
|
||||
method => <<"{{httpMethod}}">>,
|
||||
handler => '{{classname}}'
|
||||
}{{#hasMore}},{{/hasMore}}{{/operation}}{{#hasMore}},{{/hasMore}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
}.
|
||||
|
||||
prepare_validator() ->
|
||||
R = jsx:decode(element(2, file:read_file(get_swagger_path()))),
|
||||
jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]).
|
||||
|
||||
|
||||
get_swagger_path() ->
|
||||
{ok, AppName} = application:get_application(?MODULE),
|
||||
filename:join({{packageName}}_utils:priv_dir(AppName), "swagger.json").
|
||||
|
||||
|
@ -0,0 +1,64 @@
|
||||
-module({{packageName}}_server).
|
||||
|
||||
|
||||
-define(DEFAULT_ACCEPTORS_POOLSIZE, 100).
|
||||
-define(DEFAULT_LOGIC_HANDLER, {{packageName}}_default_logic_handler).
|
||||
|
||||
-export([child_spec/2]).
|
||||
|
||||
-spec child_spec( ID :: any(), #{
|
||||
ip => inet:ip_address(),
|
||||
port => inet:port_number(),
|
||||
net_opts => []
|
||||
}) -> supervisor:child_spec().
|
||||
|
||||
child_spec(ID, #{
|
||||
ip := IP ,
|
||||
port := Port,
|
||||
net_opts := NetOpts
|
||||
} = Params) ->
|
||||
AcceptorsPool = ?DEFAULT_ACCEPTORS_POOLSIZE,
|
||||
{Transport, TransportOpts} = get_socket_transport(IP, Port, NetOpts),
|
||||
LogicHandler = maps:get(logic_handler, Params, ?DEFAULT_LOGIC_HANDLER),
|
||||
ExtraOpts = maps:get(cowboy_extra_opts, Params, []),
|
||||
CowboyOpts = get_cowboy_config(LogicHandler, ExtraOpts),
|
||||
ranch:child_spec({?MODULE, ID}, AcceptorsPool,
|
||||
Transport, TransportOpts, cowboy_protocol, CowboyOpts).
|
||||
|
||||
get_socket_transport(IP, Port, Options) ->
|
||||
Opts = [
|
||||
{ip, IP},
|
||||
{port, Port}
|
||||
],
|
||||
case {{packageName}}_utils:get_opt(ssl, Options) of
|
||||
SslOpts = [_|_] ->
|
||||
{ranch_ssl, Opts ++ SslOpts};
|
||||
undefined ->
|
||||
{ranch_tcp, Opts}
|
||||
end.
|
||||
|
||||
get_cowboy_config(LogicHandler, ExtraOpts) ->
|
||||
get_cowboy_config(LogicHandler, ExtraOpts, get_default_opts(LogicHandler)).
|
||||
|
||||
get_cowboy_config(_LogicHandler, [], Opts) ->
|
||||
Opts;
|
||||
|
||||
get_cowboy_config(LogicHandler, [{env, Env} | Rest], Opts) ->
|
||||
NewEnv = case proplists:get_value(dispatch, Env) of
|
||||
undefined -> [get_default_dispatch(LogicHandler) | Env];
|
||||
_ -> Env
|
||||
end,
|
||||
get_cowboy_config(LogicHandler, Rest, store_key(env, NewEnv, Opts));
|
||||
|
||||
get_cowboy_config(LogicHandler, [{Key, Value}| Rest], Opts) ->
|
||||
get_cowboy_config(LogicHandler, Rest, store_key(Key, Value, Opts)).
|
||||
|
||||
get_default_dispatch(LogicHandler) ->
|
||||
Paths = {{packageName}}_router:get_paths(LogicHandler),
|
||||
{dispatch, cowboy_router:compile(Paths)}.
|
||||
|
||||
get_default_opts(LogicHandler) ->
|
||||
[{env, [get_default_dispatch(LogicHandler)]}].
|
||||
|
||||
store_key(Key, Value, Opts) ->
|
||||
lists:keystore(Key, 1, Opts, {Key, Value}).
|
@ -0,0 +1 @@
|
||||
{{{swagger-json}}}
|
@ -0,0 +1,173 @@
|
||||
-module({{packageName}}_utils).
|
||||
|
||||
-export([to_binary/1]).
|
||||
-export([to_list/1]).
|
||||
-export([to_float/1]).
|
||||
-export([to_int/1]).
|
||||
-export([to_lower/1]).
|
||||
-export([to_upper/1]).
|
||||
-export([set_resp_headers/2]).
|
||||
-export([to_header/1]).
|
||||
-export([to_qs/1]).
|
||||
-export([to_binding/1]).
|
||||
-export([get_opt/2]).
|
||||
-export([get_opt/3]).
|
||||
-export([priv_dir/0]).
|
||||
-export([priv_dir/1]).
|
||||
-export([priv_path/1]).
|
||||
|
||||
|
||||
-spec to_binary(iodata() | atom() | number()) -> binary().
|
||||
|
||||
to_binary(V) when is_binary(V) -> V;
|
||||
to_binary(V) when is_list(V) -> iolist_to_binary(V);
|
||||
to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
|
||||
to_binary(V) when is_integer(V) -> integer_to_binary(V);
|
||||
to_binary(V) when is_float(V) -> float_to_binary(V).
|
||||
|
||||
-spec to_list(iodata() | atom() | number()) -> string().
|
||||
|
||||
to_list(V) when is_list(V) -> V;
|
||||
to_list(V) -> binary_to_list(to_binary(V)).
|
||||
|
||||
-spec to_float(iodata()) -> number().
|
||||
|
||||
to_float(V) ->
|
||||
Data = iolist_to_binary([V]),
|
||||
case binary:split(Data, <<$.>>) of
|
||||
[Data] ->
|
||||
binary_to_integer(Data);
|
||||
[<<>>, _] ->
|
||||
binary_to_float(<<$0, Data/binary>>);
|
||||
_ ->
|
||||
binary_to_float(Data)
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
-spec to_int(integer() | binary() | list()) -> integer().
|
||||
|
||||
to_int(Data) when is_integer(Data) ->
|
||||
Data;
|
||||
to_int(Data) when is_binary(Data) ->
|
||||
binary_to_integer(Data);
|
||||
to_int(Data) when is_list(Data) ->
|
||||
list_to_integer(Data).
|
||||
|
||||
-spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req().
|
||||
|
||||
set_resp_headers([], Req) ->
|
||||
Req;
|
||||
set_resp_headers([{K, V} | T], Req0) ->
|
||||
Req = cowboy_req:set_resp_header(K, V, Req0),
|
||||
set_resp_headers(T, Req).
|
||||
|
||||
-spec to_header(iodata() | atom() | number()) -> binary().
|
||||
|
||||
to_header(Name) ->
|
||||
Prepared = to_binary(Name),
|
||||
to_lower(Prepared).
|
||||
|
||||
-spec to_qs(iodata() | atom() | number()) -> binary().
|
||||
|
||||
to_qs(Name) ->
|
||||
to_binary(Name).
|
||||
|
||||
-spec to_binding(iodata() | atom() | number()) -> atom().
|
||||
|
||||
to_binding(Name) ->
|
||||
Prepared = to_binary(Name),
|
||||
binary_to_atom(Prepared, utf8).
|
||||
|
||||
-spec get_opt(any(), []) -> any().
|
||||
|
||||
get_opt(Key, Opts) ->
|
||||
get_opt(Key, Opts, undefined).
|
||||
|
||||
-spec get_opt(any(), [], any()) -> any().
|
||||
|
||||
get_opt(Key, Opts, Default) ->
|
||||
case lists:keyfind(Key, 1, Opts) of
|
||||
{_, Value} -> Value;
|
||||
false -> Default
|
||||
end.
|
||||
|
||||
-spec priv_dir() -> file:filename().
|
||||
|
||||
priv_dir() ->
|
||||
{ok, AppName} = application:get_application(),
|
||||
priv_dir(AppName).
|
||||
|
||||
-spec priv_dir(Application :: atom()) -> file:filename().
|
||||
|
||||
priv_dir(AppName) ->
|
||||
case code:priv_dir(AppName) of
|
||||
Value when is_list(Value) ->
|
||||
Value ++ "/";
|
||||
_Error ->
|
||||
select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"])
|
||||
end.
|
||||
|
||||
-spec priv_path(Relative :: file:filename()) -> file:filename().
|
||||
|
||||
priv_path(Relative) ->
|
||||
filename:join(priv_dir(), Relative).
|
||||
|
||||
-include_lib("kernel/include/file.hrl").
|
||||
|
||||
select_priv_dir(Paths) ->
|
||||
case lists:dropwhile(fun test_priv_dir/1, Paths) of
|
||||
[Path | _] -> Path;
|
||||
_ -> exit(no_priv_dir)
|
||||
end.
|
||||
|
||||
test_priv_dir(Path) ->
|
||||
case file:read_file_info(Path) of
|
||||
{ok, #file_info{type = directory}} ->
|
||||
false;
|
||||
_ ->
|
||||
true
|
||||
end.
|
||||
|
||||
|
||||
%%
|
||||
|
||||
-spec to_lower(binary()) -> binary().
|
||||
|
||||
to_lower(S) ->
|
||||
to_case(lower, S, <<>>).
|
||||
|
||||
-spec to_upper(binary()) -> binary().
|
||||
|
||||
to_upper(S) ->
|
||||
to_case(upper, S, <<>>).
|
||||
|
||||
to_case(_Case, <<>>, Acc) ->
|
||||
Acc;
|
||||
|
||||
to_case(_Case, <<C, _/binary>>, _Acc) when C > 127 ->
|
||||
error(badarg);
|
||||
|
||||
to_case(Case = lower, <<C, Rest/binary>>, Acc) ->
|
||||
to_case(Case, Rest, <<Acc/binary, (to_lower_char(C))>>);
|
||||
|
||||
to_case(Case = upper, <<C, Rest/binary>>, Acc) ->
|
||||
to_case(Case, Rest, <<Acc/binary, (to_upper_char(C))>>).
|
||||
|
||||
to_lower_char(C) when is_integer(C), $A =< C, C =< $Z ->
|
||||
C + 32;
|
||||
to_lower_char(C) when is_integer(C), 16#C0 =< C, C =< 16#D6 ->
|
||||
C + 32;
|
||||
to_lower_char(C) when is_integer(C), 16#D8 =< C, C =< 16#DE ->
|
||||
C + 32;
|
||||
to_lower_char(C) ->
|
||||
C.
|
||||
|
||||
to_upper_char(C) when is_integer(C), $a =< C, C =< $z ->
|
||||
C - 32;
|
||||
to_upper_char(C) when is_integer(C), 16#E0 =< C, C =< 16#F6 ->
|
||||
C - 32;
|
||||
to_upper_char(C) when is_integer(C), 16#F8 =< C, C =< 16#FE ->
|
||||
C - 32;
|
||||
to_upper_char(C) ->
|
||||
C.
|
Loading…
x
Reference in New Issue
Block a user