mirror of
https://github.com/valitydev/openapi-generator.git
synced 2024-11-06 02:25:20 +00:00
MSPF-555: Initial migration to OAS3 generator (#1)
This commit is contained in:
parent
00ffcea6ef
commit
5b9e7db104
@ -2003,6 +2003,16 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
return oasType;
|
||||
}
|
||||
|
||||
/**
|
||||
* Output the type declaration of a given array schema
|
||||
*
|
||||
* @param name name
|
||||
* @return a string presentation of the type
|
||||
*/
|
||||
public String getArraySchemaTypeDeclaration(ArraySchema arraySchema, String name) {
|
||||
return getTypeDeclaration(arraySchema);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine the type alias for the given type if it exists. This feature
|
||||
* was originally developed for Java because the language does not have an aliasing
|
||||
@ -5403,6 +5413,7 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
if (StringUtils.isNotBlank(schema.get$ref())) {
|
||||
name = ModelUtils.getSimpleRef(schema.get$ref());
|
||||
}
|
||||
|
||||
schema = ModelUtils.getReferencedSchema(this.openAPI, schema);
|
||||
|
||||
ModelUtils.syncValidationProperties(schema, codegenParameter);
|
||||
@ -5480,7 +5491,7 @@ public class DefaultCodegen implements CodegenConfig {
|
||||
codegenParameter.paramName = toArrayModelParamName(codegenParameter.baseName);
|
||||
codegenParameter.items = codegenProperty.items;
|
||||
codegenParameter.mostInnerItems = codegenProperty.mostInnerItems;
|
||||
codegenParameter.dataType = getTypeDeclaration(arraySchema);
|
||||
codegenParameter.dataType = getArraySchemaTypeDeclaration(arraySchema, name);
|
||||
codegenParameter.baseType = getSchemaType(inner);
|
||||
codegenParameter.isContainer = Boolean.TRUE;
|
||||
codegenParameter.isListContainer = Boolean.TRUE;
|
||||
|
@ -17,12 +17,18 @@
|
||||
|
||||
package org.openapitools.codegen.languages;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.samskivert.mustache.Mustache;
|
||||
import com.samskivert.mustache.Template;
|
||||
import io.swagger.v3.oas.models.media.Schema;
|
||||
import io.swagger.v3.core.util.Json;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.media.ArraySchema;
|
||||
import org.openapitools.codegen.*;
|
||||
import org.openapitools.codegen.meta.features.*;
|
||||
import org.openapitools.codegen.templating.mustache.JoinWithCommaLambda;
|
||||
import org.openapitools.codegen.utils.ModelUtils;
|
||||
import org.openapitools.codegen.serializer.SerializerUtils;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
|
||||
@ -42,6 +48,7 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
protected String packageName = "openapi";
|
||||
protected String packageVersion = "1.0.0";
|
||||
protected String sourceFolder = "src";
|
||||
protected String openApiSpecName = "openapi";
|
||||
|
||||
public CodegenType getTag() {
|
||||
return CodegenType.CLIENT;
|
||||
@ -80,7 +87,7 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
);
|
||||
|
||||
outputFolder = "generated-code/erlang";
|
||||
modelTemplateFiles.put("model.mustache", ".erl");
|
||||
modelTemplateFiles.clear();
|
||||
apiTemplateFiles.put("api.mustache", ".erl");
|
||||
|
||||
embeddedTemplateDir = templateDir = "erlang-client";
|
||||
@ -96,63 +103,41 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
instantiationTypes.clear();
|
||||
|
||||
typeMapping.clear();
|
||||
typeMapping.put("enum", "binary()");
|
||||
typeMapping.put("date", "calendar:date()");
|
||||
typeMapping.put("datetime", "calendar:datetime()");
|
||||
typeMapping.put("date-time", "calendar: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", "maps:map()");
|
||||
typeMapping.put("number", "integer()");
|
||||
typeMapping.put("bigdecimal", "float()");
|
||||
typeMapping.put("List", "list()");
|
||||
typeMapping.put("object", "maps:map()");
|
||||
typeMapping.put("file", "binary()");
|
||||
typeMapping.put("binary", "binary()");
|
||||
typeMapping.put("bytearray", "binary()");
|
||||
typeMapping.put("byte", "binary()");
|
||||
typeMapping.put("uuid", "binary()");
|
||||
typeMapping.put("uri", "binary()");
|
||||
typeMapping.put("password", "binary()");
|
||||
typeMapping.put("enum", "binary");
|
||||
typeMapping.put("date", "date");
|
||||
typeMapping.put("datetime", "datetime");
|
||||
typeMapping.put("date-time", "datetime");
|
||||
typeMapping.put("boolean", "boolean");
|
||||
typeMapping.put("string", "binary");
|
||||
typeMapping.put("integer", "int32");
|
||||
typeMapping.put("int", "int32");
|
||||
typeMapping.put("float", "integer");
|
||||
typeMapping.put("long", "int64");
|
||||
typeMapping.put("double", "float");
|
||||
typeMapping.put("array", "list");
|
||||
typeMapping.put("map", "map");
|
||||
typeMapping.put("number", "float");
|
||||
typeMapping.put("bigdecimal", "float");
|
||||
typeMapping.put("List", "list");
|
||||
typeMapping.put("object", "map");
|
||||
typeMapping.put("file", "binary");
|
||||
typeMapping.put("binary", "binary");
|
||||
typeMapping.put("bytearray", "binary");
|
||||
typeMapping.put("byte", "binary");
|
||||
typeMapping.put("uuid", "binary");
|
||||
typeMapping.put("uri", "binary");
|
||||
typeMapping.put("password", "binary");
|
||||
|
||||
cliOptions.clear();
|
||||
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Erlang application name (convention: lowercase).")
|
||||
.defaultValue(this.packageName));
|
||||
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_VERSION, "Erlang application version")
|
||||
.defaultValue(this.packageVersion));
|
||||
}
|
||||
cliOptions.add(new CliOption(CodegenConstants.OPEN_API_SPEC_NAME, "Openapi Spec Name.")
|
||||
.defaultValue(this.openApiSpecName));
|
||||
|
||||
@Override
|
||||
public String getTypeDeclaration(String name) {
|
||||
return name + ":" + name + "()";
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTypeDeclaration(Schema p) {
|
||||
String schemaType = getSchemaType(p);
|
||||
if (typeMapping.containsKey(schemaType)) {
|
||||
return typeMapping.get(schemaType);
|
||||
}
|
||||
return schemaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getSchemaType(Schema p) {
|
||||
String schemaType = super.getSchemaType(p);
|
||||
String type = null;
|
||||
if (typeMapping.containsKey(schemaType)) {
|
||||
type = typeMapping.get(schemaType);
|
||||
if (languageSpecificPrimitives.contains(type))
|
||||
return (type);
|
||||
} else
|
||||
type = getTypeDeclaration(toModelName(lowerCamelCase(schemaType)));
|
||||
return type;
|
||||
additionalProperties.put("apiVersion", packageVersion);
|
||||
additionalProperties.put("apiPath", sourceFolder);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -173,6 +158,12 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
setPackageVersion("1.0.0");
|
||||
}
|
||||
|
||||
if (additionalProperties.containsKey(CodegenConstants.OPEN_API_SPEC_NAME)) {
|
||||
setOpenApiSpecName((String) additionalProperties.get(CodegenConstants.OPEN_API_SPEC_NAME));
|
||||
} else {
|
||||
additionalProperties.put(CodegenConstants.OPEN_API_SPEC_NAME, openApiSpecName);
|
||||
}
|
||||
|
||||
additionalProperties.put(CodegenConstants.PACKAGE_NAME, packageName);
|
||||
additionalProperties.put(CodegenConstants.PACKAGE_VERSION, packageVersion);
|
||||
|
||||
@ -195,8 +186,17 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
|
||||
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("utils.mustache", "", "src" + File.separator + this.packageName + "_utils.erl"));
|
||||
supportingFiles.add(new SupportingFile("README.mustache", "", "README.md"));
|
||||
supportingFiles.add(new SupportingFile("processor.mustache", "", toSourceFilePath("processor", "erl")));
|
||||
supportingFiles.add(new SupportingFile("utils.mustache", "", toSourceFilePath("utils", "erl")));
|
||||
supportingFiles.add(new SupportingFile("types.mustache", "", toPackageNameSrcFile("erl")));
|
||||
supportingFiles.add(new SupportingFile("validation.mustache", "", toSourceFilePath("validation", "erl")));
|
||||
supportingFiles.add(new SupportingFile("param_validator.mustache", "", toSourceFilePath("param_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("schema_validator.mustache", "", toSourceFilePath("schema_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("schema.mustache", "", toSourceFilePath("schema", "erl")));
|
||||
supportingFiles.add(new SupportingFile("openapi.mustache", "", toPrivFilePath(this.openApiSpecName, "json")));
|
||||
writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md"));
|
||||
|
||||
ModelUtils.setGenerateAliasAsModel(true);
|
||||
}
|
||||
|
||||
public String qsEncode(Object o) {
|
||||
@ -231,6 +231,14 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return camelize(name) + '_';
|
||||
}
|
||||
|
||||
@Override
|
||||
public String addRegularExpressionDelimiter(String pattern) {
|
||||
if (pattern != null) {
|
||||
return pattern.replaceAll("^/","").replaceAll("/$","");
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String apiFileFolder() {
|
||||
return outputFolder + File.separator + sourceFolder + File.separator;
|
||||
@ -273,7 +281,7 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
|
||||
@Override
|
||||
public String toModelName(String name) {
|
||||
return this.packageName + "_" + underscore(name.replaceAll("-", "_").replaceAll("\\.", "_"));
|
||||
return camelize(toModelFilename(name));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -281,11 +289,6 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return this.packageName + "_" + underscore(name.replaceAll("-", "_").replaceAll("\\.", "_"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toModelFilename(String name) {
|
||||
return this.packageName + "_" + underscore(name.replaceAll("\\.", "_"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toApiFilename(String name) {
|
||||
// replace - with _ e.g. created-at => created_at
|
||||
@ -344,6 +347,17 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return objs;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> postProcessSupportingFileData(Map<String, Object> objs) {
|
||||
generateJSONSpecFile(objs);
|
||||
return super.postProcessSupportingFileData(objs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArraySchemaTypeDeclaration(ArraySchema arraySchema, String name) {
|
||||
return getTypeDeclaration(name);
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
@ -466,4 +480,24 @@ public class ErlangClientCodegen extends DefaultCodegen implements CodegenConfig
|
||||
this.replacedPathName = replacedPathName;
|
||||
}
|
||||
}
|
||||
|
||||
protected String toModuleName(String name) {
|
||||
return this.packageName + "_" + underscore(name.replaceAll("-", "_"));
|
||||
}
|
||||
|
||||
public void setOpenApiSpecName(String openApiSpecName) {
|
||||
this.openApiSpecName = openApiSpecName;
|
||||
}
|
||||
|
||||
protected String toSourceFilePath(String name, String extension) {
|
||||
return "src" + File.separator + toModuleName(name) + "." + extension;
|
||||
}
|
||||
|
||||
protected String toPrivFilePath(String name, String extension) {
|
||||
return "priv" + File.separator + name + "." + extension;
|
||||
}
|
||||
|
||||
protected String toPackageNameSrcFile(String extension) {
|
||||
return "src" + File.separator + this.packageName + "." + extension;
|
||||
}
|
||||
}
|
||||
|
@ -17,10 +17,16 @@
|
||||
|
||||
package org.openapitools.codegen.languages;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import org.openapitools.codegen.*;
|
||||
import org.openapitools.codegen.meta.features.*;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import io.swagger.v3.core.util.Json;
|
||||
import io.swagger.v3.oas.models.OpenAPI;
|
||||
import io.swagger.v3.oas.models.media.ArraySchema;
|
||||
import org.openapitools.codegen.utils.ModelUtils;
|
||||
import org.openapitools.codegen.serializer.SerializerUtils;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.Arrays;
|
||||
@ -104,29 +110,30 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig
|
||||
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("uri", "binary");
|
||||
typeMapping.put("password", "binary");
|
||||
typeMapping.put("enum", "binary");
|
||||
typeMapping.put("date", "binary");
|
||||
typeMapping.put("DateTime", "binary");
|
||||
typeMapping.put("string", "binary");
|
||||
typeMapping.put("char", "binary");
|
||||
typeMapping.put("binary", "binary");
|
||||
typeMapping.put("UUID", "binary");
|
||||
typeMapping.put("password", "binary");
|
||||
typeMapping.put("boolean", "boolean");
|
||||
typeMapping.put("integer", "integer");
|
||||
typeMapping.put("long", "integer");
|
||||
typeMapping.put("float", "float");
|
||||
typeMapping.put("double", "float");
|
||||
typeMapping.put("number", "float");
|
||||
typeMapping.put("array", "list");
|
||||
typeMapping.put("List", "list");
|
||||
typeMapping.put("map", "map");
|
||||
typeMapping.put("object", "object");
|
||||
typeMapping.put("file", "file");
|
||||
typeMapping.put("ByteArray", "byte");
|
||||
typeMapping.put("int", "integer");
|
||||
typeMapping.put("bigdecimal", "float");
|
||||
typeMapping.put("byte", "binary");
|
||||
typeMapping.put("uri", "binary");
|
||||
|
||||
cliOptions.clear();
|
||||
cliOptions.add(new CliOption(CodegenConstants.PACKAGE_NAME, "Erlang package name (convention: lowercase).")
|
||||
@ -166,14 +173,20 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig
|
||||
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("openapi.mustache", "", toPrivFilePath(this.openApiSpecName, "json")));
|
||||
supportingFiles.add(new SupportingFile("default_logic_handler.mustache", "", toSourceFilePath("default_logic_handler", "erl")));
|
||||
supportingFiles.add(new SupportingFile("types.mustache", "", toPackageNameSrcFile("erl")));
|
||||
supportingFiles.add(new SupportingFile("handler_api.mustache", "", toSourceFilePath("handler_api", "erl")));
|
||||
supportingFiles.add(new SupportingFile("logic_handler.mustache", "", toSourceFilePath("logic_handler", "erl")));
|
||||
supportingFiles.add(new SupportingFile("validation.mustache", "", toSourceFilePath("validation", "erl")));
|
||||
supportingFiles.add(new SupportingFile("common_validator.mustache", "", toSourceFilePath("common_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("param_validator.mustache", "", toSourceFilePath("param_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("schema_validator.mustache", "", toSourceFilePath("schema_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("custom_validator.mustache", "", toSourceFilePath("custom_validator", "erl")));
|
||||
supportingFiles.add(new SupportingFile("schema.mustache", "", toSourceFilePath("schema", "erl")));
|
||||
supportingFiles.add(new SupportingFile("openapi.mustache", "", toPrivFilePath(this.openApiSpecName, "json")));
|
||||
writeOptional(outputFolder, new SupportingFile("README.mustache", "", "README.md"));
|
||||
|
||||
ModelUtils.setGenerateAliasAsModel(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -237,6 +250,21 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return "_" + name;
|
||||
}
|
||||
|
||||
/**
|
||||
* If the pattern contains "/" in the beginning or in the end
|
||||
* remove those "/" symbols.
|
||||
*
|
||||
* @param pattern the pattern (regular expression)
|
||||
* @return the pattern with delimiter
|
||||
*/
|
||||
@Override
|
||||
public String addRegularExpressionDelimiter(String pattern) {
|
||||
if (pattern != null) {
|
||||
return pattern.replaceAll("^/","").replaceAll("/$","");
|
||||
}
|
||||
return pattern;
|
||||
}
|
||||
|
||||
/**
|
||||
* Location to write api files. You can use the apiPackage() as defined when the class is
|
||||
* instantiated
|
||||
@ -285,6 +313,11 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return super.postProcessSupportingFileData(objs);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getArraySchemaTypeDeclaration(ArraySchema arraySchema, String name) {
|
||||
return getTypeDeclaration(name);
|
||||
}
|
||||
|
||||
public void setPackageName(String packageName) {
|
||||
this.packageName = packageName;
|
||||
}
|
||||
@ -313,6 +346,10 @@ public class ErlangServerCodegen extends DefaultCodegen implements CodegenConfig
|
||||
return "priv" + File.separator + name + "." + extension;
|
||||
}
|
||||
|
||||
protected String toPackageNameSrcFile(String extension) {
|
||||
return "src" + File.separator + this.packageName + "." + extension;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String escapeQuotationMark(String input) {
|
||||
// remove ' to avoid code injection
|
||||
|
@ -1,5 +1,19 @@
|
||||
# OpenAPI client server library for Erlang
|
||||
# OpenAPI rest client library for Erlang
|
||||
|
||||
## Overview
|
||||
|
||||
An Erlang client stub generated by [OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec.
|
||||
|
||||
Dependency: [hackney](https://github.com/benoitc/hackney)
|
||||
|
||||
## Supported features
|
||||
|
||||
Currently only features available in OAS2 specification are supported
|
||||
|
||||
## Prerequisites
|
||||
|
||||
TODO
|
||||
|
||||
## Getting started
|
||||
|
||||
TODO
|
||||
|
@ -1,35 +1,65 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{classname}}_api).
|
||||
|
||||
-export([{{#operations}}{{#operation}}{{^-first}},
|
||||
{{/-first}}{{operationId}}/{{arityRequired}}, {{operationId}}/{{arityOptional}}{{/operation}}{{/operations}}]).
|
||||
%% generated methods
|
||||
{{#operations}}{{#operation}}
|
||||
-export([{{operationId}}/2]).
|
||||
-export([{{operationId}}/3]).
|
||||
{{/operation}}{{/operations}}
|
||||
{{#operations}}{{#operation}}
|
||||
-spec {{operationId}}(Endpoint :: {{packageName}}:endpoint(), Params :: map()) ->
|
||||
{ok, Code :: integer(), RespHeaders :: list(), Response :: map()} |
|
||||
{error, _Reason}.
|
||||
{{operationId}}(Endpoint, Params) ->
|
||||
{{operationId}}(Endpoint, Params, []).
|
||||
|
||||
-define(BASE_URL, "{{{basePathWithoutHost}}}").
|
||||
-spec {{operationId}}(Endpoint :: {{packageName}}:endpoint(), Params :: map(), Opts :: {{packageName}}:transport_opts()) ->
|
||||
{ok, Code :: integer(), RespHeaders :: list(), Response :: map()} |
|
||||
{error, _Reason}.
|
||||
{{operationId}}(Endpoint, Params, Opts) ->
|
||||
process_response({{packageName}}_processor:process_request(
|
||||
{{httpMethod}},
|
||||
{{packageName}}_utils:get_url(Endpoint, "{{basePathWithoutHost}}{{path}}"),
|
||||
Params,
|
||||
get_request_spec({{operationId}}),
|
||||
Opts
|
||||
), {{operationId}}).
|
||||
{{/operation}}{{/operations}}
|
||||
process_response({ok, Code, Headers, RespBody}, OperationID) ->
|
||||
try {{packageName}}_processor:process_response(
|
||||
get_response_spec(OperationID, Code),
|
||||
RespBody
|
||||
) of
|
||||
{ok, Resp} ->
|
||||
{ok, Code, Headers, Resp};
|
||||
Error ->
|
||||
Error
|
||||
catch
|
||||
error:invalid_response_code ->
|
||||
{error, {invalid_response_code, Code}}
|
||||
end;
|
||||
process_response(Error, _) ->
|
||||
Error.
|
||||
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
%% @doc {{{summary}}}
|
||||
{{^notes.isEmpty}}
|
||||
%% {{{notes}}}
|
||||
{{/notes.isEmpty}}
|
||||
-spec {{operationId}}(ctx:ctx(){{#allParams}}{{#required}}, {{{dataType}}}{{/required}}{{/allParams}}) -> {ok, {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}[]{{/returnType}}, {{packageName}}_utils:response_info()} | {ok, hackney:client_ref()} | {error, term(), {{packageName}}_utils:response_info()}.
|
||||
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}) ->
|
||||
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}, #{}).
|
||||
|
||||
-spec {{operationId}}(ctx:ctx(){{#allParams}}{{#required}}, {{{dataType}}}{{/required}}{{/allParams}}, maps:map()) -> {ok, {{#returnType}}{{{returnType}}}{{/returnType}}{{^returnType}}[]{{/returnType}}, {{packageName}}_utils:response_info()} | {ok, hackney:client_ref()} | {error, term(), {{packageName}}_utils:response_info()}.
|
||||
{{operationId}}(Ctx{{#allParams}}{{#required}}, {{paramName}}{{/required}}{{/allParams}}, Optional) ->
|
||||
_OptionalParams = maps:get(params, Optional, #{}),
|
||||
Cfg = maps:get(cfg, Optional, application:get_env(kuberl, config, #{})),
|
||||
-spec get_request_spec(OperationID :: {{packageName}}:operation_id()) ->
|
||||
Spec :: {{packageName}}_processor:request_spec().
|
||||
{{#operations}}{{#operation}}
|
||||
get_request_spec('{{operationId}}') ->
|
||||
[
|
||||
{{#allParams}}{{^isBodyParam}}{'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}{'{{dataType}}'{{/isBodyParam}}, #{
|
||||
source => {{#isQueryParam}}qs_val{{/isQueryParam}}{{#isPathParam}}binding{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isBodyParam}}body{{/isBodyParam}},
|
||||
rules => [{{^isBodyParam}}{{#isListContainer}}{list, '{{collectionFormat}}', {{#items}}[{{>api.param_info}}]{{/items}}}, {{/isListContainer}}{{>api.param_info}}, {{/isBodyParam}}{{#isBodyParam}}schema, {{/isBodyParam}}{required, {{#required}}true{{/required}}{{^required}}false{{/required}}}]
|
||||
}}{{#hasMore}},
|
||||
{{/hasMore}}{{/allParams}}
|
||||
]{{#hasMore}};{{/hasMore}}{{/operation}}.{{/operations}}
|
||||
|
||||
Method = {{httpMethod}},
|
||||
Path = ["{{{replacedPathName}}}"],
|
||||
QS = {{#queryParams.isEmpty}}[]{{/queryParams.isEmpty}}{{^queryParams.isEmpty}}lists:flatten([{{#joinWithComma}}{{#queryParams}}{{#required}}{{#qsEncode}}{{this}}{{/qsEncode}} {{/required}}{{/queryParams}}{{/joinWithComma}}])++{{packageName}}_utils:optional_params([{{#joinWithComma}}{{#queryParams}}{{^required}} '{{baseName}}'{{/required}}{{/queryParams}}{{/joinWithComma}}], _OptionalParams){{/queryParams.isEmpty}},
|
||||
Headers = {{#headerParams.isEmpty}}[]{{/headerParams.isEmpty}}{{^headerParams.isEmpty}}[{{#headerParams}}{{#required}} {<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/headerParams}}]++{{packageName}}_utils:optional_params([{{#joinWithComma}}{{#headerParams}}{{^required}} '{{baseName}}'{{/required}}{{/headerParams}}{{/joinWithComma}}], _OptionalParams){{/headerParams.isEmpty}},
|
||||
Body1 = {{^formParams.isEmpty}}{form, [{{#joinWithComma}}{{#formParams}}{{#required}} {<<"{{baseName}}">>, {{paramName}}}{{/required}}{{/formParams}}{{/joinWithComma}}]++{{packageName}}_utils:optional_params([{{#joinWithComma}}{{#formParams}}{{^required}} '{{baseName}}'{{/required}}{{/formParams}}{{/joinWithComma}}], _OptionalParams)}{{/formParams.isEmpty}}{{#formParams.isEmpty}}{{#bodyParams.isEmpty}}[]{{/bodyParams.isEmpty}}{{^bodyParams.isEmpty}}{{#bodyParams}}{{paramName}}{{/bodyParams}}{{/bodyParams.isEmpty}}{{/formParams.isEmpty}},
|
||||
ContentTypeHeader = {{packageName}}_utils:select_header_content_type([{{#consumes}}{{^-first}}, {{/-first}}<<"{{mediaType}}">>{{/consumes}}]),
|
||||
Opts = maps:get(hackney_opts, Optional, []),
|
||||
-spec get_response_spec(OperationID :: {{packageName}}:operation_id(), Code :: {{packageName}}_processor:code()) ->
|
||||
Spec :: {{packageName}}_processor:response_spec() | no_return().
|
||||
|
||||
{{packageName}}_utils:request(Ctx, Method, [?BASE_URL, Path], QS, ContentTypeHeader++Headers, Body1, Opts, Cfg).
|
||||
|
||||
{{/operation}}
|
||||
|
||||
{{/operations}}
|
||||
{{#operations}}{{#operation}}{{#responses}}
|
||||
get_response_spec('{{operationId}}', {{code}}) ->
|
||||
{{#dataType}}{'{{dataType}}', '{{baseType}}'};{{/dataType}}{{^dataType}}undefined;{{/dataType}}
|
||||
{{/responses}}{{/operation}}{{/operations}}
|
||||
get_response_spec(_, _) ->
|
||||
error(invalid_response_code).
|
||||
|
1
modules/openapi-generator/src/main/resources/erlang-client/api.param_info.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/erlang-client/api.param_info.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{^isContainer}}{{#isString}}{type, 'binary'}, {{/isString}}{{#isInteger}}{type, 'int32'}, {{/isInteger}}{{#isLong}}{type, 'int64'}, {{/isLong}}{{#isFloat}}{type, 'float'}, {{/isFloat}}{{#isDouble}}{type, 'float'}, {{/isDouble}}{{#isByteArray}}{type, 'byte'}, {{/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}}, {{#exclusiveMaximum}}exclusive{{/exclusiveMaximum}}{{^exclusiveMaximum}}inclusive{{/exclusiveMaximum}}}, {{/maximum}}{{#minimum}}{min, {{minimum}}, {{#exclusiveMinimum}}exclusive{{/exclusiveMinimum}}{{^exclusiveMinimum}}inclusive{{/exclusiveMinimum}}},{{/minimum}}{{#maxLength}}{max_length, {{maxLength}}}, {{/maxLength}}{{#minLength}}{min_length, {{minLength}}}, {{/minLength}}{{#pattern}}{pattern, "{{pattern}}"}, {{/pattern}}{{/isContainer}}true
|
@ -1,18 +1,18 @@
|
||||
{application, {{packageName}},
|
||||
[{description, {{#appDescription}}"{{appDescription}}"{{/appDescription}}{{^appDescription}}"OpenAPI client library"{{/appDescription}}},
|
||||
{vsn, "{{#apiVersion}}{{apiVersion}}{{/apiVersion}}{{^apiVersion}}0.1.0{{/apiVersion}}"},
|
||||
{registered, []},
|
||||
{applications,
|
||||
[kernel,
|
||||
stdlib,
|
||||
ssl,
|
||||
hackney,
|
||||
ctx
|
||||
{application, {{packageName}}, [
|
||||
{description, {{#appDescription}}"{{appDescription}}"{{/appDescription}}{{^appDescription}}"OpenAPI rest client library"{{/appDescription}}},
|
||||
{vsn, "{{apiVersion}}"},
|
||||
{registered, []},
|
||||
{applications, [
|
||||
kernel,
|
||||
stdlib,
|
||||
hackney,
|
||||
jsx,
|
||||
jesse
|
||||
]},
|
||||
{env, [{host, "{{#host}}{{{host}}}{{/host}}{{^host}}localhost{{/host}}"}]},
|
||||
{modules, []},
|
||||
|
||||
{maintainers, []},
|
||||
{licenses, [{{#licenseInfo}}"{{licenseInfo}}"{{/licenseInfo}}]},
|
||||
{links, [{{#infoUrl}}"{{infoUrl}}"{{/infoUrl}}]}
|
||||
{env, [
|
||||
]},
|
||||
{modules, []},
|
||||
{licenses, [{{#licenseInfo}}"{{licenseInfo}}"{{/licenseInfo}}]},
|
||||
{links, [{{#infoUrl}}"{{infoUrl}}"{{/infoUrl}}]}
|
||||
]}.
|
||||
|
||||
|
@ -1,21 +0,0 @@
|
||||
{{#models}}
|
||||
{{#model}}
|
||||
-module({{classname}}).
|
||||
|
||||
-export([encode/1]).
|
||||
|
||||
-export_type([{{classname}}/0]).
|
||||
|
||||
-type {{classname}}() ::
|
||||
#{ {{#vars}}'{{name}}' {{#required}}:={{/required}}{{^required}}=>{{/required}} {{{dataType}}}{{#hasMore}},
|
||||
{{/hasMore}}{{/vars}}
|
||||
}.
|
||||
|
||||
encode(#{ {{#vars}}'{{name}}' := {{{nameInCamelCase}}}{{#hasMore}},
|
||||
{{/hasMore}}{{/vars}}
|
||||
}) ->
|
||||
#{ {{#vars}}'{{baseName}}' => {{{nameInCamelCase}}}{{#hasMore}},
|
||||
{{/hasMore}}{{/vars}}
|
||||
}.
|
||||
{{/model}}
|
||||
{{/models}}
|
1
modules/openapi-generator/src/main/resources/erlang-client/openapi.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/erlang-client/openapi.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{{openapi-json}}}
|
372
modules/openapi-generator/src/main/resources/erlang-client/param_validator.mustache
vendored
Normal file
372
modules/openapi-generator/src/main/resources/erlang-client/param_validator.mustache
vendored
Normal file
@ -0,0 +1,372 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_param_validator).
|
||||
|
||||
-export([validate/2]).
|
||||
|
||||
-type param_base_rule() ::
|
||||
{type, 'binary'} |
|
||||
{type, 'byte'} |
|
||||
{type, 'int32'} |
|
||||
{type, 'int64'} |
|
||||
{type, 'float'} |
|
||||
{type, 'boolean'} |
|
||||
{type, 'date'} |
|
||||
{type, 'datetime'} |
|
||||
{enum, [atom()]} |
|
||||
{max, Max :: number(), Type :: exclusive | inclusive} |
|
||||
{min, Min :: number(), Type :: exclusive | inclusive} |
|
||||
{max_length, MaxLength :: integer()} |
|
||||
{min_length, MaxLength :: integer()} |
|
||||
{pattern, Pattern :: string()} |
|
||||
boolean().
|
||||
-type collection_format() ::
|
||||
'csv' |
|
||||
'ssv' |
|
||||
'tsv' |
|
||||
'pipes'.
|
||||
-type param_rule() ::
|
||||
param_base_rule() |
|
||||
{'list', collection_format(), [param_base_rule()]}.
|
||||
|
||||
-export_type([param_rule/0]).
|
||||
|
||||
|
||||
%% API
|
||||
|
||||
-spec validate(Rule :: param_rule(), Param :: {{packageName}}:value()) ->
|
||||
ok | {ok, Prepared :: {{packageName}}:value()} | error.
|
||||
|
||||
validate(true, _Value) ->
|
||||
ok;
|
||||
|
||||
validate(false, _Value) ->
|
||||
error;
|
||||
|
||||
validate({'list', Format, Ruleset}, Value) ->
|
||||
try
|
||||
Values = parse_array(Format, Value),
|
||||
{ok, [validate_ruleset(Ruleset, V) || V <- Values]}
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'int64'}, Value0) ->
|
||||
try
|
||||
Value = {{packageName}}_utils:to_int(Value0),
|
||||
ok = validate_between(Value, -9223372036854775808, 922337203685477580),
|
||||
{ok, Value}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'int32'}, Value0) ->
|
||||
try
|
||||
Value = {{packageName}}_utils:to_int(Value0),
|
||||
ok = validate_between(Value, -2147483648, 2147483647),
|
||||
{ok, Value}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'float'}, Value) ->
|
||||
try
|
||||
{ok, {{packageName}}_utils:to_float(Value)}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'binary'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({type, 'byte'}, Value) ->
|
||||
try
|
||||
validate_base64(Value)
|
||||
catch error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'boolean'}, Value) when is_boolean(Value) ->
|
||||
{ok, Value};
|
||||
|
||||
validate({type, 'boolean'}, Value) ->
|
||||
V = {{packageName}}_utils:to_lower(Value),
|
||||
try
|
||||
case {{packageName}}_utils:binary_to_existing_atom(V, utf8) of
|
||||
B when is_boolean(B) -> {ok, B};
|
||||
_ -> error
|
||||
end
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'date'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true ->
|
||||
validate_date(Value);
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({type, 'datetime'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true ->
|
||||
validate_datetime(Value);
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({enum, Values}, Value) ->
|
||||
try
|
||||
FormattedValue = {{packageName}}_utils:binary_to_existing_atom(Value, utf8),
|
||||
case lists:member(FormattedValue, Values) of
|
||||
true -> {ok, FormattedValue};
|
||||
false -> error
|
||||
end
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({max, Max, Type}, Value) ->
|
||||
Result = case Value of
|
||||
_ when Value < Max andalso Type =:= exclusive ->
|
||||
true;
|
||||
_ when Value =< Max andalso Type =:= inclusive ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
case Result of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({min, Min, Type}, Value) ->
|
||||
Result = case Value of
|
||||
_ when Value > Min andalso Type =:= exclusive ->
|
||||
true;
|
||||
_ when Value >= Min andalso Type =:= inclusive ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
case Result of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({max_length, MaxLength}, Value) ->
|
||||
case size(Value) =< MaxLength of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({min_length, MinLength}, Value) ->
|
||||
case size(Value) >= MinLength of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({pattern, Pattern}, Value) ->
|
||||
{ok, MP} = re:compile(Pattern, [unicode, ucp]),
|
||||
case re:run(Value, MP) of
|
||||
{match, _} -> ok;
|
||||
_ -> error
|
||||
end.
|
||||
|
||||
-spec validate_between(Value :: {{packageName}}:value(), Min :: integer(), Max :: integer()) ->
|
||||
ok | no_return().
|
||||
|
||||
validate_between(Value, Min, Max) when
|
||||
is_integer(Value),
|
||||
Value >= Min,
|
||||
Value =< Max ->
|
||||
ok;
|
||||
|
||||
validate_between(_, _, _) ->
|
||||
error(badarg).
|
||||
|
||||
|
||||
%% Internal
|
||||
|
||||
-spec validate_base64(Value :: {{packageName}}:value()) ->
|
||||
ok | no_return().
|
||||
|
||||
validate_base64(Value) when is_binary(Value) ->
|
||||
try
|
||||
_ = base64:decode(Value),
|
||||
ok
|
||||
catch
|
||||
_:_ ->
|
||||
error(badarg)
|
||||
end;
|
||||
|
||||
validate_base64(_) ->
|
||||
error(badarg).
|
||||
|
||||
|
||||
-spec validate_date(Value :: binary()) ->
|
||||
ok | error.
|
||||
|
||||
validate_date(Value) when is_binary(Value) ->
|
||||
validate_datetime(<<Value/binary, "T00:00:00Z">>).
|
||||
|
||||
-spec validate_datetime(Value :: binary()) ->
|
||||
ok | error.
|
||||
|
||||
validate_datetime(Value) when is_binary(Value) ->
|
||||
Str = erlang:binary_to_list(Value),
|
||||
try
|
||||
_Seconds = calendar:rfc3339_to_system_time(Str),
|
||||
ok
|
||||
catch
|
||||
error:_ ->
|
||||
error
|
||||
end.
|
||||
|
||||
-spec validate_ruleset(
|
||||
Ruleset :: [param_base_rule()],
|
||||
Value :: {{packageName}}:value()
|
||||
) ->
|
||||
Value :: {{packageName}}:value().
|
||||
|
||||
validate_ruleset(Ruleset, Value) ->
|
||||
lists:foldl(
|
||||
fun(R, V0) ->
|
||||
case validate(R, Value) of
|
||||
{ok, V} -> V;
|
||||
ok -> V0;
|
||||
error -> throw(wrong_param)
|
||||
end
|
||||
end,
|
||||
Value,
|
||||
Ruleset
|
||||
).
|
||||
|
||||
-spec parse_array(
|
||||
Format :: collection_format(),
|
||||
Array :: binary()
|
||||
) ->
|
||||
Values :: [binary()].
|
||||
|
||||
parse_array(Format, Array) ->
|
||||
binary:split(Array, get_split_pattern(Format), [global]).
|
||||
|
||||
get_split_pattern('csv') ->
|
||||
<<",">>;
|
||||
get_split_pattern('ssv') ->
|
||||
<<" ">>;
|
||||
get_split_pattern('tsv') ->
|
||||
<<"\t">>;
|
||||
get_split_pattern('pipes') ->
|
||||
<<"|">>.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
-spec validate_int64_test() -> _.
|
||||
-spec validate_int32_test() -> _.
|
||||
-spec validate_float_test() -> _.
|
||||
-spec validate_binary_test() -> _.
|
||||
-spec validate_byte_test() -> _.
|
||||
-spec validate_boolean_test() -> _.
|
||||
-spec validate_date_test() -> _.
|
||||
-spec validate_datetime_test() -> _.
|
||||
-spec validate_enum_test() -> _.
|
||||
-spec validate_max_test() -> _.
|
||||
-spec validate_min_test() -> _.
|
||||
-spec validate_max_length_test() -> _.
|
||||
-spec validate_min_length_test() -> _.
|
||||
-spec validate_pattern_test() -> _.
|
||||
-spec validate_array_test() -> _.
|
||||
|
||||
validate_int64_test() ->
|
||||
?assertEqual({ok, 2}, validate({type, 'int64'},2)),
|
||||
?assertEqual({ok, 6}, validate({type, 'int64'},<<"6">>)),
|
||||
?assertEqual(error, validate({type, 'int64'}, 922337203685477581)),
|
||||
?assertEqual(error, validate({type, 'int64'},-9223372036854775809)).
|
||||
|
||||
validate_int32_test() ->
|
||||
?assertEqual({ok, 6}, validate({type, 'int32'}, 6)),
|
||||
?assertEqual({ok, 21}, validate({type, 'int32'}, <<"21">>)),
|
||||
?assertEqual(error, validate({type, 'int32'}, -2147483649)),
|
||||
?assertEqual(error, validate({type, 'int32'},2147483648)).
|
||||
|
||||
validate_float_test() ->
|
||||
?assertEqual({ok, 1.9}, validate({type, 'float'}, <<"1.9">>)),
|
||||
?assertEqual({ok, 3.0}, validate({type, 'float'}, <<"3">>)),
|
||||
?assertEqual(error, validate({type, 'float'}, <<"c">>)).
|
||||
|
||||
validate_binary_test() ->
|
||||
?assertEqual(ok, validate({type, 'binary'}, <<"f">>)),
|
||||
?assertEqual(error, validate({type, 'binary'}, [])),
|
||||
?assertEqual(error, validate({type, 'binary'}, 3)).
|
||||
|
||||
validate_byte_test() ->
|
||||
?assertEqual(ok, validate({type, 'byte'}, <<"0YXRg9C5">>)),
|
||||
?assertEqual(error, validate({type, 'byte'}, <<"g">>)).
|
||||
|
||||
validate_boolean_test() ->
|
||||
?assertEqual({ok, true}, validate({type, 'boolean'}, <<"true">>)),
|
||||
?assertEqual({ok, false}, validate({type, 'boolean'}, <<"false">>)),
|
||||
?assertEqual({ok, false}, validate({type, 'boolean'}, false)),
|
||||
?assertEqual(error, validate({type, 'boolean'}, <<"nope">>)).
|
||||
|
||||
validate_date_test() ->
|
||||
?assertEqual(ok, validate({type, 'date'}, <<"2014-03-19">>)),
|
||||
?assertEqual(error, validate({type, 'date'}, <<"2014-19-03">>)),
|
||||
?assertEqual(error, validate({type, 'date'}, <<"2013">>)),
|
||||
?assertEqual(error, validate({type, 'date'}, <<"nope">>)),
|
||||
?assertEqual(error, validate({type, 'date'}, <<"2014-03-19 18:00:05-04:00">>)).
|
||||
|
||||
validate_datetime_test() ->
|
||||
?assertEqual(ok, validate({type, 'datetime'}, <<"2014-03-19T18:35:03-04:00">>)),
|
||||
?assertEqual(error, validate({type, 'datetime'}, <<"2014-11-19">>)),
|
||||
?assertEqual(error, validate({type, 'datetime'}, <<"nope">>)).
|
||||
|
||||
validate_enum_test() ->
|
||||
?assertEqual({ok, sad}, validate({enum, [i, am, sad]} , <<"sad">>)),
|
||||
?assertEqual(error, validate({enum, ['All work and no play', 'makes Jack a dull boy']}, <<"Artem">>)),
|
||||
?assertEqual(error, validate({enum, []}, <<"">>)).
|
||||
|
||||
validate_max_test() ->
|
||||
?assertEqual(ok, validate({max, 10, inclusive}, 10)),
|
||||
?assertEqual(error, validate({max, 10, exclusive}, 10)),
|
||||
?assertEqual(ok, validate({max, 32, inclusive}, 21)),
|
||||
?assertEqual(ok, validate({max, 32, exclusive}, 21)).
|
||||
|
||||
validate_min_test() ->
|
||||
?assertEqual(ok, validate({min, 33, inclusive}, 33)),
|
||||
?assertEqual(error, validate({min, 33, exclusive}, 33)),
|
||||
?assertEqual(ok, validate({min, 57, inclusive}, 60)),
|
||||
?assertEqual(ok, validate({min, 57, inclusive}, 60)).
|
||||
|
||||
validate_max_length_test() ->
|
||||
?assertEqual(ok, validate({max_length, 5}, <<"hello">>)),
|
||||
?assertEqual(ok, validate({max_length, 5}, <<"h">>)),
|
||||
?assertEqual(error, validate({max_length, 5}, <<"hello?">>)).
|
||||
|
||||
validate_min_length_test() ->
|
||||
?assertEqual(ok, validate({min_length, 5}, <<"hello">>)),
|
||||
?assertEqual(ok, validate({min_length, 5}, <<"hello?">>)),
|
||||
?assertEqual(error, validate({min_length, 5}, <<"h">>)).
|
||||
|
||||
validate_pattern_test() ->
|
||||
?assertEqual(ok, validate({pattern, <<"[abc]">>}, <<"adcv">>)),
|
||||
?assertEqual(error, validate({pattern, <<"[abc]">>}, <<"fgh0">>)),
|
||||
?assertEqual(ok, validate({pattern, <<"^[0-9]{2}\/[0-9]{2}$">>}, <<"22/22">>)),
|
||||
?assertEqual(error, validate({pattern, <<"^[0-9]{2}\/[0-9]{2}$">>}, <<"22/225">>)).
|
||||
|
||||
validate_array_test() ->
|
||||
?assertEqual({ok, [10,11,12]}, validate({list, 'csv', [{type, 'int32'}]}, <<"10,11,12">>)),
|
||||
?assertEqual(error, validate({list, 'csv', [{type, 'int32'}]}, <<"10,xyi,12">>)).
|
||||
|
||||
-endif.
|
137
modules/openapi-generator/src/main/resources/erlang-client/processor.mustache
vendored
Normal file
137
modules/openapi-generator/src/main/resources/erlang-client/processor.mustache
vendored
Normal file
@ -0,0 +1,137 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_processor).
|
||||
|
||||
-export([process_request/5]).
|
||||
-export([process_response/2]).
|
||||
|
||||
%%
|
||||
|
||||
-type params() :: #{
|
||||
header := any(),
|
||||
binding := any(),
|
||||
body := any(),
|
||||
qs_val := any()
|
||||
}.
|
||||
-type opts() :: {{packageName}}:transport_opts().
|
||||
-type object() :: {{packageName}}:object().
|
||||
-type url() :: string().
|
||||
-type method() :: atom().
|
||||
-type code() :: pos_integer().
|
||||
-type body() :: binary().
|
||||
|
||||
-type param_source() ::
|
||||
qs_val |
|
||||
binding |
|
||||
header |
|
||||
body.
|
||||
|
||||
-type request_spec() :: [{
|
||||
{{packageName}}:param_name(),
|
||||
#{source := param_source(), rules := [{{packageName}}_validation:rule()]}
|
||||
}].
|
||||
|
||||
-type response_spec() :: {{packageName}}_validation:response_spec().
|
||||
|
||||
-export_type([code/0]).
|
||||
-export_type([body/0]).
|
||||
-export_type([request_spec/0]).
|
||||
-export_type([response_spec/0]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec process_request(method(), url(), params(), request_spec(), opts()) ->
|
||||
{ok, code(), list(), body()} |
|
||||
{error, {request_validation_failed, { {{packageName}}_validation:error(), BadParam :: atom()}}} |
|
||||
{error, _Reason}.
|
||||
process_request(Method, BaseUrl, Params, Spec, Opts) ->
|
||||
case prepare_request(Spec, BaseUrl, Params) of
|
||||
{ok, #{
|
||||
url := Url,
|
||||
headers := Headers,
|
||||
body := Body
|
||||
}} ->
|
||||
hackney:request(Method, Url, Headers, Body, [with_body] ++ Opts);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
-spec process_response(response_spec(), body()) ->
|
||||
{ok, object()} |
|
||||
{error, {response_validation_failed, {{packageName}}_validation:error(), _Response}}.
|
||||
process_response(undefined, <<>>) ->
|
||||
{ok, #{}};
|
||||
process_response(Spec, RespBody) ->
|
||||
Resp = jsx:decode(RespBody, [return_maps]),
|
||||
case validate_response(Spec, Resp) of
|
||||
ok ->
|
||||
{ok, Resp};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
%% Internal
|
||||
|
||||
prepare_request(Spec, Url, Params) ->
|
||||
case validate_request(Spec, Params) of
|
||||
ok ->
|
||||
{ok, prepare_params(Url, Params)};
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
prepare_params(Url, #{binding := Bindings, qs_val := Query, header := Headers, body := Body}) ->
|
||||
#{
|
||||
url => prepare_url(Url, Bindings, Query),
|
||||
headers => maps:to_list(Headers),
|
||||
body => jsx:encode(Body)
|
||||
}.
|
||||
|
||||
prepare_url(Url, Params, Qs) ->
|
||||
{{packageName}}_utils:fill_url(Url, Params, Qs).
|
||||
|
||||
validate_request([], _) ->
|
||||
ok;
|
||||
validate_request([ParamSpec | T], Params) ->
|
||||
case validate_request_param(ParamSpec, Params) of
|
||||
ok ->
|
||||
validate_request(T, Params);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
validate_request_param({Name, #{rules := Rules, source := Source}}, Params) ->
|
||||
Value = get_value(Source, Name, Params),
|
||||
case {{packageName}}_validation:prepare_request_param(Rules, Name, Value) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
{error, {request_validation_failed, {Reason, Name}}}
|
||||
end.
|
||||
|
||||
-spec get_value(atom(), any(), params()) ->
|
||||
any().
|
||||
get_value(body, _Name, Params) ->
|
||||
maps:get(body, Params);
|
||||
get_value(qs_val, Name, Params) ->
|
||||
QueryParams = maps:get(qs_val, Params),
|
||||
get_opt({{packageName}}_utils:to_binary(Name), QueryParams);
|
||||
get_value(header, Name, Params) ->
|
||||
HeaderParams = maps:get(header, Params),
|
||||
get_opt({{packageName}}_utils:to_binary(Name), HeaderParams);
|
||||
get_value(binding, Name, Params) ->
|
||||
BindingParams = maps:get(binding, Params),
|
||||
get_opt({{packageName}}_utils:to_binary(Name), BindingParams).
|
||||
|
||||
get_opt(Key, Opts) ->
|
||||
get_opt(Key, Opts, undefined).
|
||||
|
||||
get_opt(Key, Opts, Default) ->
|
||||
maps:get(Key, Opts, Default).
|
||||
|
||||
validate_response(Spec, RespBody) ->
|
||||
case {{packageName}}_validation:validate_response(Spec, RespBody) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Error} ->
|
||||
{error, {response_validation_failed, Error, RespBody}}
|
||||
end.
|
@ -1,5 +1,20 @@
|
||||
{erl_opts, [debug_info, warnings_as_errors, warn_untyped_record]}.
|
||||
{erl_opts, [
|
||||
{parse_transform, ct_expand}
|
||||
]}.
|
||||
|
||||
{deps, [ctx, jsx, hackney]}.
|
||||
|
||||
{shell, [{apps, [{{packageName}}]}]}.
|
||||
{deps, [
|
||||
{hackney, "1.15.1"},
|
||||
{parse_trans,
|
||||
{git, "https://github.com/uwiger/parse_trans.git",
|
||||
{ref, "76abb347c3c1d00fb0ccf9e4b43e22b3d2288484"}
|
||||
}
|
||||
},
|
||||
{jsx,
|
||||
{git, "https://github.com/talentdeficit/jsx.git", {tag, "2.9.0"}}
|
||||
},
|
||||
{jesse,
|
||||
{git, "https://github.com/rbkmoney/jesse.git",
|
||||
{ref, "600cc8318c685de60a1ceee055b3f69bc884800d"}
|
||||
}
|
||||
}
|
||||
]}.
|
||||
|
238
modules/openapi-generator/src/main/resources/erlang-client/schema.mustache
vendored
Normal file
238
modules/openapi-generator/src/main/resources/erlang-client/schema.mustache
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_schema).
|
||||
|
||||
-export([get/0]).
|
||||
-export([get_raw/0]).
|
||||
-export([load_raw/0]).
|
||||
-export([enumerate_components/1]).
|
||||
|
||||
-define(COMPONENTS, <<"components">>).
|
||||
|
||||
-spec get() -> {{packageName}}:object().
|
||||
get() ->
|
||||
ct_expand:term(enumerate_components(maps:with([?COMPONENTS], load_raw()))).
|
||||
|
||||
-spec enumerate_components(Schema :: map()) ->
|
||||
Schema :: map() | no_return().
|
||||
enumerate_components(Schema = #{?COMPONENTS := Components}) ->
|
||||
%@NOTICE only parents within the same component type are supported
|
||||
Schema#{?COMPONENTS => maps:map(fun enumerate_discriminator_children/2, Components)};
|
||||
enumerate_components(Schema) ->
|
||||
Schema.
|
||||
|
||||
-spec enumerate_discriminator_children(ComponentType :: binary(), Schema :: map()) ->
|
||||
Schema :: map() | no_return().
|
||||
enumerate_discriminator_children(_ComponentType, Defs) ->
|
||||
try
|
||||
{Parents, _} = maps:fold(
|
||||
fun(Name, Schema, Acc) ->
|
||||
check_definition(Name, Schema, Acc)
|
||||
end,
|
||||
{#{}, #{}},
|
||||
Defs
|
||||
),
|
||||
maps:fold(
|
||||
fun(Parent, Children, Schema) ->
|
||||
correct_schema(Parent, Children, Schema)
|
||||
end,
|
||||
Defs,
|
||||
Parents
|
||||
)
|
||||
catch
|
||||
_:Error ->
|
||||
handle_error(Error)
|
||||
end.
|
||||
|
||||
-spec handle_error(_) ->
|
||||
no_return().
|
||||
handle_error(Error) ->
|
||||
erlang:error({schema_invalid, Error}).
|
||||
|
||||
check_definition(Name, Schema, Acc) ->
|
||||
Acc1 = check_discriminator(Name, Schema, Acc),
|
||||
check_backrefs(Name, Schema, Acc1).
|
||||
|
||||
check_discriminator(Name, Schema, {Parents, Candidates}) ->
|
||||
case maps:get(<<"discriminator">>, Schema, undefined) of
|
||||
undefined ->
|
||||
{Parents, Candidates};
|
||||
_ ->
|
||||
{
|
||||
Parents#{Name => maps:get(Name, Candidates, [])},
|
||||
maps:without([Name], Candidates)
|
||||
}
|
||||
end.
|
||||
|
||||
check_backrefs(Name, Schema, Acc) ->
|
||||
case maps:get(<<"allOf">>, Schema, undefined) of
|
||||
undefined ->
|
||||
Acc;
|
||||
AllOf ->
|
||||
lists:foldl(fun(E, A) -> check_allOf(E, Name, A) end, Acc, AllOf)
|
||||
end.
|
||||
|
||||
check_allOf(#{<<"$ref">> := RefPath}, Child, {Parents, Candidates}) ->
|
||||
Parent = get_parent_from_ref(RefPath),
|
||||
case maps:get(Parent, Parents, undefined) of
|
||||
undefined ->
|
||||
{Parents, update_candidates(Parent, Child, Candidates)};
|
||||
Children ->
|
||||
{Parents#{Parent => [Child | Children]}, Candidates}
|
||||
end;
|
||||
check_allOf(_, _, Acc) ->
|
||||
Acc.
|
||||
|
||||
get_parent_from_ref(RefPath) ->
|
||||
Split = binary:split(RefPath, [<<"/">>], [global]),
|
||||
lists:last(Split).
|
||||
|
||||
update_candidates(Parent, Child, Candidates) ->
|
||||
case maps:get(Parent, Candidates, undefined) of
|
||||
undefined ->
|
||||
Candidates#{Parent => [Child]};
|
||||
Children ->
|
||||
Candidates#{Parent => [Child | Children]}
|
||||
end.
|
||||
|
||||
correct_schema(Parent, Children, Schema) ->
|
||||
BasePath = [Parent],
|
||||
Discr = maps:get(<<"discriminator">>, get_sub_schema(BasePath, Schema)),
|
||||
PropertyName = maps:get(<<"propertyName">>, Discr),
|
||||
update_schema(Children, [<<"enum">>, PropertyName, <<"properties">> | BasePath], Schema).
|
||||
|
||||
update_schema(Value, [], _Schema) ->
|
||||
Value;
|
||||
update_schema(Value, [Key | Path], Schema) ->
|
||||
SubSchema0 = get_sub_schema(Path, Schema),
|
||||
SubSchema1 = update_sub_schema(Key, Value, SubSchema0),
|
||||
update_schema(SubSchema1, Path, Schema).
|
||||
|
||||
get_sub_schema(ReversedPath, Schema) ->
|
||||
lists:foldr(fun(K, S) -> maps:get(K, S) end, Schema, ReversedPath).
|
||||
|
||||
update_sub_schema(Key, Value, Schema) ->
|
||||
Schema#{Key => Value}.
|
||||
|
||||
-spec get_raw() -> map().
|
||||
get_raw() ->
|
||||
ct_expand:term(load_raw()).
|
||||
|
||||
-spec load_raw() -> map().
|
||||
load_raw() ->
|
||||
{ok, Data} = file:read_file(get_openapi_path()),
|
||||
jsx:decode(Data, [return_maps]).
|
||||
|
||||
get_openapi_path() ->
|
||||
filename:join(code:priv_dir({{packageName}}), "{{{openAPISpecName}}}.json").
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Pet\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"petType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"petType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"petType\"]
|
||||
},
|
||||
\"Cat\": {
|
||||
\"description\": \"A representation of a cat\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"huntingSkill\": {
|
||||
\"type\": \"string\",
|
||||
\"description\": \"The measured skill for hunting\",
|
||||
\"default\": \"lazy\",
|
||||
\"enum\": [\"clueless\", \"lazy\", \"adventurous\", \"aggressive\"]
|
||||
}
|
||||
},
|
||||
\"required\": [\"huntingSkill\"]
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Dog\": {
|
||||
\"description\": \"A representation of a dog\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"packSize\": {
|
||||
\"type\": \"integer\",
|
||||
\"format\": \"int32\",
|
||||
\"description\": \"the size of the pack the dog is from\",
|
||||
\"default\": 0,
|
||||
\"minimum\": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"required\": [\"packSize\"]
|
||||
},
|
||||
\"Person\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"personType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"sex\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"male\", \"female\"]
|
||||
},
|
||||
\"personType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"sex\", \"personType\"]
|
||||
},
|
||||
\"WildMix\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{\"$ref\": \"#/components/schemas/Person\"}
|
||||
],
|
||||
},
|
||||
\"Dummy\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"dummyType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"dummyType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"dummyType\"]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
|
||||
get_enum(Parent, Discr, ComponentType, Schema) ->
|
||||
lists:sort(get_sub_schema([<<"enum">>, Discr, <<"properties">>, Parent, ComponentType, ?COMPONENTS], Schema)).
|
||||
|
||||
-spec test() -> _.
|
||||
-spec enumerate_discriminator_children_test() -> _.
|
||||
enumerate_discriminator_children_test() ->
|
||||
Schema = jsx:decode(?SCHEMA, [return_maps]),
|
||||
FixedSchema = enumerate_components(Schema),
|
||||
?assertEqual(
|
||||
lists:sort([<<"Dog">>, <<"Cat">>, <<"WildMix">>]),
|
||||
get_enum(<<"Pet">>, <<"petType">>, <<"schemas">>, FixedSchema)
|
||||
),
|
||||
?assertEqual([<<"WildMix">>], get_enum(<<"Person">>, <<"personType">>, <<"schemas">>, FixedSchema)),
|
||||
?assertEqual([], get_enum(<<"Dummy">>, <<"dummyType">>, <<"schemas">>, FixedSchema)).
|
||||
|
||||
-spec get_test() -> _.
|
||||
get_test() ->
|
||||
?assertEqual(
|
||||
enumerate_components(maps:with([?COMPONENTS], get_raw())),
|
||||
?MODULE:get()
|
||||
).
|
||||
-endif.
|
489
modules/openapi-generator/src/main/resources/erlang-client/schema_validator.mustache
vendored
Normal file
489
modules/openapi-generator/src/main/resources/erlang-client/schema_validator.mustache
vendored
Normal file
@ -0,0 +1,489 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_schema_validator).
|
||||
|
||||
-behaviour(jesse_schema_validator).
|
||||
|
||||
%% API
|
||||
-export([validate/3]).
|
||||
|
||||
%% Behaviour callbacks
|
||||
-export([init_state/1]).
|
||||
-export([check_value/3]).
|
||||
|
||||
-define(DISCRIMINATOR, <<"discriminator">>).
|
||||
-define(ALLOF, <<"allOf">>).
|
||||
-define(FORMAT, <<"format">>).
|
||||
-define(REF, <<"$ref">>).
|
||||
-define(DEFINITIONS, "components/schemas").
|
||||
-define(NOT_FOUND, not_found).
|
||||
-define(READ_ONLY, <<"readOnly">>).
|
||||
|
||||
-define(DISCR_ERROR(Error), {discriminator_not_valid, Error}).
|
||||
-define(READ_ONLY_ERROR, read_only_property_in_request).
|
||||
|
||||
-type msg_type() :: request | response.
|
||||
-type state() ::
|
||||
#{
|
||||
refs => [],
|
||||
msg_type => msg_type()
|
||||
}.
|
||||
|
||||
%%
|
||||
-spec init_state(Opts :: jesse_state:validator_opts()) -> state().
|
||||
|
||||
init_state(Opts) ->
|
||||
Opts#{refs => []}.
|
||||
|
||||
-spec validate(
|
||||
Value :: {{packageName}}:value(),
|
||||
DefName :: {{packageName}}:param_name(),
|
||||
MsgType :: msg_type()
|
||||
) ->
|
||||
ok | {error, Error :: {{packageName}}:error_reason()}.
|
||||
validate(Value, DefName, MsgType) ->
|
||||
validate(Value, DefName, MsgType, {{packageName}}_schema:get()).
|
||||
|
||||
validate(Value, DefinitionName, MsgType, Schema) ->
|
||||
Options = [{validator_opts, #{msg_type => MsgType}} | options()],
|
||||
RefPath = "#/components/schemas/" ++ swag_server_utils:to_list(DefinitionName),
|
||||
case jesse:validate_local_ref(RefPath, Schema, Value, Options) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, [Error]} ->
|
||||
{error, map_error_reason(Error)}
|
||||
end.
|
||||
|
||||
-spec check_value(
|
||||
Value :: any(),
|
||||
Attr :: {binary(), jesse:json_term()},
|
||||
State :: jesse_state:state()
|
||||
) ->
|
||||
State :: jesse_state:state() |
|
||||
no_return().
|
||||
|
||||
check_value(Value, {?DISCRIMINATOR, DiscrField}, State) ->
|
||||
case jesse_lib:is_json_object(Value) of
|
||||
true -> validate_discriminator(Value, DiscrField, State);
|
||||
false -> State
|
||||
end;
|
||||
check_value(Value, {?ALLOF, Schemas}, State) -> % Override AllOf check to preserve error information
|
||||
check_all_of(Value, Schemas, State);
|
||||
check_value(Value, Attr = {?REF, Ref}, State) ->
|
||||
case is_recursive_ref(Ref, State) of
|
||||
true -> State;
|
||||
false -> validate_ref(Value, Attr, State)
|
||||
end;
|
||||
check_value(Value, Format = {?FORMAT, _}, State) ->
|
||||
validate_format(Value, Format, State);
|
||||
check_value(Value, {?READ_ONLY, ReadOnly}, State) ->
|
||||
validate_read_only(Value, ReadOnly, State);
|
||||
check_value(Value, Attr, State) ->
|
||||
jesse_validator_draft4:check_value(Value, Attr, State).
|
||||
|
||||
validate_discriminator(Value, #{<<"propertyName">> := DiscrField}, State) when is_binary(DiscrField) ->
|
||||
case jesse_json_path:value(DiscrField, Value, ?NOT_FOUND) of
|
||||
?NOT_FOUND ->
|
||||
State;
|
||||
SchemaName ->
|
||||
validate_child_schema(Value, SchemaName, State)
|
||||
end.
|
||||
|
||||
validate_child_schema(Value, SchemaName, State) ->
|
||||
Ref = <<"#/" ?DEFINITIONS "/", SchemaName/binary>>,
|
||||
BadRef = {{packageName}}_utils:to_list(Ref),
|
||||
Schema = make_ref_schema(Ref),
|
||||
try jesse_schema_validator:validate_with_state(Schema, Value, State)
|
||||
catch
|
||||
throw:[{schema_invalid, _Schema, {schema_not_found, BadRef}}] ->
|
||||
jesse_error:handle_data_invalid(?DISCR_ERROR(SchemaName), Value, State)
|
||||
end.
|
||||
|
||||
check_all_of(Value, [_ | _] = Schemas, State) ->
|
||||
check_all_of_(Value, Schemas, State);
|
||||
check_all_of(_Value, _InvalidSchemas, State) ->
|
||||
jesse_error:handle_schema_invalid(wrong_all_of_schema_array, State).
|
||||
|
||||
check_all_of_(_Value, [], State) ->
|
||||
State;
|
||||
check_all_of_(Value, [Schema | Schemas], State) ->
|
||||
check_all_of_(Value, Schemas, validate_schema(Value, Schema, State)).
|
||||
|
||||
validate_schema(Value, Schema, State0) ->
|
||||
case jesse_lib:is_json_object(Schema) of
|
||||
true ->
|
||||
State1 = jesse_state:set_current_schema(State0, Schema),
|
||||
jesse_schema_validator:validate_with_state(Schema, Value, State1);
|
||||
false ->
|
||||
jesse_error:handle_schema_invalid(schema_invalid, State0)
|
||||
end.
|
||||
|
||||
validate_format(Value, {?FORMAT, Format}, State) when
|
||||
Format =:= <<"int32">> orelse
|
||||
Format =:= <<"int64">> orelse
|
||||
Format =:= <<"float">> orelse
|
||||
Format =:= <<"byte">> orelse
|
||||
Format =:= <<"binary">> orelse
|
||||
Format =:= <<"date">>
|
||||
->
|
||||
case {{packageName}}_param_validator:validate({type, erlang:binary_to_atom(Format, utf8)}, Value) of
|
||||
error ->
|
||||
jesse_error:handle_data_invalid(wrong_format, Value, State);
|
||||
_ ->
|
||||
State
|
||||
end;
|
||||
validate_format(Value, Format, State) ->
|
||||
jesse_validator_draft4:check_value(Value, Format, State).
|
||||
|
||||
validate_ref(Value, Attr = {?REF, Ref} , State) ->
|
||||
Path = jesse_state:get_current_path(State),
|
||||
State1 = add_ref_to_state(State, ref_tag(Ref, Path)),
|
||||
State2 = jesse_validator_draft4:check_value(Value, Attr, State1),
|
||||
remove_last_ref_from_state(State2).
|
||||
|
||||
add_ref_to_state(State, Ref) ->
|
||||
ValidatorState = jesse_state:get_validator_state(State),
|
||||
#{refs := Refs} = ValidatorState,
|
||||
jesse_state:set_validator_state(State, ValidatorState#{refs => [Ref | Refs]}).
|
||||
|
||||
remove_last_ref_from_state(State) ->
|
||||
ValidatorState = jesse_state:get_validator_state(State),
|
||||
#{refs := Refs} = ValidatorState,
|
||||
case Refs of
|
||||
[_ | Rest] ->
|
||||
jesse_state:set_validator_state(State, ValidatorState#{refs => Rest});
|
||||
[] ->
|
||||
State
|
||||
end.
|
||||
|
||||
is_recursive_ref(Ref, State) ->
|
||||
RefTag = ref_tag(Ref, jesse_state:get_current_path(State)),
|
||||
#{refs := Refs} = jesse_state:get_validator_state(State),
|
||||
lists:member(RefTag, Refs).
|
||||
|
||||
make_ref_schema(Ref) ->
|
||||
[{?REF, Ref}].
|
||||
|
||||
ref_tag(Ref, Path) ->
|
||||
{Ref, Path}.
|
||||
|
||||
validate_read_only(Value, true, State) ->
|
||||
#{msg_type := MsgType} = jesse_state:get_validator_state(State),
|
||||
case MsgType of
|
||||
request ->
|
||||
jesse_error:handle_data_invalid(?READ_ONLY_ERROR, Value, State);
|
||||
response ->
|
||||
State
|
||||
end.
|
||||
|
||||
options() ->
|
||||
[
|
||||
{validator, ?MODULE},
|
||||
{allowed_errors, 0},
|
||||
{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}
|
||||
].
|
||||
|
||||
map_error_reason({'data_invalid', _Schema, Error, Data, Path0}) ->
|
||||
Path = get_error_path(Path0),
|
||||
Description = get_error_description(Error, Data),
|
||||
{{packageName}}_utils:join(".", [Description, Path]).
|
||||
|
||||
get_error_path([]) ->
|
||||
<<"">>;
|
||||
get_error_path(Path0) ->
|
||||
Mapper = fun
|
||||
(N, Acc) when is_integer(N) ->
|
||||
["[", {{packageName}}_utils:to_binary(N), "]" | Acc];
|
||||
(X, Acc) ->
|
||||
[$., X | Acc]
|
||||
end,
|
||||
Path2 = case lists:foldr(Mapper, [], Path0) of
|
||||
[$.| Path1] -> Path1;
|
||||
Path1 -> Path1
|
||||
end,
|
||||
Path3 = {{packageName}}_utils:to_binary(Path2),
|
||||
<<" Path to item: ", Path3/binary>>.
|
||||
|
||||
get_error_description(any_schemas_not_valid, Value) ->
|
||||
Formatted = format_value(Value),
|
||||
<<"Value does not validate against any of supplied schemas. Value: ", Formatted/binary>>;
|
||||
get_error_description({missing_dependency, Dependency0}, _Value) ->
|
||||
Dependency = {{packageName}}_utils:to_binary(Dependency0),
|
||||
<<"Missing dependency: ", Dependency/binary>>;
|
||||
get_error_description(missing_required_property, Value) ->
|
||||
PropertyName = {{packageName}}_utils:to_binary(Value),
|
||||
<<"Missing required property: ", PropertyName/binary>>;
|
||||
get_error_description(no_extra_items_allowed, _Value) ->
|
||||
<<"Extra items not allowed">>;
|
||||
get_error_description(no_extra_properties_allowed, _Value) ->
|
||||
<<"Extra properties not allowed">>;
|
||||
get_error_description(no_match, _Value) ->
|
||||
<<"No match to pattern">>;
|
||||
get_error_description(not_found, _Value) ->
|
||||
<<"Not found">>;
|
||||
get_error_description(not_in_enum, _Value) ->
|
||||
<<"Not in enum">>;
|
||||
get_error_description(not_in_range, _Value) ->
|
||||
<<"Not in range">>;
|
||||
get_error_description(not_multiple_of, Value) ->
|
||||
Formatted = format_value(Value),
|
||||
<<"Schema rule \"MultipleOf\" violated. Value: ", Formatted/binary>>;
|
||||
get_error_description(not_one_schema_valid, Value) ->
|
||||
Formatted = format_value(Value),
|
||||
<<"Schema rule \"OneOf\" violated. Value: ", Formatted/binary>>;
|
||||
get_error_description(not_schema_valid, Value) ->
|
||||
Formatted = format_value(Value),
|
||||
<<"Schema rule \"Not\" violated. Value: ", Formatted/binary>>;
|
||||
get_error_description(too_few_properties, _Value) ->
|
||||
<<"Too few properties">>;
|
||||
get_error_description(too_many_properties, _Value) ->
|
||||
<<"Too many properties">>;
|
||||
get_error_description(wrong_length, _Value) ->
|
||||
<<"Wrong length">>;
|
||||
get_error_description(wrong_size, _Value) ->
|
||||
<<"Wrong size">>;
|
||||
get_error_description(wrong_type, _Value) ->
|
||||
<<"Wrong type">>;
|
||||
get_error_description(wrong_format, _Value) ->
|
||||
<<"Wrong format">>;
|
||||
get_error_description(?DISCR_ERROR(SchemaName), _Value) ->
|
||||
<<"Discriminator child schema ", SchemaName/binary, " doesn't exist">>;
|
||||
get_error_description(?READ_ONLY_ERROR, _Value) ->
|
||||
<<"Property that marked as \"readOnly\" must not be sent as part of the request">>.
|
||||
|
||||
format_value(Value) ->
|
||||
list_to_binary(io_lib:format("~p", [Value])).
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(PET_SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Pet\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"petType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"petType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"petType\"]
|
||||
},
|
||||
\"Cat\": {
|
||||
\"description\": \"A representation of a cat\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"huntingSkill\": {
|
||||
\"type\": \"string\",
|
||||
\"description\": \"The measured skill for hunting\",
|
||||
\"default\": \"lazy\",
|
||||
\"enum\": [\"clueless\", \"lazy\", \"adventurous\", \"aggressive\"]
|
||||
}
|
||||
},
|
||||
\"required\": [\"huntingSkill\"]
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Dog\": {
|
||||
\"description\": \"A representation of a dog\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"packSize\": {
|
||||
\"type\": \"integer\",
|
||||
\"format\": \"int32\",
|
||||
\"description\": \"the size of the pack the dog is from\",
|
||||
\"default\": 0,
|
||||
\"minimum\": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"required\": [\"packSize\"]
|
||||
},
|
||||
\"Pig\": {
|
||||
\"description\": \"A representation of a pig\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"weight\": {
|
||||
\"type\": \"integer\",
|
||||
\"description\": \"the weight of the pig\",
|
||||
\"readOnly\": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
-define(PET, 'Pet').
|
||||
|
||||
-define(PRED_SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Predicate\": {
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"type\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"type\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"type\"]
|
||||
},
|
||||
\"Constant\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Predicate\"},
|
||||
{
|
||||
\"properties\": {
|
||||
\"type\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"Constant\"]
|
||||
},
|
||||
\"value\": {
|
||||
\"type\": \"boolean\"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Conjunction\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Predicate\"},
|
||||
{
|
||||
\"properties\": {
|
||||
\"type\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"Conjunction\"]
|
||||
},
|
||||
\"operands\": {
|
||||
\"type\": \"array\",
|
||||
\"items\": {\"$ref\": \"#/components/schemas/Predicate\"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
-define(PRED, 'Predicate').
|
||||
|
||||
test_validate(Value, DefName, BinSchema) ->
|
||||
test_validate(Value, DefName, BinSchema, response).
|
||||
|
||||
test_validate(Value, DefName, BinSchema, MsgType) ->
|
||||
Schema = jsx:decode(BinSchema, [return_maps]),
|
||||
JsonValue = jsx:decode(Value, [return_maps]),
|
||||
case validate(JsonValue, DefName, MsgType, Schema) of
|
||||
ok -> ok;
|
||||
{error, Error} -> Error
|
||||
end.
|
||||
|
||||
expect(Error, Path) ->
|
||||
expect(Error, Path, undefined).
|
||||
|
||||
expect(Error, Path, Data) ->
|
||||
map_error_reason({data_invalid, undefined, Error, Data, Path}).
|
||||
|
||||
%% Test cases
|
||||
-spec test() -> _.
|
||||
|
||||
-spec ok_discr_simple_test() -> _.
|
||||
ok_discr_simple_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"adventurous\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec bad_1st_level_discr_simple_test() -> _.
|
||||
bad_1st_level_discr_simple_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"wrong\"
|
||||
}">>,
|
||||
?assertEqual(expect(not_in_enum, [<<"huntingSkill">>]), test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_discr_recursive_definition_test() -> _.
|
||||
ok_discr_recursive_definition_test() ->
|
||||
Predicate = <<"{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{\"type\": \"Constant\", \"value\": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Predicate, ?PRED, ?PRED_SCHEMA)).
|
||||
|
||||
-spec bad_3d_level_discr_recursive_definition_test() -> _.
|
||||
bad_3d_level_discr_recursive_definition_test() ->
|
||||
Predicate = <<"{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": \"wrong\"},
|
||||
{\"type\": \"Constant\", \"value\": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}">>,
|
||||
?assertEqual(
|
||||
expect(wrong_type, [<<"operands">>, 1, <<"operands">>, 0, <<"value">>]),
|
||||
test_validate(Predicate, ?PRED, ?PRED_SCHEMA)
|
||||
).
|
||||
|
||||
-spec exceed_int32_swagger_format_test() -> _.
|
||||
exceed_int32_swagger_format_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Rex\",
|
||||
\"petType\": \"Dog\",
|
||||
\"packSize\": 2147483650
|
||||
}">>,
|
||||
?assertEqual(expect(wrong_format, [<<"packSize">>]), test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_read_only_request_test() -> _.
|
||||
ok_read_only_request_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA, request)).
|
||||
|
||||
-spec error_read_only_request_test() -> _.
|
||||
error_read_only_request_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\",
|
||||
\"weight\": 0
|
||||
}">>,
|
||||
?assertEqual(expect(?READ_ONLY_ERROR, [<<"weight">>]), test_validate(Pet, ?PET, ?PET_SCHEMA, request)).
|
||||
|
||||
|
||||
-spec ok_read_only_response_test() -> _.
|
||||
ok_read_only_response_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\",
|
||||
\"weight\": 0
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
-endif.
|
42
modules/openapi-generator/src/main/resources/erlang-client/types.mustache
vendored
Normal file
42
modules/openapi-generator/src/main/resources/erlang-client/types.mustache
vendored
Normal file
@ -0,0 +1,42 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}).
|
||||
%% Type definitions
|
||||
|
||||
%% API
|
||||
|
||||
-export_type([request_context/0]).
|
||||
-export_type([auth_context/0]).
|
||||
-export_type([client_peer/0]).
|
||||
-export_type([operation_id/0]).
|
||||
-export_type([api_key/0]).
|
||||
-export_type([object/0]).
|
||||
-export_type([endpoint/0]).
|
||||
-export_type([transport_opts/0]).
|
||||
|
||||
-type auth_context() :: any().
|
||||
-type operation_id() :: atom().
|
||||
-type api_key() :: binary().
|
||||
-type object() :: map().
|
||||
-type endpoint() :: string() | {string(), pos_integer()}.
|
||||
|
||||
-type client_peer() :: #{
|
||||
ip_address => IP :: inet:ip_address(),
|
||||
port_number => Port :: inet:port_number()
|
||||
}.
|
||||
|
||||
-type request_context() :: #{
|
||||
auth_context => AuthContext :: auth_context(),
|
||||
peer => client_peer()
|
||||
}.
|
||||
|
||||
-type transport_opts() :: list(). % you can find it in hackney:request/5
|
||||
|
||||
%% Internal
|
||||
|
||||
-export_type([param_name/0]).
|
||||
-export_type([value/0]).
|
||||
-export_type([error_reason/0]).
|
||||
|
||||
-type param_name() :: atom().
|
||||
-type value() :: term().
|
||||
-type error_reason() :: binary().
|
@ -1,94 +1,238 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_utils).
|
||||
|
||||
-export([request/8,
|
||||
select_header_content_type/1,
|
||||
optional_params/2]).
|
||||
-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([binary_to_existing_atom/2]).
|
||||
-export([get_opt/2]).
|
||||
-export([get_opt/3]).
|
||||
-export([priv_dir/0]).
|
||||
-export([priv_dir/1]).
|
||||
-export([priv_path/1]).
|
||||
-export([get_url/2]).
|
||||
-export([fill_url/3]).
|
||||
-export([join/1]).
|
||||
-export([join/2]).
|
||||
|
||||
-type response_info() :: #{status := integer(),
|
||||
headers := list()}.
|
||||
-export_type([response_info/0]).
|
||||
|
||||
request(_Ctx, Method, Path, QS, Headers, Body, Opts, Cfg) ->
|
||||
{Headers1, QS1} = update_params_with_auth(Cfg, Headers, QS),
|
||||
Host = maps:get(host, Cfg, "localhost:8001"),
|
||||
Url = hackney_url:make_url(Host, Path, QS1),
|
||||
-spec to_binary(iodata() | atom() | number()) -> binary().
|
||||
|
||||
ConfigHackneyOpts = maps:get(hackney_opts, Cfg, []),
|
||||
Body1 = case lists:keyfind(<<"Content-Type">>, 1, Headers1) of
|
||||
{_, <<"application/json", _/binary>>} ->
|
||||
jsx:encode(Body);
|
||||
_ ->
|
||||
Body
|
||||
end,
|
||||
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);
|
||||
to_binary(_) -> erlang:error(badarg).
|
||||
|
||||
case hackney:request(Method, Url, Headers1, Body1, Opts++ConfigHackneyOpts) of
|
||||
{ok, ClientRef} ->
|
||||
%% return value if Opts includes `async`
|
||||
{ok, ClientRef};
|
||||
{ok, Status, RespHeaders, ClientRef} when Status >= 200,
|
||||
Status =< 299 ->
|
||||
{ok, ResponseBody} = hackney:body(ClientRef),
|
||||
Resp = decode_response(RespHeaders, ResponseBody),
|
||||
{ok, Resp, #{status => Status,
|
||||
headers => RespHeaders}};
|
||||
{ok, Status, RespHeaders, ClientRef} when Status >= 300 ->
|
||||
{ok, ResponseBody} = hackney:body(ClientRef),
|
||||
Resp = decode_response(RespHeaders, ResponseBody),
|
||||
{error, Resp, #{status => Status,
|
||||
headers => RespHeaders}}
|
||||
end.
|
||||
-spec to_list(iodata() | atom() | number()) -> string().
|
||||
|
||||
decode_response(Headers, Body) ->
|
||||
case lists:keyfind(<<"Content-Type">>, 1, Headers) of
|
||||
{_, <<"application/json", _/binary>>} ->
|
||||
jsx:decode(Body, [return_maps, {labels, atom}]);
|
||||
%% TODO: yml, protobuf, user defined function
|
||||
to_list(V) when is_list(V) -> V;
|
||||
to_list(V) -> binary_to_list(to_binary(V)).
|
||||
|
||||
-spec to_float(iodata() | number()) -> float().
|
||||
|
||||
to_float(V) when is_integer(V) -> float(V);
|
||||
to_float(V) when is_float(V) -> V;
|
||||
to_float(V) ->
|
||||
Data = iolist_to_binary([V]),
|
||||
case binary:split(Data, <<$.>>) of
|
||||
[Data] ->
|
||||
float(binary_to_integer(Data));
|
||||
[<<>>, _] ->
|
||||
binary_to_float(<<$0, Data/binary>>);
|
||||
_ ->
|
||||
Body
|
||||
binary_to_float(Data)
|
||||
end.
|
||||
|
||||
optional_params([], _Params) -> [];
|
||||
optional_params(Keys, Params) ->
|
||||
[{Key, maps:get(Key, Params)} || Key <- Keys, maps:is_key(Key, Params)].
|
||||
%%
|
||||
|
||||
select_header_content_type([]) ->
|
||||
[];
|
||||
select_header_content_type(ContentTypes) ->
|
||||
case lists:member(<<"application/json">>, ContentTypes) orelse lists:member(<<"*/*">>, ContentTypes) of
|
||||
-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);
|
||||
to_int(_) ->
|
||||
erlang:error(badarg).
|
||||
|
||||
-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 binary_to_existing_atom(binary(), latin1 | unicode | utf8) -> atom().
|
||||
binary_to_existing_atom(Bin, Encoding) when is_binary(Bin) ->
|
||||
try erlang:binary_to_existing_atom(Bin, Encoding)
|
||||
catch
|
||||
_:_ ->
|
||||
erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-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 ->
|
||||
erlang: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_case(_, _, _) ->
|
||||
erlang:error(badarg).
|
||||
|
||||
|
||||
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.
|
||||
|
||||
-spec join([iodata(), ...]) -> binary().
|
||||
|
||||
join(List) ->
|
||||
join($\s, List).
|
||||
|
||||
-spec join(char() | iodata(), [iodata(), ...]) -> binary().
|
||||
|
||||
join(Delim, List) ->
|
||||
iolist_to_binary(join_(Delim, List)).
|
||||
|
||||
join_(_, [H]) ->
|
||||
[H];
|
||||
|
||||
join_(Delim, [H | T]) ->
|
||||
[H, Delim | join_(Delim, T)].
|
||||
|
||||
-type url() :: string().
|
||||
|
||||
-spec get_url(Endpoint::{{packageName}}:endpoint(), Path::string()) -> url().
|
||||
get_url({Host, Port}, Path) ->
|
||||
Host ++ ":" ++ integer_to_list(Port) ++ Path;
|
||||
get_url(Url, Path) ->
|
||||
Url ++ Path.
|
||||
|
||||
-spec fill_url(Url::string(), Params::map(), Qs::map()) -> url().
|
||||
fill_url(Url, Params, Qs) ->
|
||||
Fun = fun(K, V, AccIn) ->
|
||||
re:replace(
|
||||
AccIn,
|
||||
<<"{", (to_binary(K))/binary, "}">>,
|
||||
to_binary(V),
|
||||
[{return, list}]
|
||||
)
|
||||
end,
|
||||
case maps:size(Qs) > 0 of
|
||||
% true ->
|
||||
true ->
|
||||
[{<<"Content-Type">>, <<"application/json">>}];
|
||||
false ->
|
||||
[{<<"Content-Type">>, hd(ContentTypes)}]
|
||||
Url1 = hackney_url:make_url(Url, [], maps:to_list(Qs)),
|
||||
maps:fold(Fun, Url1, Params);
|
||||
false -> maps:fold(Fun, Url, Params)
|
||||
end.
|
||||
|
||||
auth_with_prefix(Cfg, Key, Token) ->
|
||||
Prefixes = maps:get(api_key_prefix, Cfg, #{}),
|
||||
case maps:get(Key, Prefixes, undefined) of
|
||||
undefined ->
|
||||
Token;
|
||||
Prefix ->
|
||||
<<Prefix/binary, " ", Token/binary>>
|
||||
end.
|
||||
|
||||
update_params_with_auth(Cfg, Headers, QS) ->
|
||||
AuthSettings = maps:get(auth, Cfg, #{}),
|
||||
Auths = #{ {{#authMethods}}'{{name}}' =>
|
||||
#{type => '{{type}}',
|
||||
key => <<"{{#isApiKey}}{{keyParamName}}{{/isApiKey}}{{^isApiKey}}Authorization{{/isApiKey}}">>,
|
||||
in => {{^isApiKey}}header{{/isApiKey}}{{#isKeyInHeader}}header{{/isKeyInHeader}}{{#isKeyInQuery}}query{{/isKeyInQuery}}}{{#hasMore}}, {{/hasMore}}{{/authMethods}}},
|
||||
|
||||
maps:fold(fun(AuthName, #{type := _Type,
|
||||
in := In,
|
||||
key := Key}, {HeadersAcc, QSAcc}) ->
|
||||
case maps:get(AuthName, AuthSettings, undefined) of
|
||||
undefined ->
|
||||
{HeadersAcc, QSAcc};
|
||||
Value ->
|
||||
case In of
|
||||
header ->
|
||||
{[{Key, auth_with_prefix(Cfg, Key, Value)} | HeadersAcc], QSAcc};
|
||||
query ->
|
||||
{HeadersAcc, [{Key, auth_with_prefix(Cfg, Key, Value)} | QSAcc]}
|
||||
end
|
||||
end
|
||||
end, {Headers, QS}, Auths).
|
||||
|
173
modules/openapi-generator/src/main/resources/erlang-client/validation.mustache
vendored
Normal file
173
modules/openapi-generator/src/main/resources/erlang-client/validation.mustache
vendored
Normal file
@ -0,0 +1,173 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_validation).
|
||||
|
||||
-export([prepare_request_param/3]).
|
||||
-export([validate_response/2]).
|
||||
|
||||
-type rule() :: schema | {required, boolean()} | {{packageName}}_param_validator:param_rule().
|
||||
-type data_type() :: 'list' | atom().
|
||||
-type response_spec() :: {data_type(), {{packageName}}:param_name()} | undefined.
|
||||
|
||||
-type error() :: #{
|
||||
type := error_type(),
|
||||
description => {{packageName}}:error_reason()
|
||||
}.
|
||||
|
||||
-type error_type() ::
|
||||
no_match |
|
||||
not_found |
|
||||
not_in_range |
|
||||
wrong_length |
|
||||
wrong_size |
|
||||
schema_violated |
|
||||
wrong_type |
|
||||
wrong_array.
|
||||
|
||||
-export_type([rule/0]).
|
||||
-export_type([response_spec/0]).
|
||||
-export_type([error/0]).
|
||||
-export_type([error_type/0]).
|
||||
|
||||
-define(catch_error(Block),
|
||||
try
|
||||
{ok, Block}
|
||||
catch
|
||||
throw:{wrong_param, _Name, Error} ->
|
||||
{error, Error}
|
||||
end
|
||||
).
|
||||
|
||||
|
||||
%% API
|
||||
|
||||
-spec prepare_request_param(
|
||||
Rules :: [rule()],
|
||||
Name :: {{packageName}}:param_name(),
|
||||
Value :: {{packageName}}:value()
|
||||
) ->
|
||||
{ok, Value :: {{packageName}}:value()} |
|
||||
{error, Error :: error()}.
|
||||
|
||||
prepare_request_param(Rules, Name, Value) ->
|
||||
?catch_error(validate_param(Rules, Name, Value)).
|
||||
|
||||
-spec validate_response(
|
||||
Spec :: response_spec(),
|
||||
Resp :: {{packageName}}:object() | [{{packageName}}:object()] | undefined
|
||||
) ->
|
||||
ok |
|
||||
{error, Error :: error()}.
|
||||
|
||||
validate_response({DataType, SchemaName}, Body) ->
|
||||
Result = case DataType of
|
||||
'list' ->
|
||||
?catch_error([validate(schema, SchemaName, Item, response) || Item <- Body]);
|
||||
_ ->
|
||||
?catch_error(validate(schema, SchemaName, Body, response))
|
||||
end,
|
||||
case Result of
|
||||
E = {error, _} -> E;
|
||||
_ -> ok
|
||||
end;
|
||||
validate_response(undefined, undefined) ->
|
||||
ok;
|
||||
validate_response(undefined, _) ->
|
||||
{error, map_error(schema, <<"Must be empty">>)}.
|
||||
|
||||
|
||||
%% Internal
|
||||
|
||||
-spec validate_param(
|
||||
Rules :: [rule()],
|
||||
Name :: {{packageName}}:param_name(),
|
||||
Value :: {{packageName}}:value()
|
||||
) ->
|
||||
Prepared :: {{packageName}}:value() | no_return().
|
||||
|
||||
validate_param(Rules, Name, Value) ->
|
||||
lists:foldl(
|
||||
fun(Rule, Acc) ->
|
||||
case validate(Rule, Name, Acc, request) of
|
||||
ok -> Acc;
|
||||
{ok, Prepared} -> Prepared
|
||||
end
|
||||
end,
|
||||
Value,
|
||||
Rules
|
||||
).
|
||||
|
||||
validate(Rule = {required, true}, Name, undefined, _MsgType) ->
|
||||
report_validation_error(Rule, Name);
|
||||
validate({required, _}, _Name, _, _MsgType) ->
|
||||
ok;
|
||||
validate(_, _Name, undefined, _MsgType) ->
|
||||
ok;
|
||||
validate(Rule = schema, Name, Value, MsgType) ->
|
||||
case {{packageName}}_schema_validator:validate(Value, Name, MsgType) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
report_validation_error(Rule, Name, Reason)
|
||||
end;
|
||||
validate(Rule, Name, Value, _MsgType) ->
|
||||
case {{packageName}}_param_validator:validate(Rule, Value) of
|
||||
ok ->
|
||||
ok;
|
||||
Ok = {ok, _} ->
|
||||
Ok;
|
||||
error ->
|
||||
report_validation_error(Rule, Name)
|
||||
end.
|
||||
|
||||
-spec report_validation_error(Rule :: rule(), Param :: {{packageName}}:param_name()) ->
|
||||
no_return().
|
||||
|
||||
report_validation_error(Rule, Param) ->
|
||||
report_validation_error(Rule, Param, undefined).
|
||||
|
||||
-spec report_validation_error(
|
||||
Rule :: rule(),
|
||||
Param :: {{packageName}}:param_name(),
|
||||
Description :: {{packageName}}:error_reason() | undefined
|
||||
) ->
|
||||
no_return().
|
||||
|
||||
report_validation_error(Rule, Param, Description) ->
|
||||
throw({wrong_param, Param, map_error(Rule, Description)}).
|
||||
|
||||
-spec map_error(Rule :: rule(), Description :: {{packageName}}:error_reason() | undefined) ->
|
||||
Error :: error().
|
||||
map_error(Rule, Description) ->
|
||||
Error = #{type => map_violated_rule(Rule)},
|
||||
case Description of
|
||||
undefined -> Error;
|
||||
_ -> Error#{description => Description}
|
||||
end.
|
||||
|
||||
-spec map_violated_rule(Rule :: rule()) ->
|
||||
ErrorType :: error_type().
|
||||
|
||||
map_violated_rule({type, _Type}) -> wrong_type;
|
||||
map_violated_rule({enum, _}) -> not_in_range;
|
||||
map_violated_rule({max, _, _}) -> wrong_size;
|
||||
map_violated_rule({min, _, _}) -> wrong_size;
|
||||
map_violated_rule({max_length, _}) -> wrong_length;
|
||||
map_violated_rule({min_length, _}) -> wrong_length;
|
||||
map_violated_rule({pattern, _}) -> no_match;
|
||||
map_violated_rule(schema) -> schema_violated;
|
||||
map_violated_rule({required, _}) -> not_found;
|
||||
map_violated_rule({list, _, _}) -> wrong_array.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
-spec validate_required_test() -> _.
|
||||
|
||||
validate_required_test() ->
|
||||
?assertEqual(ok, validate({required, true}, 'Name', <<"test">>, request)),
|
||||
?assertEqual(ok, validate({required, false}, 'Name', <<"test">>, request)),
|
||||
?assertThrow({wrong_param, _, _}, validate({required, true}, 'Name', undefined, request)).
|
||||
|
||||
-endif.
|
@ -1,4 +1,4 @@
|
||||
# OpenAPI server library for Erlang
|
||||
# OpenAPI rest server library for Erlang
|
||||
|
||||
## Overview
|
||||
|
||||
@ -6,53 +6,14 @@ An Erlang server stub generated by [OpenAPI Generator](https://openapi-generator
|
||||
|
||||
Dependency: [Cowboy](https://github.com/ninenines/cowboy)
|
||||
|
||||
## Supported features
|
||||
|
||||
Currently only features available in OAS2 specification are supported
|
||||
|
||||
## Prerequisites
|
||||
|
||||
TODO
|
||||
|
||||
## Getting started
|
||||
Use erlang-server with erlang.mk
|
||||
|
||||
1, Create an application by using erlang.mk
|
||||
$ mkdir http_server
|
||||
$ cd http_server
|
||||
$ wget https://erlang.mk/erlang.mk
|
||||
$ make -f erlang.mk bootstrap bootstrap-rel
|
||||
$ make run
|
||||
|
||||
2, Modify the Makefile in the http_server directory to the following to introduce the dependency library:
|
||||
PROJECT = http_server
|
||||
PROJECT_DESCRIPTION = New project
|
||||
PROJECT_VERSION = 0.1.0
|
||||
|
||||
DEPS = cowboy jesse jsx
|
||||
dep_cowboy_commit = 2.5.0
|
||||
dep_jesse_commit = 1.5.2
|
||||
dep_jsx_commit = 2.9.0
|
||||
DEP_PLUGINS = cowboy jesse jsx
|
||||
|
||||
PACKAGES += rfc3339
|
||||
pkg_rfc3339_name = rfc3339
|
||||
pkg_rfc3339_description = an erlang/elixir rfc3339 lib
|
||||
pkg_rfc3339_homepage = https://github.com/talentdeficit/rfc3339
|
||||
pkg_rfc3339_fetch = git
|
||||
pkg_rfc3339_repo = https://github.com/talentdeficit/rfc3339
|
||||
pkg_rfc3339_commit = master
|
||||
|
||||
include erlang.mk
|
||||
|
||||
3, Generate erlang-server project using openapi-generator
|
||||
https://github.com/OpenAPITools/openapi-generator#2---getting-started
|
||||
|
||||
4, Copy erlang-server file to http_server project,Don't forget the 'priv' folder.
|
||||
|
||||
5, Start in the http_server project:
|
||||
1, Introduce the following line in the http_server_app:start(_Type, _Args) function
|
||||
openapi_server:start(http_server, #{ip=>{127,0,0,1}, port=>8080, net_opts=>[]})
|
||||
2, Compilation http_server project
|
||||
$ make
|
||||
3, Start erlang virtual machine
|
||||
$erl -pa ./deps/cowboy/ebin -pa ./deps/cowlib/ebin -pa ./deps/ranch/ebin -pa ./deps/jsx/ebin -pa ./deps/jesse/ebin -pa ./deps/rfc3339/ebin -pa ./ebin
|
||||
4, Start project
|
||||
application:ensure_all_started(http_server).
|
||||
|
||||
TODO
|
||||
|
@ -1,363 +0,0 @@
|
||||
-module({{packageName}}_api).
|
||||
|
||||
-export([request_params/1]).
|
||||
-export([request_param_info/2]).
|
||||
-export([populate_request/3]).
|
||||
-export([validate_response/4]).
|
||||
%% exported to silence openapi complains
|
||||
-export([get_value/3, validate_response_body/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}}{{#isFormParam}}body{{/isFormParam}},
|
||||
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),
|
||||
case get_value(Source, Name, Req0) of
|
||||
{error, Reason, Req} ->
|
||||
{error, Reason, Req};
|
||||
{Value, Req} ->
|
||||
case prepare_param(Rules, Name, Value, ValidatorState) of
|
||||
{ok, Result} -> {ok, Name, Result, Req};
|
||||
{error, Reason} ->
|
||||
{error, Reason, Req}
|
||||
end
|
||||
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("#/components/schemas/" ++ {{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}).
|
||||
|
||||
-spec get_value(body | qs_val | header | binding, Name :: any(), Req0 :: cowboy_req:req()) ->
|
||||
{Value :: any(), Req :: cowboy_req:req()} |
|
||||
{error, Reason :: any(), Req :: cowboy_req:req()}.
|
||||
get_value(body, _Name, Req0) ->
|
||||
{ok, Body, Req} = cowboy_req:read_body(Req0),
|
||||
case prepare_body(Body) of
|
||||
{error, Reason} ->
|
||||
{error, Reason, Req};
|
||||
Value ->
|
||||
{Value, Req}
|
||||
end;
|
||||
|
||||
get_value(qs_val, Name, Req) ->
|
||||
QS = cowboy_req:parse_qs(Req),
|
||||
Value = {{packageName}}_utils:get_opt({{packageName}}_utils:to_qs(Name), QS),
|
||||
{Value, Req};
|
||||
|
||||
get_value(header, Name, Req) ->
|
||||
Headers = cowboy_req:headers(Req),
|
||||
Value = maps:get({{packageName}}_utils:to_header(Name), Headers, undefined),
|
||||
{Value, Req};
|
||||
|
||||
get_value(binding, Name, Req) ->
|
||||
Value = cowboy_req:binding({{packageName}}_utils:to_binding(Name), Req),
|
||||
{Value, Req}.
|
||||
|
||||
prepare_body(Body) ->
|
||||
case Body of
|
||||
<<"">> -> <<"">>;
|
||||
_ ->
|
||||
try
|
||||
jsx:decode(Body, [return_maps])
|
||||
catch
|
||||
error:_ ->
|
||||
{error, {invalid_body, not_json, Body}}
|
||||
end
|
||||
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))).
|
1
modules/openapi-generator/src/main/resources/erlang-server/api.param_info.mustache
vendored
Normal file
1
modules/openapi-generator/src/main/resources/erlang-server/api.param_info.mustache
vendored
Normal file
@ -0,0 +1 @@
|
||||
{{^isContainer}}{{#dataFormat}}{format, '{{dataFormat}}'}, {{/dataFormat}}{{#isEnum}}{enum, [{{#allowableValues}}{{#values}}'{{.}}'{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}]}, {{/isEnum}}{{#maximum}}{max, {{maximum}}, {{#exclusiveMaximum}}exclusive{{/exclusiveMaximum}}{{^exclusiveMaximum}}inclusive{{/exclusiveMaximum}}}, {{/maximum}}{{#minimum}}{min, {{minimum}}, {{#exclusiveMinimum}}exclusive{{/exclusiveMinimum}}{{^exclusiveMinimum}}inclusive{{/exclusiveMinimum}}}, {{/minimum}}{{#maxLength}}{max_length, {{maxLength}}}, {{/maxLength}}{{#minLength}}{min_length, {{minLength}}}, {{/minLength}}{{#pattern}}{pattern, "{{pattern}}"}, {{/pattern}}{{/isContainer}}true
|
@ -9,7 +9,8 @@
|
||||
inets,
|
||||
jsx,
|
||||
jesse,
|
||||
cowboy
|
||||
cowboy,
|
||||
email_validator
|
||||
]},
|
||||
{env, [
|
||||
]},
|
||||
|
@ -1,55 +0,0 @@
|
||||
-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
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
{true, Context} ->
|
||||
{true, Context, Req};
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
false ->
|
||||
AuthHeader = <<"">>,
|
||||
{false, AuthHeader, Req}
|
||||
end
|
||||
end.
|
||||
|
||||
get_api_key(header, KeyParam, Req) ->
|
||||
Headers = cowboy_req:headers(Req),
|
||||
{
|
||||
maps:get(
|
||||
openapi_utils:to_header(KeyParam),
|
||||
Headers,
|
||||
undefined
|
||||
),
|
||||
Req
|
||||
};
|
||||
|
||||
get_api_key(qs_val, KeyParam, Req) ->
|
||||
QS = cowboy_req:parse_qs(Req),
|
||||
{ {{packageName}}_utils:get_opt(KeyParam, QS), Req}.
|
||||
|
||||
|
||||
|
187
modules/openapi-generator/src/main/resources/erlang-server/common_validator.mustache
vendored
Normal file
187
modules/openapi-generator/src/main/resources/erlang-server/common_validator.mustache
vendored
Normal file
@ -0,0 +1,187 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_common_validator).
|
||||
|
||||
-export([validate/2]).
|
||||
|
||||
-type param_rule() ::
|
||||
{type, 'binary'} |
|
||||
{type, 'integer'} |
|
||||
{type, 'boolean'} |
|
||||
{type, 'byte'} |
|
||||
{type, 'float'} |
|
||||
%{type, 'list'} | ?
|
||||
%{type, 'map'} | ?
|
||||
%{type, 'object'} | ?
|
||||
%{type, 'file'} | ?
|
||||
{format, 'int32'} |
|
||||
{format, 'int64'} |
|
||||
{format, 'date'} |
|
||||
{format, 'date-time'} |
|
||||
{format, 'email'} |
|
||||
{format, 'ip-address'}.
|
||||
|
||||
-type result() ::
|
||||
ok | {ok, Prepared :: {{packageName}}:value()} | error | {error, Message :: term()}.
|
||||
|
||||
-export_type([
|
||||
param_rule/0,
|
||||
result/0
|
||||
]).
|
||||
|
||||
-type value() :: {{packageName}}:value().
|
||||
|
||||
%% API
|
||||
|
||||
-spec validate(param_rule(), value()) ->
|
||||
result().
|
||||
|
||||
%% TYPES
|
||||
|
||||
validate({type, 'binary'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({type, 'integer'}, Value0) ->
|
||||
try
|
||||
Value = {{packageName}}_utils:to_int(Value0),
|
||||
{ok, Value}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'boolean'}, Value) when is_boolean(Value) ->
|
||||
{ok, Value};
|
||||
validate({type, 'boolean'}, Value) ->
|
||||
case {{packageName}}_utils:to_lower(Value) of
|
||||
<<"true">> -> {ok, true};
|
||||
<<"false">> -> {ok, false};
|
||||
_ -> error
|
||||
end;
|
||||
|
||||
validate({type, 'byte'}, Value) ->
|
||||
try
|
||||
validate_base64(Value)
|
||||
catch error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({type, 'float'}, Value) ->
|
||||
try
|
||||
{ok, {{packageName}}_utils:to_float(Value)}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
%% FORMATS
|
||||
|
||||
validate({format, 'int64'}, Value0) ->
|
||||
try
|
||||
Value = {{packageName}}_utils:to_int(Value0),
|
||||
ok = validate_between(Value, -9223372036854775808, 922337203685477580),
|
||||
{ok, Value}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({format, 'int32'}, Value0) ->
|
||||
try
|
||||
Value = {{packageName}}_utils:to_int(Value0),
|
||||
ok = validate_between(Value, -2147483648, 2147483647),
|
||||
{ok, Value}
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
validate({format, 'date'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true ->
|
||||
validate_date(Value);
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({format, 'date-time'}, Value) ->
|
||||
case is_binary(Value) of
|
||||
true ->
|
||||
validate_datetime(Value);
|
||||
false -> error
|
||||
end;
|
||||
|
||||
validate({format, 'email'}, Value) when is_binary(Value) ->
|
||||
case email_validator:validate(Value) of
|
||||
ok -> ok;
|
||||
{error, _} -> error
|
||||
end;
|
||||
validate({format, 'email'}, _Value) ->
|
||||
error;
|
||||
|
||||
validate({format, 'ip-address'}, Value0) when is_binary(Value0) ->
|
||||
Value = binary_to_list(Value0),
|
||||
case inet:parse_strict_address(Value) of
|
||||
{ok, _IPAddress} ->
|
||||
ok;
|
||||
{error, einval} ->
|
||||
error
|
||||
end;
|
||||
validate({format, 'ip-address'}, _Value) ->
|
||||
error;
|
||||
|
||||
validate({format, Unknown}, _Value) ->
|
||||
_ = error_logger:warning_msg("Attempted to validate unknown format ~p, ignored.", [Unknown]),
|
||||
ok.
|
||||
|
||||
%%
|
||||
|
||||
-spec validate_between(Value :: {{packageName}}:value(), Min :: integer(), Max :: integer()) ->
|
||||
ok | no_return().
|
||||
|
||||
validate_between(Value, Min, Max) when
|
||||
is_integer(Value),
|
||||
Value >= Min,
|
||||
Value =< Max ->
|
||||
ok;
|
||||
|
||||
validate_between(_, _, _) ->
|
||||
error(badarg).
|
||||
|
||||
%% Internal
|
||||
|
||||
-spec validate_base64(Value :: {{packageName}}:value()) ->
|
||||
ok | no_return().
|
||||
|
||||
validate_base64(Value) when is_binary(Value) ->
|
||||
try
|
||||
_ = base64:decode(Value),
|
||||
ok
|
||||
catch
|
||||
_:_ ->
|
||||
error(badarg)
|
||||
end;
|
||||
|
||||
validate_base64(_) ->
|
||||
error(badarg).
|
||||
|
||||
|
||||
-spec validate_date(Value :: binary()) ->
|
||||
ok | error.
|
||||
|
||||
validate_date(Value) when is_binary(Value) ->
|
||||
validate_datetime(<<Value/binary, "T00:00:00Z">>).
|
||||
|
||||
-spec validate_datetime(Value :: binary()) ->
|
||||
ok | error.
|
||||
|
||||
validate_datetime(Value) when is_binary(Value) ->
|
||||
Str = erlang:binary_to_list(Value),
|
||||
try
|
||||
_Seconds = calendar:rfc3339_to_system_time(Str),
|
||||
ok
|
||||
catch
|
||||
error:_ ->
|
||||
error
|
||||
end.
|
61
modules/openapi-generator/src/main/resources/erlang-server/custom_validator.mustache
vendored
Normal file
61
modules/openapi-generator/src/main/resources/erlang-server/custom_validator.mustache
vendored
Normal file
@ -0,0 +1,61 @@
|
||||
-module({{packageName}}_custom_validator).
|
||||
-export([validate_param/4]).
|
||||
-export([validate_schema/3]).
|
||||
|
||||
-type param_rule() :: {{packageName}}_param_validator:param_rule().
|
||||
-type schema_rule() :: {{packageName}}_schema_validator:schema_rule().
|
||||
-type value() :: {{packageName}}:value().
|
||||
|
||||
-type validation_opts() :: {{packageName}}_validation:validation_opts().
|
||||
-type param_context() :: {{packageName}}_param_validator:context().
|
||||
-type schema_context() :: {{packageName}}_schema_validator:context().
|
||||
|
||||
-type validate_param_result() ::
|
||||
ok | {ok, term()} | pass | error | {error, Error :: term()}.
|
||||
|
||||
-type validate_schema_result() ::
|
||||
jesse_state:state() | pass | no_return().
|
||||
|
||||
%% BEHAVIOUR
|
||||
|
||||
-callback validate_param(param_rule(), value(), param_context()) ->
|
||||
validate_param_result().
|
||||
|
||||
-callback validate_schema(schema_rule(), value(), schema_context(), jesse_state:state()) ->
|
||||
validate_schema_result().
|
||||
|
||||
%% API
|
||||
|
||||
-spec validate_param(param_rule(), value(), param_context(), validation_opts()) ->
|
||||
validate_param_result().
|
||||
validate_param(Rule, Value, Meta, ValidationOpts) ->
|
||||
case get_validatior(ValidationOpts) of
|
||||
undefined -> pass;
|
||||
Module -> Module:validate_param(Rule, Value, Meta)
|
||||
end.
|
||||
|
||||
-spec validate_schema(schema_rule(), value(), jesse_state:state()) ->
|
||||
validate_schema_result().
|
||||
validate_schema(Rule, Value, JesseState) ->
|
||||
Meta = get_schema_context(JesseState),
|
||||
ValidationOpts = get_schema_opts(JesseState),
|
||||
case get_validatior(ValidationOpts) of
|
||||
undefined -> pass;
|
||||
Module -> Module:validate_schema(Rule, Value, Meta, JesseState)
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
get_schema_context(JesseState) ->
|
||||
#{validation_meta := Meta} = jesse_state:get_validator_state(JesseState),
|
||||
CurrentPath = lists:reverse(jesse_state:get_current_path(JesseState)),
|
||||
maps:merge(
|
||||
#{current_path => CurrentPath},
|
||||
Meta
|
||||
).
|
||||
|
||||
get_schema_opts(JesseState) ->
|
||||
maps:with([custom_validator], jesse_state:get_validator_state(JesseState)).
|
||||
|
||||
get_validatior(ValidationOpts) ->
|
||||
maps:get(custom_validator, ValidationOpts, undefined).
|
@ -1,32 +0,0 @@
|
||||
-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 :: jsx:json_term()}.
|
||||
|
||||
handle_request(OperationID, Req, Context) ->
|
||||
error_logger:error_msg(
|
||||
"Got not implemented request to process: ~p~n",
|
||||
[{OperationID, Req, Context}]
|
||||
),
|
||||
{501, #{}, #{}}.
|
@ -1,3 +1,5 @@
|
||||
%% -*- mode: erlang -*-
|
||||
|
||||
%% basic handler
|
||||
-module({{classname}}).
|
||||
|
||||
@ -7,6 +9,7 @@
|
||||
-export([allow_missing_post/2]).
|
||||
-export([content_types_accepted/2]).
|
||||
-export([content_types_provided/2]).
|
||||
-export([charsets_provided/2]).
|
||||
-export([delete_resource/2]).
|
||||
-export([is_authorized/2]).
|
||||
-export([known_content_type/2]).
|
||||
@ -18,27 +21,31 @@
|
||||
-export([handle_request_json/2]).
|
||||
|
||||
-record(state, {
|
||||
operation_id :: {{packageName}}_api:operation_id(),
|
||||
logic_handler :: atom(),
|
||||
validator_state :: jesse_state:state(),
|
||||
context=#{} :: #{}
|
||||
operation_id :: {{packageName}}:operation_id(),
|
||||
logic_handler :: module(),
|
||||
swagger_handler_opts :: {{packageName}}_router:swagger_handler_opts(),
|
||||
context :: {{packageName}}:request_context()
|
||||
}).
|
||||
|
||||
-type state() :: state().
|
||||
-type state() :: state().
|
||||
-type content_type() :: {binary(), binary(), '*' | [{binary(), binary()}]}.
|
||||
-type processed_response() :: {stop, cowboy_req:req(), state()}.
|
||||
|
||||
%% Cowboy REST callbacks
|
||||
|
||||
-spec init(Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) ->
|
||||
{cowboy_rest, Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
init(Req, {Operations, LogicHandler, ValidatorState}) ->
|
||||
Method = cowboy_req:method(Req),
|
||||
OperationID = maps:get(Method, Operations, undefined),
|
||||
init(Req, {_Operations, LogicHandler, SwaggerHandlerOpts} = InitOpts) ->
|
||||
OperationID = {{packageName}}_utils:get_operation_id(Req, InitOpts),
|
||||
|
||||
error_logger:info_msg("Attempt to process operation: ~p", [OperationID]),
|
||||
|
||||
State = #state{
|
||||
operation_id = OperationID,
|
||||
operation_id = OperationID,
|
||||
logic_handler = LogicHandler,
|
||||
validator_state = ValidatorState
|
||||
swagger_handler_opts = SwaggerHandlerOpts,
|
||||
context = #{}
|
||||
},
|
||||
{cowboy_rest, Req, State}.
|
||||
|
||||
@ -60,69 +67,64 @@ allowed_methods(Req, State) ->
|
||||
-spec is_authorized(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: true | {false, AuthHeader :: iodata()},
|
||||
Req :: cowboy_req:req(),
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
{{#operations}}
|
||||
{{#operation}}
|
||||
{{#authMethods}}
|
||||
{{#operations}}{{#operation}}
|
||||
{{#hasAuthMethods}}
|
||||
is_authorized(
|
||||
Req0,
|
||||
State = #state{
|
||||
operation_id = '{{operationId}}' = OperationID,
|
||||
logic_handler = LogicHandler
|
||||
operation_id = '{{operationId}}' = OperationID,
|
||||
logic_handler = LogicHandler,
|
||||
context = Context
|
||||
}
|
||||
) ->
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
From = {{#isKeyInQuery}}qs_val{{/isKeyInQuery}}{{#isKeyInHeader}}header{{/isKeyInHeader}},
|
||||
Result = {{packageName}}_auth:authorize_api_key(
|
||||
Result = {{packageName}}_handler_api:authorize_api_key(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
From,
|
||||
"{{keyParamName}}",
|
||||
'{{keyParamName}}',
|
||||
Req0
|
||||
),
|
||||
case Result of
|
||||
{true, Context, Req} -> {true, Req, State#state{context = Context}};
|
||||
{false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State}
|
||||
{true, AuthContext, Req} ->
|
||||
NewContext = Context#{
|
||||
auth_context => AuthContext
|
||||
},
|
||||
{true, Req, State#state{context = NewContext}};
|
||||
{false, AuthHeader, Req} ->
|
||||
{{false, AuthHeader}, Req, State}
|
||||
end;
|
||||
{{/isApiKey}}
|
||||
{{#isOAuth}}
|
||||
From = header,
|
||||
Result = {{packageName}}_auth:authorize_api_key(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
From,
|
||||
"Authorization",
|
||||
Req0
|
||||
),
|
||||
case Result of
|
||||
{true, Context, Req} -> {true, Req, State#state{context = Context}};
|
||||
{false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State}
|
||||
end;
|
||||
{{/isOAuth}}
|
||||
{{/authMethods}}
|
||||
{{/operation}}
|
||||
{{/operations}}
|
||||
{{^authMethods}}
|
||||
is_authorized(Req, State) ->
|
||||
{true, Req, State}.
|
||||
{{/authMethods}}
|
||||
{{#authMethods}}
|
||||
{{/hasAuthMethods}}
|
||||
{{^hasAuthMethods}}
|
||||
is_authorized(
|
||||
Req,
|
||||
State = #state{
|
||||
operation_id = '{{operationId}}'
|
||||
}
|
||||
) ->
|
||||
{true, Req, State};
|
||||
{{/hasAuthMethods}}
|
||||
{{/operation}}{{/operations}}
|
||||
is_authorized(Req, State) ->
|
||||
{{false, <<"">>}, Req, State}.
|
||||
{{/authMethods}}
|
||||
|
||||
-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: [{binary(), AcceptResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
Value :: [{content_type(), AcceptResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
|
||||
content_types_accepted(Req, State) ->
|
||||
{[
|
||||
{<<"application/json">>, handle_request_json}
|
||||
{{<<"application">>, <<"json">>, [{<<"charset">>, <<"utf-8">>}]}, handle_request_json}
|
||||
], Req, State}.
|
||||
|
||||
-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) ->
|
||||
@ -143,21 +145,36 @@ valid_content_headers(Req, State) ->
|
||||
|
||||
-spec content_types_provided(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{
|
||||
Value :: [{binary(), ProvideResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
Value :: [{content_type(), ProvideResource :: atom()}],
|
||||
Req :: cowboy_req:req(),
|
||||
State :: state()
|
||||
}.
|
||||
|
||||
content_types_provided(Req, State) ->
|
||||
{[
|
||||
{<<"application/json">>, handle_request_json}
|
||||
{{<<"application">>, <<"json">>, '*'}, handle_request_json}
|
||||
], Req, State}.
|
||||
|
||||
-spec malformed_request(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: false, Req :: cowboy_req:req(), State :: state()}.
|
||||
-spec charsets_provided(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Charsets :: [binary()], Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
malformed_request(Req, State) ->
|
||||
{false, Req, State}.
|
||||
charsets_provided(Req, State) ->
|
||||
{[<<"utf-8">>], Req, State}.
|
||||
|
||||
-spec malformed_request(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: boolean(), Req :: cowboy_req:req(), State :: state()}.
|
||||
|
||||
malformed_request(Req, State = #state{context = Context}) ->
|
||||
PeerResult = {{packageName}}_handler_api:determine_peer(Req),
|
||||
case PeerResult of
|
||||
{ok, Peer} ->
|
||||
Context1 = Context#{peer => Peer},
|
||||
State1 = State#state{context = Context1},
|
||||
{false, Req, State1};
|
||||
{error, Reason} ->
|
||||
error_logger:error_msg("Unable to determine client peer: ~p", [Reason]),
|
||||
{true, Req, State}
|
||||
end.
|
||||
|
||||
-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) ->
|
||||
{Value :: false, Req :: cowboy_req:req(), State :: state()}.
|
||||
@ -184,60 +201,84 @@ 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()}.
|
||||
%% Handlers
|
||||
|
||||
-type processed_response() :: {stop, cowboy_req:req(), state()}.
|
||||
|
||||
-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) ->
|
||||
-spec handle_request_json(Req :: cowboy_req:req(), State :: state()) ->
|
||||
processed_response().
|
||||
|
||||
process_response(Response, Req0, State = #state{operation_id = OperationID}) ->
|
||||
case Response of
|
||||
{ok, {Code, Headers, Body}} ->
|
||||
Req = cowboy_req:reply(Code, Headers, Body, Req0),
|
||||
{stop, Req, State};
|
||||
{error, Message} ->
|
||||
error_logger:error_msg("Unable to process request for ~p: ~p", [OperationID, Message]),
|
||||
|
||||
Req = cowboy_req:reply(400, Req0),
|
||||
{stop, Req, State}
|
||||
end.
|
||||
|
||||
-spec handle_request_json(cowboy_req:req(), state()) -> {cowboy_req:resp_body(), cowboy_req:req(), state()}.
|
||||
|
||||
handle_request_json(
|
||||
Req0,
|
||||
State = #state{
|
||||
operation_id = OperationID,
|
||||
operation_id = OperationID,
|
||||
logic_handler = LogicHandler,
|
||||
validator_state = ValidatorState
|
||||
swagger_handler_opts = SwaggerHandlerOpts,
|
||||
context = Context
|
||||
}
|
||||
) ->
|
||||
case {{packageName}}_api:populate_request(OperationID, Req0, ValidatorState) of
|
||||
ValidationOpts = maps:get(validation_opts, SwaggerHandlerOpts, #{}),
|
||||
case populate_request(OperationID, Req0, ValidationOpts) of
|
||||
{ok, Populated, Req1} ->
|
||||
{Code, Headers, Body} = {{packageName}}_logic_handler:handle_request(
|
||||
LogicHandler,
|
||||
OperationID,
|
||||
Req1,
|
||||
maps:merge(State#state.context, Populated)
|
||||
),
|
||||
_ = {{packageName}}_api:validate_response(
|
||||
OperationID,
|
||||
Code,
|
||||
Body,
|
||||
ValidatorState
|
||||
),
|
||||
PreparedBody = jsx:encode(Body),
|
||||
Response = {ok, {Code, Headers, PreparedBody}},
|
||||
process_response(Response, Req1, State);
|
||||
{Status, Resp} = handle_request(LogicHandler, OperationID, Populated, Context),
|
||||
ok = validate_response(Status, Resp, OperationID, ValidationOpts),
|
||||
process_response(ok, encode_response(Resp), Req1, State);
|
||||
{error, Reason, Req1} ->
|
||||
process_response({error, Reason}, Req1, State)
|
||||
process_response(error, Reason, Req1, State)
|
||||
end.
|
||||
|
||||
validate_headers(_, Req) -> {true, Req}.
|
||||
|
||||
%% Internal
|
||||
|
||||
populate_request(OperationID, Req, ValidationOpts) ->
|
||||
Spec = get_request_spec(OperationID),
|
||||
{{packageName}}_handler_api:populate_request(OperationID, Spec, Req, ValidationOpts).
|
||||
|
||||
handle_request(LogicHandler, OperationID, Populated, Context) ->
|
||||
{{packageName}}_logic_handler:handle_request(LogicHandler, OperationID, Populated, Context).
|
||||
|
||||
validate_response(error, _, _, _) ->
|
||||
ok;
|
||||
validate_response(ok, {Code, _Headers, Body}, OperationID, ValidationOpts) ->
|
||||
Spec = get_response_spec(OperationID, Code),
|
||||
{{packageName}}_handler_api:validate_response(OperationID, Spec, Body, ValidationOpts).
|
||||
|
||||
encode_response(Resp) ->
|
||||
{{packageName}}_handler_api:encode_response(Resp).
|
||||
|
||||
process_response(Status, Result, Req0, State = #state{operation_id = OperationID}) ->
|
||||
Req = {{packageName}}_handler_api:process_response(Status, Result, Req0, OperationID),
|
||||
{stop, Req, State}.
|
||||
|
||||
validate_headers(_, Req) ->
|
||||
{true, Req}.
|
||||
|
||||
-spec get_request_spec(OperationID :: {{packageName}}:operation_id()) ->
|
||||
Spec :: {{packageName}}_handler_api:request_spec() | no_return().
|
||||
|
||||
{{#operations}}{{#operation}}
|
||||
{{!
|
||||
NOTICE: Datatype definition was taken out of api.param_info partial since `items` context makes it impossible
|
||||
to check if we are currently in a list container. This information is required to get the data type from the correct
|
||||
source (`datatype` field of class `CodegenProperty`, instead of `dataType` field of class `CodegenParameter`)
|
||||
}}
|
||||
get_request_spec('{{operationId}}') ->
|
||||
[
|
||||
{{#allParams}}{{^isBodyParam}}{'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}{'{{dataType}}'{{/isBodyParam}}, #{
|
||||
source => {{#isQueryParam}}qs_val{{/isQueryParam}}{{#isPathParam}}binding{{/isPathParam}}{{#isHeaderParam}}header{{/isHeaderParam}}{{#isBodyParam}}body{{/isBodyParam}},
|
||||
rules => [{{^isBodyParam}}{{#isListContainer}}{list, '{{collectionFormat}}', {{#items}}[{{#datatype}}{type, '{{datatype}}'}, {{/datatype}}{{>api.param_info}}]{{/items}}}, {{/isListContainer}}{{^isListContainer}}{{#dataType}}{type, '{{dataType}}'}, {{/dataType}}{{/isListContainer}}{{>api.param_info}}, {{/isBodyParam}}{{#isBodyParam}}schema, {{/isBodyParam}}{required, {{#required}}true{{/required}}{{^required}}false{{/required}}}]
|
||||
}}{{#hasMore}},
|
||||
{{/hasMore}}{{/allParams}}
|
||||
];
|
||||
{{/operation}}{{/operations}}
|
||||
get_request_spec(OperationID) ->
|
||||
error({invalid_operation_id, OperationID}).
|
||||
|
||||
-spec get_response_spec(OperationID :: {{packageName}}:operation_id(), Code :: cowboy:http_status()) ->
|
||||
Spec :: {{packageName}}_handler_api:response_spec() | no_return().
|
||||
|
||||
{{#operations}}{{#operation}}{{#responses}}
|
||||
get_response_spec('{{operationId}}', {{code}}) ->
|
||||
{{#dataType}}{'{{dataType}}', '{{baseType}}'};{{/dataType}}{{^dataType}}undefined;{{/dataType}}
|
||||
{{/responses}}{{/operation}}{{/operations}}
|
||||
get_response_spec(OperationID, Code) ->
|
||||
error({invalid_response_code, OperationID, Code}).
|
||||
|
231
modules/openapi-generator/src/main/resources/erlang-server/handler_api.mustache
vendored
Normal file
231
modules/openapi-generator/src/main/resources/erlang-server/handler_api.mustache
vendored
Normal file
@ -0,0 +1,231 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_handler_api).
|
||||
|
||||
-export([authorize_api_key/5]).
|
||||
-export([determine_peer/1]).
|
||||
-export([populate_request/4]).
|
||||
-export([validate_response/4]).
|
||||
-export([encode_response/1]).
|
||||
-export([process_response/4]).
|
||||
|
||||
%%
|
||||
-type param_source() ::
|
||||
qs_val |
|
||||
binding |
|
||||
header |
|
||||
body.
|
||||
|
||||
-type request_spec() :: [{
|
||||
{{packageName}}:param_name(),
|
||||
#{source := param_source(), rules := [{{packageName}}_validation:rule()]}
|
||||
}].
|
||||
|
||||
-type response_spec() :: {{packageName}}_validation:response_spec().
|
||||
|
||||
-export_type([param_source/0]).
|
||||
-export_type([request_spec/0]).
|
||||
-export_type([response_spec/0]).
|
||||
|
||||
-type response() :: {cowboy:http_status(), cowboy:http_headers(), binary() | undefined}.
|
||||
|
||||
%% API
|
||||
|
||||
-spec authorize_api_key(
|
||||
LogicHandler :: module(),
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
From :: header | qs_val,
|
||||
KeyParam :: iodata() | atom(),
|
||||
Req :: cowboy_req:req()
|
||||
)->
|
||||
{true, Context :: {{packageName}}:auth_context(), Req ::cowboy_req:req()} |
|
||||
{false, AuthHeader :: binary(), Req ::cowboy_req:req()}.
|
||||
|
||||
authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) ->
|
||||
{ok, ApiKey, Req} = get_value(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.
|
||||
|
||||
-spec determine_peer(Req :: cowboy_req:req()) ->
|
||||
{ok, Peer :: {{packageName}}:client_peer()} | {error, Reason :: malformed | einval}.
|
||||
|
||||
determine_peer(Req) ->
|
||||
Peer = cowboy_req:peer(Req),
|
||||
Value = cowboy_req:header(<<"x-forwarded-for">>, Req),
|
||||
determine_peer_from_header(Value, Peer).
|
||||
|
||||
-spec populate_request(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Spec :: request_spec(),
|
||||
Req :: cowboy_req:req(),
|
||||
ValidationOpts :: {{packageName}}_validation:validation_opts()
|
||||
) ->
|
||||
{ok, Populated :: {{packageName}}:object(), Req :: cowboy_req:req()} |
|
||||
{error, Message :: {{packageName}}:error_reason(), Req :: cowboy_req:req()}.
|
||||
|
||||
populate_request(OperationID, Spec, Req, ValidationOpts) ->
|
||||
populate_request(OperationID, Spec, Req, #{}, ValidationOpts).
|
||||
|
||||
-spec validate_response(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Spec :: response_spec(),
|
||||
RespBody :: {{packageName}}:object() | [{{packageName}}:object()] | undefined,
|
||||
ValidationOpts :: {{packageName}}_validation:validation_opts()
|
||||
) ->
|
||||
ok | no_return().
|
||||
|
||||
validate_response(OperationID, Spec, RespBody, ValidationOpts) ->
|
||||
case {{packageName}}_validation:validate_response(OperationID, Spec, RespBody, ValidationOpts) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Error} ->
|
||||
erlang:error({response_validation_failed, Error, RespBody})
|
||||
end.
|
||||
|
||||
-spec encode_response(Resp :: {{packageName}}:response()) ->
|
||||
Encoded :: response().
|
||||
|
||||
encode_response(Resp = {_, _, undefined}) ->
|
||||
Resp;
|
||||
encode_response({Code, Headers, Body}) ->
|
||||
{Code, Headers, jsx:encode(Body)}.
|
||||
|
||||
-spec process_response(
|
||||
Status :: ok | error,
|
||||
Result :: response() | {{packageName}}:error_reason(),
|
||||
Req :: cowboy_req:req(),
|
||||
OperationID :: {{packageName}}:operation_id()
|
||||
) ->
|
||||
Req :: cowboy_req:req().
|
||||
|
||||
process_response(ok, {Code, Headers, undefined}, Req, _) ->
|
||||
cowboy_req:reply(Code, Headers, Req);
|
||||
process_response(ok, {Code, Headers, Body}, Req, _) ->
|
||||
cowboy_req:reply(Code, Headers, Body, Req);
|
||||
process_response(error, Message, Req, OperationID) ->
|
||||
error_logger:info_msg(
|
||||
"Unable to process request for ~p: ~ts",
|
||||
[OperationID, Message]
|
||||
),
|
||||
cowboy_req:reply(400, #{}, Message, Req).
|
||||
|
||||
|
||||
%% Internal
|
||||
|
||||
populate_request(_OperationID, [], Req, Populated, _ValidationOpts) ->
|
||||
{ok, Populated, Req};
|
||||
populate_request(OperationID, [ParamSpec | T], Req0, Populated, ValidationOpts) ->
|
||||
case populate_request_param(OperationID, ParamSpec, Req0, ValidationOpts) of
|
||||
{ok, K, V, Req} ->
|
||||
populate_request(OperationID, T, Req, maps:put(K, V, Populated), ValidationOpts);
|
||||
Error ->
|
||||
Error
|
||||
end.
|
||||
|
||||
populate_request_param(OperationID, {Name, #{rules := Rules, source := Source}}, Req0, ValidationOpts) ->
|
||||
case get_value(Source, Name, Req0) of
|
||||
{ok, Value, Req} ->
|
||||
case {{packageName}}_validation:prepare_request_param(OperationID, Rules, Name, Value, ValidationOpts) of
|
||||
{ok, Result} ->
|
||||
{ok, Name, Result, Req};
|
||||
{error, Error} ->
|
||||
{error, error_message(Name, Error), Req}
|
||||
end;
|
||||
{error, Message, Req} ->
|
||||
{error, error_message(Name, wrong_body, Message), Req}
|
||||
end.
|
||||
|
||||
-spec error_message(Name :: {{packageName}}:param_name(), Error :: {{packageName}}_validation:error()) ->
|
||||
Message :: {{packageName}}:error_reason().
|
||||
error_message(Name, Error = #{type := Type}) ->
|
||||
error_message(Name, Type, maps:get(description, Error, <<>>)).
|
||||
|
||||
error_message(Name, ErrorType, Description) ->
|
||||
jsx:encode(#{
|
||||
<<"name">> => {{packageName}}_utils:to_binary(Name),
|
||||
<<"errorType">> => {{packageName}}_utils:to_binary(ErrorType),
|
||||
<<"description">> => {{packageName}}_utils:to_binary(Description)
|
||||
}).
|
||||
|
||||
determine_peer_from_header(undefined, {IP, Port}) ->
|
||||
% undefined, assuming no proxies were involved
|
||||
{ok, #{ip_address => IP, port_number => Port}};
|
||||
determine_peer_from_header(Value, _Peer) when is_binary(Value) ->
|
||||
ClientPeer = string:strip(binary_to_list(Value)),
|
||||
case string:tokens(ClientPeer, ", ") of
|
||||
[ClientIP | _Proxies] ->
|
||||
case inet:parse_strict_address(ClientIP) of
|
||||
{ok, IP} ->
|
||||
% ok
|
||||
{ok, #{ip_address => IP}};
|
||||
Error ->
|
||||
% unparseable ip address
|
||||
Error
|
||||
end;
|
||||
_ ->
|
||||
% empty or malformed value
|
||||
{error, malformed}
|
||||
end.
|
||||
|
||||
-spec get_value(
|
||||
Source :: param_source(),
|
||||
Name :: {{packageName}}:param_name(),
|
||||
Req :: cowboy_req:req()
|
||||
) ->
|
||||
{ok, Value :: {{packageName}}:value(), Req :: cowboy_req:req()} |
|
||||
{error, Message :: {{packageName}}:error_reason(), Req :: cowboy_req:req()}.
|
||||
|
||||
get_value(body, _Name, Req0) ->
|
||||
case {{packageName}}_utils:get_body(Req0) of
|
||||
{ok, Body, Req} ->
|
||||
case decode_body(Body) of
|
||||
{ok, Value} ->
|
||||
{ok, Value, Req};
|
||||
{error, Message} ->
|
||||
{error, Message, Req}
|
||||
end;
|
||||
{error, Message} ->
|
||||
{error, Message, Req0}
|
||||
end;
|
||||
get_value(qs_val, Name, Req) ->
|
||||
try cowboy_req:match_qs([{Name, [], undefined}], Req) of
|
||||
#{Name := Value} ->
|
||||
{ok, Value, Req}
|
||||
catch
|
||||
exit:{request_error, _, _} ->
|
||||
{error, <<"Invalid query">>, Req}
|
||||
end;
|
||||
get_value(header, Name, Req) ->
|
||||
Headers = cowboy_req:headers(Req),
|
||||
{ok, maps:get({{packageName}}_utils:to_header(Name), Headers, undefined), Req};
|
||||
get_value(binding, Name, Req) ->
|
||||
Bindings = cowboy_req:bindings(Req),
|
||||
{ok, maps:get({{packageName}}_utils:to_binding(Name), Bindings, undefined), Req}.
|
||||
|
||||
-spec decode_body(Body :: binary()) ->
|
||||
{ok, Decoded :: {{packageName}}:object() | undefined} |
|
||||
{error, Message :: {{packageName}}:error_reason()}.
|
||||
decode_body(<<>>) ->
|
||||
{ok, undefined};
|
||||
decode_body(Body) ->
|
||||
try
|
||||
{ok, jsx:decode(Body, [return_maps])}
|
||||
catch
|
||||
_:_ ->
|
||||
{error, <<"Invalid json">>}
|
||||
end.
|
@ -1,61 +1,56 @@
|
||||
-module({{packageName}}_logic_handler).
|
||||
|
||||
-export([handle_request/4]).
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
{{#-first}}
|
||||
-export([authorize_api_key/3]).
|
||||
{{/-first}}
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
{{^authMethods}}
|
||||
-export([authorize_api_key/3]).
|
||||
{{/authMethods}}
|
||||
-type context() :: #{binary() => any()}.
|
||||
-type handler_response() ::{
|
||||
Status :: cowboy:http_status(),
|
||||
Headers :: cowboy:http_headers(),
|
||||
Body :: #{}
|
||||
}.
|
||||
|
||||
-export_type([handler_response/0]).
|
||||
-type operation_id() :: {{packageName}}:operation_id().
|
||||
-type api_key() :: {{packageName}}:api_key().
|
||||
-type auth_context() :: {{packageName}}:auth_context().
|
||||
-type object() :: {{packageName}}:object().
|
||||
-type request_context() :: {{packageName}}:request_context().
|
||||
-type handler_opts(T) :: {{packageName}}:handler_opts(T).
|
||||
-type logic_handler(T) :: {{packageName}}:logic_handler(T).
|
||||
-type response() :: {{packageName}}:response().
|
||||
|
||||
%% Behaviour definition
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-callback authorize_api_key(
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
ApiKey :: binary()
|
||||
) ->
|
||||
Result :: boolean() | {boolean(), context()}.
|
||||
-export([authorize_api_key/3]).
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-callback authorize_api_key(operation_id(), api_key(), handler_opts(_)) ->
|
||||
boolean() | {boolean(), auth_context()}.
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
|
||||
-callback handle_request(OperationID :: {{packageName}}_api:operation_id(), cowboy_req:req(), Context :: context()) ->
|
||||
handler_response().
|
||||
-callback handle_request(operation_id(), object(), request_context(), handler_opts(_)) ->
|
||||
{ok | error, response()}.
|
||||
|
||||
-spec handle_request(
|
||||
Handler :: atom(),
|
||||
OperationID :: {{packageName}}_api:operation_id(),
|
||||
Request :: cowboy_req:req(),
|
||||
Context :: context()
|
||||
) ->
|
||||
handler_response().
|
||||
%% API
|
||||
|
||||
handle_request(Handler, OperationID, Req, Context) ->
|
||||
Handler:handle_request(OperationID, Req, Context).
|
||||
-spec handle_request(logic_handler(_), operation_id(), object(), request_context()) ->
|
||||
{ok | error, response()}.
|
||||
|
||||
handle_request(Handler, OperationID, Request, Context) ->
|
||||
{Module, Opts} = get_mod_opts(Handler),
|
||||
Module:handle_request(OperationID, Request, Context, Opts).
|
||||
|
||||
{{#authMethods}}
|
||||
{{#isApiKey}}
|
||||
-spec authorize_api_key(Handler :: atom(), OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) ->
|
||||
Result :: false | {true, context()}.
|
||||
-spec authorize_api_key(logic_handler(_), operation_id(), api_key()) ->
|
||||
false | {true, auth_context()}.
|
||||
authorize_api_key(Handler, OperationID, ApiKey) ->
|
||||
Handler:authorize_api_key(OperationID, ApiKey).
|
||||
{Module, Opts} = get_mod_opts(Handler),
|
||||
Module:authorize_api_key(OperationID, ApiKey, Opts).
|
||||
{{/isApiKey}}
|
||||
{{/authMethods}}
|
||||
{{^authMethods}}
|
||||
-spec authorize_api_key(Handler :: atom(), OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) ->
|
||||
Result :: false.
|
||||
authorize_api_key(_Handler, _OperationID, _ApiKey) ->
|
||||
false.
|
||||
{{/authMethods}}
|
||||
|
||||
%% Internal functions
|
||||
|
||||
get_mod_opts(ModOpts= {_, _}) ->
|
||||
ModOpts;
|
||||
get_mod_opts(Module) ->
|
||||
{Module, undefined}.
|
||||
|
316
modules/openapi-generator/src/main/resources/erlang-server/param_validator.mustache
vendored
Normal file
316
modules/openapi-generator/src/main/resources/erlang-server/param_validator.mustache
vendored
Normal file
@ -0,0 +1,316 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_param_validator).
|
||||
|
||||
-export([validate/6]).
|
||||
|
||||
-type param_base_rule() ::
|
||||
{{packageName}}_common_validator:param_rule() |
|
||||
{enum, [atom()]} |
|
||||
{max, Max :: number(), Type :: exclusive | inclusive} |
|
||||
{min, Min :: number(), Type :: exclusive | inclusive} |
|
||||
{max_length, MaxLength :: integer()} |
|
||||
{min_length, MaxLength :: integer()} |
|
||||
{pattern, Pattern :: string()} |
|
||||
boolean().
|
||||
-type collection_format() ::
|
||||
'csv' |
|
||||
'ssv' |
|
||||
'tsv' |
|
||||
'pipes'.
|
||||
-type param_rule() ::
|
||||
param_base_rule() |
|
||||
{'list', collection_format(), [param_base_rule()]}.
|
||||
|
||||
-type operation_id() :: {{packageName}}:operation_id().
|
||||
-type name() :: {{packageName}}:param_name().
|
||||
-type value() :: {{packageName}}:value().
|
||||
-type msg_type() :: {{packageName}}_validation:msg_type().
|
||||
|
||||
-type result() :: ok | {ok, Prepared :: value()} | error | {error, Error :: term()}.
|
||||
|
||||
-type validation_opts() :: {{packageName}}_validation:validation_opts().
|
||||
|
||||
-type context() :: #{
|
||||
name := name(),
|
||||
msg_type := msg_type(),
|
||||
operation_id := operation_id()
|
||||
}.
|
||||
|
||||
-export_type([param_rule/0]).
|
||||
-export_type([context/0]).
|
||||
|
||||
%% API
|
||||
|
||||
-spec validate(operation_id(), param_rule(), name(), value(), msg_type(), validation_opts()) ->
|
||||
result().
|
||||
|
||||
validate(OperationID, Rule, Name, Value, MsgType, ValidationOpts) ->
|
||||
Meta = make_context(OperationID, Name, MsgType),
|
||||
validate_with_context(Rule, Value, Meta, ValidationOpts).
|
||||
|
||||
validate_with_context(Rule, Value, Meta, ValidationOpts) ->
|
||||
case {{packageName}}_custom_validator:validate_param(Rule, Value, Meta, ValidationOpts) of
|
||||
pass -> do_validate(Rule, Value, Meta, ValidationOpts);
|
||||
Result -> Result
|
||||
end.
|
||||
|
||||
%%
|
||||
|
||||
-spec do_validate(param_rule(), value(), context(), validation_opts()) ->
|
||||
result().
|
||||
|
||||
do_validate(true, _Value, _Meta, _Opts) ->
|
||||
ok;
|
||||
|
||||
do_validate(false, _Value, _Meta, _Opts) ->
|
||||
error;
|
||||
|
||||
do_validate({'list', Format, Ruleset}, Value, Meta, Opts) ->
|
||||
try
|
||||
Values = parse_array(Format, Value),
|
||||
{ok, [validate_ruleset(Ruleset, V, Meta, Opts) || V <- Values]}
|
||||
catch
|
||||
_:_ ->
|
||||
error
|
||||
end;
|
||||
|
||||
do_validate({enum, Values}, Value, _Meta, _Opts) ->
|
||||
try
|
||||
FormattedValue = {{packageName}}_utils:binary_to_existing_atom(Value, utf8),
|
||||
case lists:member(FormattedValue, Values) of
|
||||
true -> {ok, FormattedValue};
|
||||
false -> error
|
||||
end
|
||||
catch
|
||||
error:badarg ->
|
||||
error
|
||||
end;
|
||||
|
||||
do_validate({max, Max, Type}, Value, _Meta, _Opts) ->
|
||||
Result = case Value of
|
||||
_ when Value < Max andalso Type =:= exclusive ->
|
||||
true;
|
||||
_ when Value =< Max andalso Type =:= inclusive ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
case Result of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
do_validate({min, Min, Type}, Value, _Meta, _Opts) ->
|
||||
Result = case Value of
|
||||
_ when Value > Min andalso Type =:= exclusive ->
|
||||
true;
|
||||
_ when Value >= Min andalso Type =:= inclusive ->
|
||||
true;
|
||||
_ ->
|
||||
false
|
||||
end,
|
||||
case Result of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
do_validate({max_length, MaxLength}, Value, _Meta, _Opts) ->
|
||||
case size(Value) =< MaxLength of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
do_validate({min_length, MinLength}, Value, _Meta, _Opts) ->
|
||||
case size(Value) >= MinLength of
|
||||
true -> ok;
|
||||
false -> error
|
||||
end;
|
||||
|
||||
do_validate({pattern, Pattern}, Value, _Meta, _Opts) ->
|
||||
{ok, MP} = re:compile(Pattern, [unicode, ucp]),
|
||||
case re:run(Value, MP) of
|
||||
{match, _} -> ok;
|
||||
_ -> error
|
||||
end;
|
||||
|
||||
% Common
|
||||
|
||||
do_validate({RuleName, _} = Rule, Value, _Meta, _Opts) when
|
||||
RuleName =:= type;
|
||||
RuleName =:= format
|
||||
->
|
||||
{{packageName}}_common_validator:validate(Rule, Value).
|
||||
|
||||
%% Internal
|
||||
|
||||
make_context(OperationID, Name, MsgType) ->
|
||||
#{
|
||||
operation_id => OperationID,
|
||||
name => Name,
|
||||
msg_type => MsgType
|
||||
}.
|
||||
|
||||
-spec validate_ruleset(
|
||||
Ruleset :: [param_base_rule()],
|
||||
Value :: {{packageName}}:value(),
|
||||
Meta :: context(),
|
||||
Opts :: validation_opts()
|
||||
) ->
|
||||
Value :: {{packageName}}:value().
|
||||
|
||||
validate_ruleset(Ruleset, Value, Meta, Opts) ->
|
||||
lists:foldl(
|
||||
fun(R, V0) ->
|
||||
case validate_with_context(R, Value, Meta, Opts) of
|
||||
{ok, V} -> V;
|
||||
ok -> V0;
|
||||
error -> throw(wrong_param)
|
||||
end
|
||||
end,
|
||||
Value,
|
||||
Ruleset
|
||||
).
|
||||
|
||||
-spec parse_array(
|
||||
Format :: collection_format(),
|
||||
Array :: binary()
|
||||
) ->
|
||||
Values :: [binary()].
|
||||
|
||||
parse_array(Format, Array) ->
|
||||
binary:split(Array, get_split_pattern(Format), [global]).
|
||||
|
||||
get_split_pattern('csv') ->
|
||||
<<",">>;
|
||||
get_split_pattern('ssv') ->
|
||||
<<" ">>;
|
||||
get_split_pattern('tsv') ->
|
||||
<<"\t">>;
|
||||
get_split_pattern('pipes') ->
|
||||
<<"|">>.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
-spec validate_integer_test() -> _.
|
||||
-spec validate_ip_address_test() -> _.
|
||||
-spec validate_int64_test() -> _.
|
||||
-spec validate_int32_test() -> _.
|
||||
-spec validate_float_test() -> _.
|
||||
-spec validate_binary_test() -> _.
|
||||
-spec validate_byte_test() -> _.
|
||||
-spec validate_boolean_test() -> _.
|
||||
-spec validate_date_test() -> _.
|
||||
-spec validate_datetime_test() -> _.
|
||||
-spec validate_email_test() -> _.
|
||||
-spec validate_enum_test() -> _.
|
||||
-spec validate_max_test() -> _.
|
||||
-spec validate_min_test() -> _.
|
||||
-spec validate_max_length_test() -> _.
|
||||
-spec validate_min_length_test() -> _.
|
||||
-spec validate_pattern_test() -> _.
|
||||
-spec validate_array_test() -> _.
|
||||
|
||||
validate_integer_test() ->
|
||||
?assertEqual({ok, 2}, test_validate({type, 'integer'}, 2)),
|
||||
?assertEqual({ok, 6}, test_validate({type, 'integer'}, <<"6">>)),
|
||||
?assertEqual(error, test_validate({type, 'integer'}, <<"nope">>)).
|
||||
|
||||
validate_int64_test() ->
|
||||
?assertEqual({ok, 2}, test_validate({format, 'int64'},2)),
|
||||
?assertEqual({ok, 6}, test_validate({format, 'int64'},<<"6">>)),
|
||||
?assertEqual(error, test_validate({format, 'int64'}, 922337203685477581)),
|
||||
?assertEqual(error, test_validate({format, 'int64'},-9223372036854775809)).
|
||||
|
||||
validate_int32_test() ->
|
||||
?assertEqual({ok, 6}, test_validate({format, 'int32'}, 6)),
|
||||
?assertEqual({ok, 21}, test_validate({format, 'int32'}, <<"21">>)),
|
||||
?assertEqual(error, test_validate({format, 'int32'}, -2147483649)),
|
||||
?assertEqual(error, test_validate({format, 'int32'},2147483648)).
|
||||
|
||||
validate_float_test() ->
|
||||
?assertEqual({ok, 1.9}, test_validate({type, 'float'}, <<"1.9">>)),
|
||||
?assertEqual({ok, 3.0}, test_validate({type, 'float'}, <<"3">>)),
|
||||
?assertEqual(error, test_validate({type, 'float'}, <<"c">>)).
|
||||
|
||||
validate_binary_test() ->
|
||||
?assertEqual(ok, test_validate({type, 'binary'}, <<"f">>)),
|
||||
?assertEqual(error, test_validate({type, 'binary'}, [])),
|
||||
?assertEqual(error, test_validate({type, 'binary'}, 3)).
|
||||
|
||||
validate_byte_test() ->
|
||||
?assertEqual(ok, test_validate({type, 'byte'}, <<"0YXRg9C5">>)),
|
||||
?assertEqual(error, test_validate({type, 'byte'}, <<"g">>)).
|
||||
|
||||
validate_boolean_test() ->
|
||||
?assertEqual({ok, true}, test_validate({type, 'boolean'}, <<"true">>)),
|
||||
?assertEqual({ok, false}, test_validate({type, 'boolean'}, <<"false">>)),
|
||||
?assertEqual({ok, false}, test_validate({type, 'boolean'}, false)),
|
||||
?assertEqual(error, test_validate({type, 'boolean'}, <<"nope">>)).
|
||||
|
||||
validate_date_test() ->
|
||||
?assertEqual(ok, test_validate({format, 'date'}, <<"2014-03-19">>)),
|
||||
?assertEqual(error, test_validate({format, 'date'}, <<"2014-19-03">>)),
|
||||
?assertEqual(error, test_validate({format, 'date'}, <<"2013">>)),
|
||||
?assertEqual(error, test_validate({format, 'date'}, <<"nope">>)),
|
||||
?assertEqual(error, test_validate({format, 'date'}, <<"2014-03-19 18:00:05-04:00">>)).
|
||||
|
||||
validate_datetime_test() ->
|
||||
?assertEqual(ok, test_validate({format, 'date-time'}, <<"2014-03-19T18:35:03-04:00">>)),
|
||||
?assertEqual(error, test_validate({format, 'date-time'}, <<"2014-11-19">>)),
|
||||
?assertEqual(error, test_validate({format, 'date-time'}, <<"nope">>)).
|
||||
|
||||
validate_email_test() ->
|
||||
?assertEqual(ok, test_validate({format, 'email'}, <<"me@example.com">>)),
|
||||
?assertEqual(error, test_validate({format, 'email'}, <<"m\\e@example.com">>)),
|
||||
?assertEqual(error, test_validate({format, 'email'}, <<"nope">>)).
|
||||
|
||||
validate_ip_address_test() ->
|
||||
?assertEqual(ok, test_validate({format, 'ip-address'}, <<"127.0.0.1">>)),
|
||||
?assertEqual(ok, test_validate({format, 'ip-address'}, <<"::1">>)),
|
||||
?assertEqual(error, test_validate({format, 'ip-address'}, <<"127.0.0.0.1">>)),
|
||||
?assertEqual(error, test_validate({format, 'ip-address'}, <<"nope">>)).
|
||||
|
||||
validate_enum_test() ->
|
||||
?assertEqual({ok, sad}, test_validate({enum, [i, am, sad]} , <<"sad">>)),
|
||||
?assertEqual(error, test_validate({enum, ['All work and no play', 'makes Jack a dull boy']}, <<"Artem">>)),
|
||||
?assertEqual(error, test_validate({enum, []}, <<"">>)).
|
||||
|
||||
validate_max_test() ->
|
||||
?assertEqual(ok, test_validate({max, 10, inclusive}, 10)),
|
||||
?assertEqual(error, test_validate({max, 10, exclusive}, 10)),
|
||||
?assertEqual(ok, test_validate({max, 32, inclusive}, 21)),
|
||||
?assertEqual(ok, test_validate({max, 32, exclusive}, 21)).
|
||||
|
||||
validate_min_test() ->
|
||||
?assertEqual(ok, test_validate({min, 33, inclusive}, 33)),
|
||||
?assertEqual(error, test_validate({min, 33, exclusive}, 33)),
|
||||
?assertEqual(ok, test_validate({min, 57, inclusive}, 60)),
|
||||
?assertEqual(ok, test_validate({min, 57, inclusive}, 60)).
|
||||
|
||||
validate_max_length_test() ->
|
||||
?assertEqual(ok, test_validate({max_length, 5}, <<"hello">>)),
|
||||
?assertEqual(ok, test_validate({max_length, 5}, <<"h">>)),
|
||||
?assertEqual(error, test_validate({max_length, 5}, <<"hello?">>)).
|
||||
|
||||
validate_min_length_test() ->
|
||||
?assertEqual(ok, test_validate({min_length, 5}, <<"hello">>)),
|
||||
?assertEqual(ok, test_validate({min_length, 5}, <<"hello?">>)),
|
||||
?assertEqual(error, test_validate({min_length, 5}, <<"h">>)).
|
||||
|
||||
validate_pattern_test() ->
|
||||
?assertEqual(ok, test_validate({pattern, <<"[abc]">>}, <<"adcv">>)),
|
||||
?assertEqual(error, test_validate({pattern, <<"[abc]">>}, <<"fgh0">>)),
|
||||
?assertEqual(ok, test_validate({pattern, <<"^[0-9]{2}\/[0-9]{2}$">>}, <<"22/22">>)),
|
||||
?assertEqual(error, test_validate({pattern, <<"^[0-9]{2}\/[0-9]{2}$">>}, <<"22/225">>)).
|
||||
|
||||
validate_array_test() ->
|
||||
?assertEqual({ok, [10,11,12]}, test_validate({list, 'csv', [{format, 'int32'}]}, <<"10,11,12">>)),
|
||||
?assertEqual(error, test_validate({list, 'csv', [{format, 'int32'}]}, <<"10,xyi,12">>)).
|
||||
|
||||
test_validate(Rule, Value) ->
|
||||
do_validate(Rule, Value, #{name => 'Test', msg_type => request}, #{}).
|
||||
|
||||
-endif.
|
@ -1,6 +1,24 @@
|
||||
{deps, [
|
||||
{cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.5.0"}}},
|
||||
{rfc3339, {git, "https://github.com/talentdeficit/rfc3339.git", {tag, "master"}}},
|
||||
{jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "2.9.0"}}},
|
||||
{jesse, {git, "https://github.com/for-GET/jesse.git", {tag, "1.5.2"}}}
|
||||
{erl_opts, [
|
||||
{parse_transform, ct_expand}
|
||||
]}.
|
||||
|
||||
{deps, [
|
||||
{jsx,
|
||||
{git, "https://github.com/talentdeficit/jsx.git", {tag, "2.9.0"}}
|
||||
},
|
||||
{parse_trans,
|
||||
{git, "https://github.com/uwiger/parse_trans.git",
|
||||
{ref, "76abb347c3c1d00fb0ccf9e4b43e22b3d2288484"}
|
||||
}
|
||||
},
|
||||
{jesse,
|
||||
{git, "https://github.com/rbkmoney/jesse.git",
|
||||
{ref, "600cc8318c685de60a1ceee055b3f69bc884800d"}
|
||||
}
|
||||
},
|
||||
{email_validator,
|
||||
{git, "https://github.com/rbkmoney/email_validator.git",
|
||||
{ref, "be90c6ebd34d29fa9390136469b99d8a68ad4996"}
|
||||
}
|
||||
}
|
||||
]}.
|
||||
|
@ -1,18 +1,35 @@
|
||||
-module({{packageName}}_router).
|
||||
|
||||
-export([get_paths/1]).
|
||||
-export([get_paths/2]).
|
||||
-export([get_operation/1]).
|
||||
-export([get_operations/0]).
|
||||
|
||||
-type operations() :: #{
|
||||
Method :: binary() => {{packageName}}_api:operation_id()
|
||||
Method :: binary() => OperationID :: {{packageName}}:operation_id()
|
||||
}.
|
||||
|
||||
-type init_opts() :: {
|
||||
Operations :: operations(),
|
||||
LogicHandler :: atom(),
|
||||
ValidatorState :: jesse_state:state()
|
||||
-type logic_handler(T) :: {{packageName}}:logic_handler(T).
|
||||
|
||||
-type swagger_handler_opts() :: #{
|
||||
validation_opts => {{packageName}}_validation:validation_opts()
|
||||
}.
|
||||
|
||||
-type init_opts() :: {
|
||||
Operations :: operations(),
|
||||
LogicHandler :: logic_handler(_),
|
||||
SwaggerHandlerOpts :: swagger_handler_opts()
|
||||
}.
|
||||
|
||||
-type operation_spec() :: #{
|
||||
path := '_' | iodata(),
|
||||
method := binary(),
|
||||
handler := module()
|
||||
}.
|
||||
|
||||
-export_type([swagger_handler_opts/0]).
|
||||
-export_type([init_opts/0]).
|
||||
-export_type([operation_spec/0]).
|
||||
|
||||
-spec get_paths(LogicHandler :: atom()) -> [{'_',[{
|
||||
Path :: string(),
|
||||
@ -21,7 +38,15 @@
|
||||
}]}].
|
||||
|
||||
get_paths(LogicHandler) ->
|
||||
ValidatorState = prepare_validator(),
|
||||
get_paths(LogicHandler, #{}).
|
||||
|
||||
-spec get_paths(LogicHandler :: atom(), SwaggerHandlerOpts :: swagger_handler_opts()) -> [{'_',[{
|
||||
Path :: string(),
|
||||
Handler :: atom(),
|
||||
InitOpts :: init_opts()
|
||||
}]}].
|
||||
|
||||
get_paths(LogicHandler, SwaggerHandlerOpts) ->
|
||||
PreparedPaths = maps:fold(
|
||||
fun(Path, #{operations := Operations, handler := Handler}, Acc) ->
|
||||
[{Path, Handler, Operations} | Acc]
|
||||
@ -31,7 +56,7 @@ get_paths(LogicHandler) ->
|
||||
),
|
||||
[
|
||||
{'_',
|
||||
[{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths]
|
||||
[{P, H, {O, LogicHandler, SwaggerHandlerOpts}} || {P, H, O} <- PreparedPaths]
|
||||
}
|
||||
].
|
||||
|
||||
@ -53,6 +78,16 @@ group_paths() ->
|
||||
get_operations()
|
||||
).
|
||||
|
||||
-spec get_operation({{packageName}}:operation_id()) ->
|
||||
operation_spec().
|
||||
|
||||
get_operation(OperationId) ->
|
||||
maps:get(OperationId, get_operations()).
|
||||
|
||||
-spec get_operations() -> #{
|
||||
{{packageName}}:operation_id() := operation_spec()
|
||||
}.
|
||||
|
||||
get_operations() ->
|
||||
#{ {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}
|
||||
'{{operationId}}' => #{
|
||||
@ -61,14 +96,3 @@ get_operations() ->
|
||||
handler => '{{classname}}'
|
||||
}{{#hasMore}},{{/hasMore}}{{/operation}}{{#hasMore}},{{/hasMore}}{{/operations}}{{/apis}}{{/apiInfo}}
|
||||
}.
|
||||
|
||||
prepare_validator() ->
|
||||
R = jsx:decode(element(2, file:read_file(get_openapi_path()))),
|
||||
jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]).
|
||||
|
||||
|
||||
get_openapi_path() ->
|
||||
{ok, AppName} = application:get_application(?MODULE),
|
||||
filename:join({{packageName}}_utils:priv_dir(AppName), "{{{openAPISpecName}}}.json").
|
||||
|
||||
|
||||
|
238
modules/openapi-generator/src/main/resources/erlang-server/schema.mustache
vendored
Normal file
238
modules/openapi-generator/src/main/resources/erlang-server/schema.mustache
vendored
Normal file
@ -0,0 +1,238 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_schema).
|
||||
|
||||
-export([get/0]).
|
||||
-export([get_raw/0]).
|
||||
-export([load_raw/0]).
|
||||
-export([enumerate_components/1]).
|
||||
|
||||
-define(COMPONENTS, <<"components">>).
|
||||
|
||||
-spec get() -> {{packageName}}:object().
|
||||
get() ->
|
||||
ct_expand:term(enumerate_components(maps:with([?COMPONENTS], load_raw()))).
|
||||
|
||||
-spec enumerate_components(Schema :: map()) ->
|
||||
Schema :: map() | no_return().
|
||||
enumerate_components(Schema = #{?COMPONENTS := Components}) ->
|
||||
%@NOTICE only parents within the same component type are supported
|
||||
Schema#{?COMPONENTS => maps:map(fun enumerate_discriminator_children/2, Components)};
|
||||
enumerate_components(Schema) ->
|
||||
Schema.
|
||||
|
||||
-spec enumerate_discriminator_children(ComponentType :: binary(), Schema :: map()) ->
|
||||
Schema :: map() | no_return().
|
||||
enumerate_discriminator_children(_ComponentType, Defs) ->
|
||||
try
|
||||
{Parents, _} = maps:fold(
|
||||
fun(Name, Schema, Acc) ->
|
||||
check_definition(Name, Schema, Acc)
|
||||
end,
|
||||
{#{}, #{}},
|
||||
Defs
|
||||
),
|
||||
maps:fold(
|
||||
fun(Parent, Children, Schema) ->
|
||||
correct_schema(Parent, Children, Schema)
|
||||
end,
|
||||
Defs,
|
||||
Parents
|
||||
)
|
||||
catch
|
||||
_:Error ->
|
||||
handle_error(Error)
|
||||
end.
|
||||
|
||||
-spec handle_error(_) ->
|
||||
no_return().
|
||||
handle_error(Error) ->
|
||||
erlang:error({schema_invalid, Error}).
|
||||
|
||||
check_definition(Name, Schema, Acc) ->
|
||||
Acc1 = check_discriminator(Name, Schema, Acc),
|
||||
check_backrefs(Name, Schema, Acc1).
|
||||
|
||||
check_discriminator(Name, Schema, {Parents, Candidates}) ->
|
||||
case maps:get(<<"discriminator">>, Schema, undefined) of
|
||||
undefined ->
|
||||
{Parents, Candidates};
|
||||
_ ->
|
||||
{
|
||||
Parents#{Name => maps:get(Name, Candidates, [])},
|
||||
maps:without([Name], Candidates)
|
||||
}
|
||||
end.
|
||||
|
||||
check_backrefs(Name, Schema, Acc) ->
|
||||
case maps:get(<<"allOf">>, Schema, undefined) of
|
||||
undefined ->
|
||||
Acc;
|
||||
AllOf ->
|
||||
lists:foldl(fun(E, A) -> check_allOf(E, Name, A) end, Acc, AllOf)
|
||||
end.
|
||||
|
||||
check_allOf(#{<<"$ref">> := RefPath}, Child, {Parents, Candidates}) ->
|
||||
Parent = get_parent_from_ref(RefPath),
|
||||
case maps:get(Parent, Parents, undefined) of
|
||||
undefined ->
|
||||
{Parents, update_candidates(Parent, Child, Candidates)};
|
||||
Children ->
|
||||
{Parents#{Parent => [Child | Children]}, Candidates}
|
||||
end;
|
||||
check_allOf(_, _, Acc) ->
|
||||
Acc.
|
||||
|
||||
get_parent_from_ref(RefPath) ->
|
||||
Split = binary:split(RefPath, [<<"/">>], [global]),
|
||||
lists:last(Split).
|
||||
|
||||
update_candidates(Parent, Child, Candidates) ->
|
||||
case maps:get(Parent, Candidates, undefined) of
|
||||
undefined ->
|
||||
Candidates#{Parent => [Child]};
|
||||
Children ->
|
||||
Candidates#{Parent => [Child | Children]}
|
||||
end.
|
||||
|
||||
correct_schema(Parent, Children, Schema) ->
|
||||
BasePath = [Parent],
|
||||
Discr = maps:get(<<"discriminator">>, get_sub_schema(BasePath, Schema)),
|
||||
PropertyName = maps:get(<<"propertyName">>, Discr),
|
||||
update_schema(Children, [<<"enum">>, PropertyName, <<"properties">> | BasePath], Schema).
|
||||
|
||||
update_schema(Value, [], _Schema) ->
|
||||
Value;
|
||||
update_schema(Value, [Key | Path], Schema) ->
|
||||
SubSchema0 = get_sub_schema(Path, Schema),
|
||||
SubSchema1 = update_sub_schema(Key, Value, SubSchema0),
|
||||
update_schema(SubSchema1, Path, Schema).
|
||||
|
||||
get_sub_schema(ReversedPath, Schema) ->
|
||||
lists:foldr(fun(K, S) -> maps:get(K, S) end, Schema, ReversedPath).
|
||||
|
||||
update_sub_schema(Key, Value, Schema) ->
|
||||
Schema#{Key => Value}.
|
||||
|
||||
-spec get_raw() -> map().
|
||||
get_raw() ->
|
||||
ct_expand:term(load_raw()).
|
||||
|
||||
-spec load_raw() -> map().
|
||||
load_raw() ->
|
||||
{ok, Data} = file:read_file(get_openapi_path()),
|
||||
jsx:decode(Data, [return_maps]).
|
||||
|
||||
get_openapi_path() ->
|
||||
filename:join(code:priv_dir({{packageName}}), "{{{openAPISpecName}}}.json").
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Pet\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"petType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"petType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"petType\"]
|
||||
},
|
||||
\"Cat\": {
|
||||
\"description\": \"A representation of a cat\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"huntingSkill\": {
|
||||
\"type\": \"string\",
|
||||
\"description\": \"The measured skill for hunting\",
|
||||
\"default\": \"lazy\",
|
||||
\"enum\": [\"clueless\", \"lazy\", \"adventurous\", \"aggressive\"]
|
||||
}
|
||||
},
|
||||
\"required\": [\"huntingSkill\"]
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Dog\": {
|
||||
\"description\": \"A representation of a dog\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"packSize\": {
|
||||
\"type\": \"integer\",
|
||||
\"format\": \"int32\",
|
||||
\"description\": \"the size of the pack the dog is from\",
|
||||
\"default\": 0,
|
||||
\"minimum\": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"required\": [\"packSize\"]
|
||||
},
|
||||
\"Person\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"personType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"sex\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"male\", \"female\"]
|
||||
},
|
||||
\"personType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"sex\", \"personType\"]
|
||||
},
|
||||
\"WildMix\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{\"$ref\": \"#/components/schemas/Person\"}
|
||||
],
|
||||
},
|
||||
\"Dummy\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"dummyType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"dummyType\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"name\", \"dummyType\"]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
|
||||
get_enum(Parent, Discr, ComponentType, Schema) ->
|
||||
lists:sort(get_sub_schema([<<"enum">>, Discr, <<"properties">>, Parent, ComponentType, ?COMPONENTS], Schema)).
|
||||
|
||||
-spec test() -> _.
|
||||
-spec enumerate_discriminator_children_test() -> _.
|
||||
enumerate_discriminator_children_test() ->
|
||||
Schema = jsx:decode(?SCHEMA, [return_maps]),
|
||||
FixedSchema = enumerate_components(Schema),
|
||||
?assertEqual(
|
||||
lists:sort([<<"Dog">>, <<"Cat">>, <<"WildMix">>]),
|
||||
get_enum(<<"Pet">>, <<"petType">>, <<"schemas">>, FixedSchema)
|
||||
),
|
||||
?assertEqual([<<"WildMix">>], get_enum(<<"Person">>, <<"personType">>, <<"schemas">>, FixedSchema)),
|
||||
?assertEqual([], get_enum(<<"Dummy">>, <<"dummyType">>, <<"schemas">>, FixedSchema)).
|
||||
|
||||
-spec get_test() -> _.
|
||||
get_test() ->
|
||||
?assertEqual(
|
||||
enumerate_components(maps:with([?COMPONENTS], get_raw())),
|
||||
?MODULE:get()
|
||||
).
|
||||
-endif.
|
679
modules/openapi-generator/src/main/resources/erlang-server/schema_validator.mustache
vendored
Normal file
679
modules/openapi-generator/src/main/resources/erlang-server/schema_validator.mustache
vendored
Normal file
@ -0,0 +1,679 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_schema_validator).
|
||||
|
||||
-behaviour(jesse_schema_validator).
|
||||
|
||||
%% API
|
||||
-export([validate/5]).
|
||||
|
||||
%% Behaviour callbacks
|
||||
-export([init_state/1]).
|
||||
-export([check_value/3]).
|
||||
|
||||
-define(TYPE, <<"type">>). %mild
|
||||
-define(PROPERTIES, <<"properties">>). %mild
|
||||
-define(PATTERNPROPERTIES, <<"patternProperties">>). %mild
|
||||
-define(ADDITIONALPROPERTIES, <<"additionalProperties">>). %mild
|
||||
-define(ITEMS, <<"items">>). %mild
|
||||
-define(ADDITIONALITEMS, <<"additionalItems">>). %mild
|
||||
-define(REQUIRED, <<"required">>). %mild
|
||||
-define(DEPENDENCIES, <<"dependencies">>). %mild
|
||||
-define(MINIMUM, <<"minimum">>). %strict
|
||||
-define(MAXIMUM, <<"maximum">>). %strict
|
||||
-define(EXCLUSIVEMINIMUM, <<"exclusiveMinimum">>). %strict
|
||||
-define(EXCLUSIVEMAXIMUM, <<"exclusiveMaximum">>). %strict
|
||||
-define(MINITEMS, <<"minItems">>). %mild
|
||||
-define(MAXITEMS, <<"maxItems">>). %mild
|
||||
-define(UNIQUEITEMS, <<"uniqueItems">>). %strict
|
||||
-define(PATTERN, <<"pattern">>). %strict
|
||||
-define(MINLENGTH, <<"minLength">>). %strict
|
||||
-define(MAXLENGTH, <<"maxLength">>). %strict
|
||||
-define(ENUM, <<"enum">>). %strict
|
||||
-define(ANYOF, <<"anyOf">>). %mild
|
||||
-define(ONEOF, <<"oneOf">>). %mild
|
||||
-define(NOT, <<"not">>). %mild
|
||||
-define(MULTIPLEOF, <<"multipleOf">>). %strict
|
||||
-define(MAXPROPERTIES, <<"maxProperties">>). %mild
|
||||
-define(MINPROPERTIES, <<"minProperties">>). %mild
|
||||
|
||||
-define(DISCRIMINATOR, <<"discriminator">>). %mild
|
||||
-define(ALLOF, <<"allOf">>). %mild
|
||||
-define(FORMAT, <<"format">>). %strict
|
||||
-define(REF, <<"$ref">>). %mild
|
||||
-define(DEFINITIONS, "components/schemas").
|
||||
-define(NOT_FOUND, not_found).
|
||||
-define(READ_ONLY, <<"readOnly">>). %mild
|
||||
|
||||
-define(DISCR_ERROR(Error), {discriminator_not_valid, Error}).
|
||||
-define(READ_ONLY_ERROR, read_only_property_in_request).
|
||||
|
||||
-type validation_keyword() :: binary().
|
||||
-type validation_keyword_list() :: list(validation_keyword()).
|
||||
-type validation_ruleset() :: none | mild | strict | validation_keyword_list().
|
||||
|
||||
-type msg_type() :: {{packageName}}_validation:msg_type().
|
||||
-type schema_validation_opts() :: #{msg_type() => validation_ruleset()}.
|
||||
-type validation_opts() :: {{packageName}}_validation:validation_opts().
|
||||
|
||||
-type schema_rule() :: {binary(), jesse:json_term()}.
|
||||
-type schema_path() :: [binary() | non_neg_integer()].
|
||||
|
||||
-export_type([schema_validation_opts/0]).
|
||||
-export_type([schema_rule/0]).
|
||||
-export_type([schema_path/0]).
|
||||
|
||||
-type context() ::
|
||||
#{
|
||||
operation_id := {{packageName}}:operation_id(),
|
||||
msg_type := msg_type(),
|
||||
definition_name := {{packageName}}:param_name(),
|
||||
current_path => schema_path()
|
||||
}.
|
||||
|
||||
-export_type([context/0]).
|
||||
|
||||
-type state() ::
|
||||
#{
|
||||
refs => [],
|
||||
validation_meta => context(),
|
||||
validation_keywords => validation_keyword_list(),
|
||||
custom_validator => module()
|
||||
}.
|
||||
|
||||
%%
|
||||
-spec init_state(Opts :: jesse_state:validator_opts()) -> state().
|
||||
|
||||
init_state(Opts) ->
|
||||
Opts#{refs => []}.
|
||||
|
||||
-spec validate(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Value :: {{packageName}}:value(),
|
||||
DefinitionName :: {{packageName}}:param_name(),
|
||||
MsgType :: msg_type(),
|
||||
ValidationOpts :: validation_opts()
|
||||
) ->
|
||||
ok | {error, Error :: {{packageName}}:error_reason()}.
|
||||
validate(OperationID, Value, DefinitionName, MsgType, ValidationOpts) ->
|
||||
validate(OperationID, Value, DefinitionName, MsgType, {{packageName}}_schema:get(), ValidationOpts).
|
||||
|
||||
validate(OperationID, Value, DefinitionName, MsgType, Schema, ValidationOpts) ->
|
||||
ValidationKeywords = get_validation_keywords(MsgType, ValidationOpts),
|
||||
CustomValidator = maps:get(custom_validator, ValidationOpts, undefined),
|
||||
Options = [
|
||||
{validator_opts, #{
|
||||
validation_meta => make_context(OperationID, DefinitionName, MsgType),
|
||||
validation_keywords => ValidationKeywords,
|
||||
custom_validator => CustomValidator
|
||||
}} | options()
|
||||
],
|
||||
RefPath = "#/components/schemas/" ++ swag_server_utils:to_list(DefinitionName),
|
||||
case jesse:validate_local_ref(RefPath, Schema, Value, Options) of
|
||||
{ok, _} ->
|
||||
ok;
|
||||
{error, [Error]} ->
|
||||
{error, map_error_reason(Error)}
|
||||
end.
|
||||
|
||||
make_context(OperationID, DefinitionName, MsgType) ->
|
||||
#{
|
||||
operation_id => OperationID,
|
||||
definition_name => DefinitionName,
|
||||
msg_type => MsgType
|
||||
}.
|
||||
|
||||
get_validation_keywords(MsgType, ValidationOpts) ->
|
||||
SchemaOpts = maps:get(schema, ValidationOpts, #{}),
|
||||
Ruleset = maps:get(MsgType, SchemaOpts, strict),
|
||||
expand_ruleset(Ruleset).
|
||||
|
||||
expand_ruleset(Keywords) when is_list(Keywords) ->
|
||||
Keywords;
|
||||
expand_ruleset(none) ->
|
||||
[];
|
||||
expand_ruleset(mild) ->
|
||||
[
|
||||
?TYPE, ?PROPERTIES, ?PATTERNPROPERTIES, ?ADDITIONALPROPERTIES, ?ITEMS,
|
||||
?ADDITIONALITEMS, ?REQUIRED, ?DEPENDENCIES, ?ALLOF, ?ANYOF, ?ONEOF, ?NOT,
|
||||
?REF, ?DISCRIMINATOR, ?READ_ONLY
|
||||
];
|
||||
expand_ruleset(strict) ->
|
||||
expand_ruleset(mild) ++ [
|
||||
?MINIMUM, ?MAXIMUM, ?EXCLUSIVEMINIMUM, ?EXCLUSIVEMAXIMUM, ?UNIQUEITEMS,
|
||||
?PATTERN, ?MINLENGTH, ?MAXLENGTH, ?MINITEMS, ?MAXITEMS, ?MAXPROPERTIES,
|
||||
?MINPROPERTIES, ?ENUM, ?FORMAT, ?MULTIPLEOF
|
||||
].
|
||||
|
||||
-spec check_value(
|
||||
Value :: any(),
|
||||
Attr :: {binary(), jesse:json_term()},
|
||||
State :: jesse_state:state()
|
||||
) ->
|
||||
State :: jesse_state:state() |
|
||||
no_return().
|
||||
|
||||
check_value(Value, Attr, State) ->
|
||||
try
|
||||
maybe_use_custom_validator(Value, Attr, State)
|
||||
catch
|
||||
throw:Errors ->
|
||||
case handle_check_value_errors(Errors, Attr, Value, State) of
|
||||
[] -> State;
|
||||
Unhandled -> throw(Unhandled)
|
||||
end
|
||||
end.
|
||||
|
||||
maybe_use_custom_validator(Value, Attr, State) ->
|
||||
case {{packageName}}_custom_validator:validate_schema(Attr, Value, State) of
|
||||
pass ->
|
||||
do_check_value(Value, Attr, State);
|
||||
State ->
|
||||
State
|
||||
end.
|
||||
|
||||
do_check_value(Value, {?DISCRIMINATOR, DiscrField}, State) ->
|
||||
case jesse_lib:is_json_object(Value) of
|
||||
true -> validate_discriminator(Value, DiscrField, State);
|
||||
false -> State
|
||||
end;
|
||||
do_check_value(Value, {?ALLOF, Schemas}, State) -> % Override AllOf check to preserve error information
|
||||
check_all_of(Value, Schemas, State);
|
||||
do_check_value(Value, Attr = {?REF, Ref}, State) ->
|
||||
case is_recursive_ref(Ref, State) of
|
||||
true -> State;
|
||||
false -> validate_ref(Value, Attr, State)
|
||||
end;
|
||||
do_check_value(Value, {?READ_ONLY, ReadOnly}, State) ->
|
||||
validate_read_only(Value, ReadOnly, State);
|
||||
do_check_value(Value, {?FORMAT, Type}, State) when
|
||||
Type =:= <<"float">> orelse
|
||||
Type =:= <<"byte">> orelse
|
||||
Type =:= <<"binary">>
|
||||
->
|
||||
case {{packageName}}_common_validator:validate({type, erlang:binary_to_atom(Type, utf8)}, Value) of
|
||||
error ->
|
||||
jesse_error:handle_data_invalid(wrong_format, Value, State);
|
||||
_ ->
|
||||
State
|
||||
end;
|
||||
do_check_value(Value, {?FORMAT, Format}, State) when
|
||||
Format =:= <<"int32">> orelse
|
||||
Format =:= <<"int64">> orelse
|
||||
Format =:= <<"date">> orelse
|
||||
Format =:= <<"date-time">> orelse
|
||||
Format =:= <<"ip-address">> orelse
|
||||
Format =:= <<"email">>
|
||||
->
|
||||
case {{packageName}}_common_validator:validate({format, erlang:binary_to_atom(Format, utf8)}, Value) of
|
||||
error ->
|
||||
jesse_error:handle_data_invalid(wrong_format, Value, State);
|
||||
_ ->
|
||||
State
|
||||
end;
|
||||
do_check_value(Value, Attr, State) ->
|
||||
jesse_validator_draft4:check_value(Value, Attr, State).
|
||||
|
||||
handle_check_value_errors(Errors, Attr, Value, State) ->
|
||||
#{validation_keywords := ValidationKeywords} = jesse_state:get_validator_state(State),
|
||||
lists:filter(
|
||||
fun(Error) ->
|
||||
is_error_unignorable(Error, Attr, Value, ValidationKeywords)
|
||||
end,
|
||||
Errors
|
||||
).
|
||||
|
||||
is_error_unignorable(Error, Attr = {Keyword, _}, Value, ValidationKeywords) ->
|
||||
case is_keyword_validated(Keyword, ValidationKeywords) of
|
||||
true -> true;
|
||||
false -> is_error_reason_unsupported(Error, Value, Attr)
|
||||
end.
|
||||
|
||||
is_keyword_validated(Keyword, Keywords) ->
|
||||
lists:member(Keyword, Keywords).
|
||||
|
||||
-spec is_error_reason_unsupported(Error, Value, Attr) -> boolean() when
|
||||
Error :: jesse_error:error_reason(),
|
||||
Value :: any(),
|
||||
Attr :: {binary(), jesse:json_term()}.
|
||||
|
||||
is_error_reason_unsupported(Error, Value, Attr) when element(1, Error) =:= data_invalid ->
|
||||
_ = log_validation_warning(Error, Value, Attr),
|
||||
false;
|
||||
is_error_reason_unsupported(Error, _Value, _Attr) when element(1, Error) =:= schema_invalid ->
|
||||
true.
|
||||
|
||||
log_validation_warning(Error, Value, Attr) ->
|
||||
Reason = map_error_reason(Error),
|
||||
error_logger:warning_msg(
|
||||
"Swagger validation failed, but it was ignored: ~p. Expected: ~p. Got: ~p.",
|
||||
[Reason, Attr, Value]
|
||||
).
|
||||
|
||||
validate_discriminator(Value, #{<<"propertyName">> := DiscrField}, State) when is_binary(DiscrField) ->
|
||||
case jesse_json_path:value(DiscrField, Value, ?NOT_FOUND) of
|
||||
?NOT_FOUND ->
|
||||
State;
|
||||
SchemaName ->
|
||||
validate_child_schema(Value, SchemaName, State)
|
||||
end.
|
||||
|
||||
validate_child_schema(Value, SchemaName, State) ->
|
||||
Ref = <<"#/" ?DEFINITIONS "/", SchemaName/binary>>,
|
||||
BadRef = {{packageName}}_utils:to_list(Ref),
|
||||
Schema = make_ref_schema(Ref),
|
||||
try jesse_schema_validator:validate_with_state(Schema, Value, State)
|
||||
catch
|
||||
throw:[{schema_invalid, _Schema, {schema_not_found, BadRef}}] ->
|
||||
jesse_error:handle_data_invalid(?DISCR_ERROR(SchemaName), Value, State)
|
||||
end.
|
||||
|
||||
check_all_of(Value, [_ | _] = Schemas, State) ->
|
||||
check_all_of_(Value, Schemas, State);
|
||||
check_all_of(_Value, _InvalidSchemas, State) ->
|
||||
jesse_error:handle_schema_invalid(wrong_all_of_schema_array, State).
|
||||
|
||||
check_all_of_(_Value, [], State) ->
|
||||
State;
|
||||
check_all_of_(Value, [Schema | Schemas], State) ->
|
||||
check_all_of_(Value, Schemas, validate_schema(Value, Schema, State)).
|
||||
|
||||
validate_schema(Value, Schema, State0) ->
|
||||
case jesse_lib:is_json_object(Schema) of
|
||||
true ->
|
||||
State1 = jesse_state:set_current_schema(State0, Schema),
|
||||
jesse_schema_validator:validate_with_state(Schema, Value, State1);
|
||||
false ->
|
||||
jesse_error:handle_schema_invalid(schema_invalid, State0)
|
||||
end.
|
||||
|
||||
validate_ref(Value, Attr = {?REF, Ref} , State) ->
|
||||
Path = jesse_state:get_current_path(State),
|
||||
State1 = add_ref_to_state(State, ref_tag(Ref, Path)),
|
||||
State2 = jesse_validator_draft4:check_value(Value, Attr, State1),
|
||||
remove_last_ref_from_state(State2).
|
||||
|
||||
add_ref_to_state(State, Ref) ->
|
||||
ValidatorState = jesse_state:get_validator_state(State),
|
||||
#{refs := Refs} = ValidatorState,
|
||||
jesse_state:set_validator_state(State, ValidatorState#{refs => [Ref | Refs]}).
|
||||
|
||||
remove_last_ref_from_state(State) ->
|
||||
ValidatorState = jesse_state:get_validator_state(State),
|
||||
#{refs := Refs} = ValidatorState,
|
||||
case Refs of
|
||||
[_ | Rest] ->
|
||||
jesse_state:set_validator_state(State, ValidatorState#{refs => Rest});
|
||||
[] ->
|
||||
State
|
||||
end.
|
||||
|
||||
is_recursive_ref(Ref, State) ->
|
||||
RefTag = ref_tag(Ref, jesse_state:get_current_path(State)),
|
||||
#{refs := Refs} = jesse_state:get_validator_state(State),
|
||||
lists:member(RefTag, Refs).
|
||||
|
||||
make_ref_schema(Ref) ->
|
||||
[{?REF, Ref}].
|
||||
|
||||
ref_tag(Ref, Path) ->
|
||||
{Ref, Path}.
|
||||
|
||||
validate_read_only(Value, true, State) ->
|
||||
case get_message_type(State) of
|
||||
request ->
|
||||
jesse_error:handle_data_invalid(?READ_ONLY_ERROR, Value, State);
|
||||
response ->
|
||||
State
|
||||
end.
|
||||
|
||||
get_message_type(State) ->
|
||||
#{validation_meta := Meta} = jesse_state:get_validator_state(State),
|
||||
maps:get(msg_type, Meta).
|
||||
|
||||
options() ->
|
||||
[
|
||||
{validator, ?MODULE},
|
||||
{allowed_errors, 0},
|
||||
{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}
|
||||
].
|
||||
|
||||
map_error_reason({'data_invalid', _Schema, Error, Data, Path0}) ->
|
||||
Path = get_error_path(Path0),
|
||||
Description = get_error_description(Error, Data),
|
||||
{{packageName}}_utils:join(".", [Description, Path]).
|
||||
|
||||
get_error_path([]) ->
|
||||
<<"">>;
|
||||
get_error_path(Path0) ->
|
||||
Mapper = fun
|
||||
(N, Acc) when is_integer(N) ->
|
||||
["[", {{packageName}}_utils:to_binary(N), "]" | Acc];
|
||||
(X, Acc) ->
|
||||
[$., X | Acc]
|
||||
end,
|
||||
Path2 = case lists:foldr(Mapper, [], Path0) of
|
||||
[$.| Path1] -> Path1;
|
||||
Path1 -> Path1
|
||||
end,
|
||||
Path3 = {{packageName}}_utils:to_binary(Path2),
|
||||
<<" Path to item: ", Path3/binary>>.
|
||||
|
||||
get_error_description(all_schemas_not_valid, _Value) ->
|
||||
<<"Schema rule \"AllOf\" does not match any supplied schemas">>;
|
||||
get_error_description(any_schemas_not_valid, _Value) ->
|
||||
<<"Schema rule \"AnyOf\" does not match any supplied schemas">>;
|
||||
get_error_description({missing_dependency, Dependency0}, _Value) ->
|
||||
Dependency = {{packageName}}_utils:to_binary(Dependency0),
|
||||
<<"Missing dependency: ", Dependency/binary>>;
|
||||
get_error_description(missing_required_property, Value) ->
|
||||
PropertyName = {{packageName}}_utils:to_binary(Value),
|
||||
<<"Missing required property: ", PropertyName/binary>>;
|
||||
get_error_description(no_extra_items_allowed, _Value) ->
|
||||
<<"Extra items not allowed">>;
|
||||
get_error_description(no_extra_properties_allowed, _Value) ->
|
||||
<<"Extra properties not allowed">>;
|
||||
get_error_description(no_match, _Value) ->
|
||||
<<"No match to pattern">>;
|
||||
get_error_description(not_found, _Value) ->
|
||||
<<"Not found">>;
|
||||
get_error_description(not_in_enum, _Value) ->
|
||||
<<"Not in enum">>;
|
||||
get_error_description(not_in_range, _Value) ->
|
||||
<<"Not in range">>;
|
||||
get_error_description(not_multiple_of, _Value) ->
|
||||
<<"Schema rule \"MultipleOf\" violated">>;
|
||||
get_error_description(not_one_schema_valid, _Value) ->
|
||||
<<"Schema rule \"OneOf\" violated">>;
|
||||
get_error_description(not_schema_valid, _Value) ->
|
||||
<<"Schema rule \"Not\" violated">>;
|
||||
get_error_description(too_few_properties, _Value) ->
|
||||
<<"Too few properties">>;
|
||||
get_error_description(too_many_properties, _Value) ->
|
||||
<<"Too many properties">>;
|
||||
get_error_description(wrong_length, _Value) ->
|
||||
<<"Wrong length">>;
|
||||
get_error_description(wrong_size, _Value) ->
|
||||
<<"Wrong size">>;
|
||||
get_error_description(wrong_type, _Value) ->
|
||||
<<"Wrong type">>;
|
||||
get_error_description(wrong_format, _Value) ->
|
||||
<<"Wrong format">>;
|
||||
get_error_description(?DISCR_ERROR(SchemaName), _Value) ->
|
||||
<<"Discriminator child schema ", SchemaName/binary, " doesn't exist">>;
|
||||
get_error_description(?READ_ONLY_ERROR, _Value) ->
|
||||
<<"Property that marked as \"readOnly\" must not be sent as part of the request">>.
|
||||
|
||||
|
||||
-ifdef(TEST).
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-define(PET_SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Pet\": {
|
||||
\"type\": \"object\",
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"petType\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"name\": {\"type\": \"string\"},
|
||||
\"petType\": {\"type\": \"string\"},
|
||||
\"ownerEmail\": {\"type\": \"string\", \"format\": \"email\"}
|
||||
},
|
||||
\"required\": [\"name\", \"petType\"]
|
||||
},
|
||||
\"Cat\": {
|
||||
\"description\": \"A representation of a cat\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"huntingSkill\": {
|
||||
\"type\": \"string\",
|
||||
\"description\": \"The measured skill for hunting\",
|
||||
\"default\": \"lazy\",
|
||||
\"enum\": [\"clueless\", \"lazy\", \"adventurous\", \"aggressive\"]
|
||||
}
|
||||
},
|
||||
\"required\": [\"huntingSkill\"]
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Dog\": {
|
||||
\"description\": \"A representation of a dog\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"packSize\": {
|
||||
\"type\": \"integer\",
|
||||
\"format\": \"int32\",
|
||||
\"description\": \"the size of the pack the dog is from\",
|
||||
\"default\": 0,
|
||||
\"minimum\": 0
|
||||
}
|
||||
}
|
||||
}
|
||||
],
|
||||
\"required\": [\"packSize\"]
|
||||
},
|
||||
\"Pig\": {
|
||||
\"description\": \"A representation of a pig\",
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Pet\"},
|
||||
{
|
||||
\"type\": \"object\",
|
||||
\"properties\": {
|
||||
\"weight\": {
|
||||
\"type\": \"integer\",
|
||||
\"description\": \"the weight of the pig\",
|
||||
\"readOnly\": true
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
-define(PET, 'Pet').
|
||||
|
||||
-define(PRED_SCHEMA,
|
||||
<<"{\"components\": {
|
||||
\"schemas\": {
|
||||
\"Predicate\": {
|
||||
\"discriminator\": {
|
||||
\"propertyName\": \"type\"
|
||||
},
|
||||
\"properties\": {
|
||||
\"type\": {\"type\": \"string\"}
|
||||
},
|
||||
\"required\": [\"type\"]
|
||||
},
|
||||
\"Constant\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Predicate\"},
|
||||
{
|
||||
\"properties\": {
|
||||
\"type\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"Constant\"]
|
||||
},
|
||||
\"value\": {
|
||||
\"type\": \"boolean\"
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
\"Conjunction\": {
|
||||
\"allOf\": [
|
||||
{\"$ref\": \"#/components/schemas/Predicate\"},
|
||||
{
|
||||
\"properties\": {
|
||||
\"type\": {
|
||||
\"type\": \"string\",
|
||||
\"enum\": [\"Conjunction\"]
|
||||
},
|
||||
\"operands\": {
|
||||
\"type\": \"array\",
|
||||
\"items\": {\"$ref\": \"#/components/schemas/Predicate\"}
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}}">>).
|
||||
-define(PRED, 'Predicate').
|
||||
|
||||
test_validate(Value, DefinitionName, BinSchema) ->
|
||||
test_validate(Value, DefinitionName, BinSchema, response).
|
||||
|
||||
test_validate(Value, DefinitionName, BinSchema, MsgType) ->
|
||||
test_validate(Value, DefinitionName, BinSchema, MsgType, #{}).
|
||||
|
||||
test_validate(Value, DefinitionName, BinSchema, MsgType, Validation) ->
|
||||
Schema = jsx:decode(BinSchema, [return_maps]),
|
||||
JsonValue = jsx:decode(Value, [return_maps]),
|
||||
case validate(test, JsonValue, DefinitionName, MsgType, Schema, #{schema => Validation}) of
|
||||
ok -> ok;
|
||||
{error, Error} -> Error
|
||||
end.
|
||||
|
||||
expect(Error, Path) ->
|
||||
expect(Error, Path, undefined).
|
||||
|
||||
expect(Error, Path, Data) ->
|
||||
map_error_reason({data_invalid, undefined, Error, Data, Path}).
|
||||
|
||||
%% Test cases
|
||||
-spec test() -> _.
|
||||
|
||||
-spec ok_discr_simple_test() -> _.
|
||||
ok_discr_simple_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"adventurous\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_email_test() -> _.
|
||||
ok_email_test() ->
|
||||
Pet0 = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"adventurous\",
|
||||
\"ownerEmail\": \"me@example.com\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet0, ?PET, ?PET_SCHEMA)),
|
||||
Pet1 = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"adventurous\",
|
||||
\"ownerEmail\": \"not an email\"
|
||||
}">>,
|
||||
?assertEqual(expect(wrong_format, [<<"ownerEmail">>]), test_validate(Pet1, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec bad_1st_level_discr_simple_test() -> _.
|
||||
bad_1st_level_discr_simple_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Fluffy\",
|
||||
\"petType\": \"Cat\",
|
||||
\"huntingSkill\": \"wrong\"
|
||||
}">>,
|
||||
?assertEqual(expect(not_in_enum, [<<"huntingSkill">>]), test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_discr_recursive_definition_test() -> _.
|
||||
ok_discr_recursive_definition_test() ->
|
||||
Predicate = <<"{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{\"type\": \"Constant\", \"value\": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Predicate, ?PRED, ?PRED_SCHEMA)).
|
||||
|
||||
-spec bad_3d_level_discr_recursive_definition_test() -> _.
|
||||
bad_3d_level_discr_recursive_definition_test() ->
|
||||
Predicate = <<"{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": false},
|
||||
{
|
||||
\"type\": \"Conjunction\",
|
||||
\"operands\": [
|
||||
{\"type\": \"Constant\", \"value\": \"wrong\"},
|
||||
{\"type\": \"Constant\", \"value\": true}
|
||||
]
|
||||
}
|
||||
]
|
||||
}">>,
|
||||
?assertEqual(
|
||||
expect(wrong_type, [<<"operands">>, 1, <<"operands">>, 0, <<"value">>]),
|
||||
test_validate(Predicate, ?PRED, ?PRED_SCHEMA)
|
||||
).
|
||||
|
||||
-spec exceed_int32_swagger_format_test() -> _.
|
||||
exceed_int32_swagger_format_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Rex\",
|
||||
\"petType\": \"Dog\",
|
||||
\"packSize\": 2147483650
|
||||
}">>,
|
||||
?assertEqual(expect(wrong_format, [<<"packSize">>]), test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_read_only_request_test() -> _.
|
||||
ok_read_only_request_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA, request)).
|
||||
|
||||
-spec error_read_only_request_test() -> _.
|
||||
error_read_only_request_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\",
|
||||
\"weight\": 0
|
||||
}">>,
|
||||
?assertEqual(expect(?READ_ONLY_ERROR, [<<"weight">>]), test_validate(Pet, ?PET, ?PET_SCHEMA, request)).
|
||||
|
||||
|
||||
-spec ok_read_only_response_test() -> _.
|
||||
ok_read_only_response_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Babe\",
|
||||
\"petType\": \"Pig\",
|
||||
\"weight\": 0
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA)).
|
||||
|
||||
-spec ok_mild_validation_test() -> _.
|
||||
ok_mild_validation_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": \"Rex\",
|
||||
\"petType\": \"Dog\",
|
||||
\"packSize\": 2147483650
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA, request, #{request => mild})).
|
||||
|
||||
-spec ok_no_validation_test() -> _.
|
||||
ok_no_validation_test() ->
|
||||
Pet = <<"{
|
||||
\"name\": true,
|
||||
\"petType\": \"Cool\",
|
||||
\"meaningOfLife\": \"42\"
|
||||
}">>,
|
||||
?assertEqual(ok, test_validate(Pet, ?PET, ?PET_SCHEMA, request, #{request => none})).
|
||||
-endif.
|
@ -1,67 +0,0 @@
|
||||
-module({{packageName}}_server).
|
||||
|
||||
|
||||
-define(DEFAULT_LOGIC_HANDLER, {{packageName}}_default_logic_handler).
|
||||
|
||||
-export([start/2]).
|
||||
|
||||
-spec start( ID :: any(), #{
|
||||
ip => inet:ip_address(),
|
||||
port => inet:port_number(),
|
||||
logic_handler => module(),
|
||||
net_opts => []
|
||||
}) -> {ok, pid()} | {error, any()}.
|
||||
|
||||
start(ID, #{
|
||||
ip := IP ,
|
||||
port := Port,
|
||||
net_opts := NetOpts
|
||||
} = Params) ->
|
||||
{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),
|
||||
case Transport of
|
||||
ssl ->
|
||||
cowboy:start_tls(ID, TransportOpts, CowboyOpts);
|
||||
tcp ->
|
||||
cowboy:start_clear(ID, TransportOpts, CowboyOpts)
|
||||
end.
|
||||
|
||||
get_socket_transport(IP, Port, Options) ->
|
||||
Opts = [
|
||||
{ip, IP},
|
||||
{port, Port}
|
||||
],
|
||||
case {{packageName}}_utils:get_opt(ssl, Options) of
|
||||
SslOpts = [_|_] ->
|
||||
{ssl, Opts ++ SslOpts};
|
||||
undefined ->
|
||||
{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}).
|
49
modules/openapi-generator/src/main/resources/erlang-server/types.mustache
vendored
Normal file
49
modules/openapi-generator/src/main/resources/erlang-server/types.mustache
vendored
Normal file
@ -0,0 +1,49 @@
|
||||
-module({{packageName}}).
|
||||
%% Type definitions
|
||||
|
||||
%% API
|
||||
|
||||
-export_type([request_context/0]).
|
||||
-export_type([auth_context/0]).
|
||||
-export_type([client_peer/0]).
|
||||
-export_type([operation_id/0]).
|
||||
-export_type([api_key/0]).
|
||||
-export_type([object/0]).
|
||||
-export_type([logic_handler/1]).
|
||||
-export_type([handler_opts/1]).
|
||||
-export_type([status/0]).
|
||||
-export_type([headers/0]).
|
||||
-export_type([response_body/0]).
|
||||
-export_type([response/0]).
|
||||
|
||||
-type auth_context() :: any().
|
||||
-type operation_id() :: atom().
|
||||
-type api_key() :: binary().
|
||||
-type handler_opts(T) :: T | undefined.
|
||||
-type logic_handler(T) :: module() | {module(), handler_opts(T)}.
|
||||
-type object() :: map().
|
||||
-type status() :: cowboy:http_status().
|
||||
-type headers() :: cowboy:http_headers().
|
||||
-type response_body() :: object() | [object()] | undefined.
|
||||
-type response() :: {status(), headers(), response_body()}.
|
||||
|
||||
-type client_peer() :: #{
|
||||
ip_address => IP :: inet:ip_address(),
|
||||
port_number => Port :: inet:port_number()
|
||||
}.
|
||||
|
||||
-type request_context() :: #{
|
||||
auth_context => AuthContext :: auth_context(),
|
||||
peer => client_peer()
|
||||
}.
|
||||
|
||||
|
||||
%% Internal
|
||||
|
||||
-export_type([param_name/0]).
|
||||
-export_type([value/0]).
|
||||
-export_type([error_reason/0]).
|
||||
|
||||
-type param_name() :: atom().
|
||||
-type value() :: term().
|
||||
-type error_reason() :: binary().
|
@ -1,3 +1,4 @@
|
||||
%% -*- mode: erlang -*-
|
||||
-module({{packageName}}_utils).
|
||||
|
||||
-export([to_binary/1]).
|
||||
@ -10,12 +11,17 @@
|
||||
-export([to_header/1]).
|
||||
-export([to_qs/1]).
|
||||
-export([to_binding/1]).
|
||||
-export([binary_to_existing_atom/2]).
|
||||
-export([get_opt/2]).
|
||||
-export([get_opt/3]).
|
||||
-export([priv_dir/0]).
|
||||
-export([priv_dir/1]).
|
||||
-export([priv_path/1]).
|
||||
|
||||
-export([join/1]).
|
||||
-export([join/2]).
|
||||
-export([get_body/1]).
|
||||
-export([get_body/2]).
|
||||
-export([get_operation_id/2]).
|
||||
|
||||
-spec to_binary(iodata() | atom() | number()) -> binary().
|
||||
|
||||
@ -23,20 +29,23 @@ 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).
|
||||
to_binary(V) when is_float(V) -> float_to_binary(V);
|
||||
to_binary(_) -> erlang:error(badarg).
|
||||
|
||||
-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().
|
||||
-spec to_float(iodata() | number()) -> float().
|
||||
|
||||
to_float(V) when is_integer(V) -> float(V);
|
||||
to_float(V) when is_float(V) -> V;
|
||||
to_float(V) ->
|
||||
Data = iolist_to_binary([V]),
|
||||
case binary:split(Data, <<$.>>) of
|
||||
[Data] ->
|
||||
binary_to_integer(Data);
|
||||
float(binary_to_integer(Data));
|
||||
[<<>>, _] ->
|
||||
binary_to_float(<<$0, Data/binary>>);
|
||||
_ ->
|
||||
@ -52,7 +61,9 @@ to_int(Data) when is_integer(Data) ->
|
||||
to_int(Data) when is_binary(Data) ->
|
||||
binary_to_integer(Data);
|
||||
to_int(Data) when is_list(Data) ->
|
||||
list_to_integer(Data).
|
||||
list_to_integer(Data);
|
||||
to_int(_) ->
|
||||
erlang:error(badarg).
|
||||
|
||||
-spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req().
|
||||
|
||||
@ -79,18 +90,23 @@ to_binding(Name) ->
|
||||
Prepared = to_binary(Name),
|
||||
binary_to_atom(Prepared, utf8).
|
||||
|
||||
-spec get_opt(any(), []) -> any().
|
||||
-spec binary_to_existing_atom(binary(), latin1 | unicode | utf8) -> atom().
|
||||
binary_to_existing_atom(Bin, Encoding) when is_binary(Bin) ->
|
||||
try erlang:binary_to_existing_atom(Bin, Encoding)
|
||||
catch
|
||||
_:_ ->
|
||||
erlang:error(badarg)
|
||||
end.
|
||||
|
||||
-spec get_opt(any(), #{}) -> any().
|
||||
|
||||
get_opt(Key, Opts) ->
|
||||
get_opt(Key, Opts, undefined).
|
||||
|
||||
-spec get_opt(any(), [], any()) -> any().
|
||||
-spec get_opt(any(), #{}, any()) -> any().
|
||||
|
||||
get_opt(Key, Opts, Default) ->
|
||||
case lists:keyfind(Key, 1, Opts) of
|
||||
{_, Value} -> Value;
|
||||
false -> Default
|
||||
end.
|
||||
maps:get(Key, Opts, Default).
|
||||
|
||||
-spec priv_dir() -> file:filename().
|
||||
|
||||
@ -146,13 +162,17 @@ to_case(_Case, <<>>, Acc) ->
|
||||
Acc;
|
||||
|
||||
to_case(_Case, <<C, _/binary>>, _Acc) when C > 127 ->
|
||||
error(badarg);
|
||||
erlang: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_case(Case, Rest, <<Acc/binary, (to_upper_char(C))>>);
|
||||
|
||||
to_case(_, _, _) ->
|
||||
erlang:error(badarg).
|
||||
|
||||
|
||||
to_lower_char(C) when is_integer(C), $A =< C, C =< $Z ->
|
||||
C + 32;
|
||||
@ -171,3 +191,37 @@ to_upper_char(C) when is_integer(C), 16#F8 =< C, C =< 16#FE ->
|
||||
C - 32;
|
||||
to_upper_char(C) ->
|
||||
C.
|
||||
|
||||
-spec join([iodata(), ...]) -> binary().
|
||||
|
||||
join(List) ->
|
||||
join($\s, List).
|
||||
|
||||
-spec join(char() | iodata(), [iodata(), ...]) -> binary().
|
||||
|
||||
join(Delim, List) ->
|
||||
iolist_to_binary(join_(Delim, List)).
|
||||
|
||||
join_(_, [H]) ->
|
||||
[H];
|
||||
|
||||
join_(Delim, [H | T]) ->
|
||||
[H, Delim | join_(Delim, T)].
|
||||
|
||||
-spec get_body(Req) -> {ok, binary(), Req} | {error, {{packageName}}:error_reason()} when Req::cowboy_req:req().
|
||||
get_body(Req) ->
|
||||
get_body(Req, #{}).
|
||||
|
||||
-spec get_body(Req, cowboy_req:read_body_opts()) -> {ok, binary(), Req} | {error, {{packageName}}:error_reason()} when Req::cowboy_req:req().
|
||||
get_body(Req, Opts) ->
|
||||
case cowboy_req:read_body(Req, Opts) of
|
||||
{ok, Body, Req1} ->
|
||||
{ok, Body, Req1};
|
||||
{more, _, _} ->
|
||||
{error, <<"Body is too long">>}
|
||||
end.
|
||||
|
||||
-spec get_operation_id(cowboy_req:req(), {{packageName}}_router:init_opts()) -> {{packageName}}:operation_id() | undefined.
|
||||
get_operation_id(Req, {Operations, _LogicHandler, _SwaggerHandlerOpts}) ->
|
||||
Method = cowboy_req:method(Req),
|
||||
maps:get(Method, Operations, undefined).
|
||||
|
190
modules/openapi-generator/src/main/resources/erlang-server/validation.mustache
vendored
Normal file
190
modules/openapi-generator/src/main/resources/erlang-server/validation.mustache
vendored
Normal file
@ -0,0 +1,190 @@
|
||||
-module({{packageName}}_validation).
|
||||
|
||||
-export([prepare_request_param/5]).
|
||||
-export([validate_response/4]).
|
||||
|
||||
-type rule() :: schema | {required, boolean()} | {{packageName}}_param_validator:param_rule().
|
||||
-type data_type() :: 'list' | atom().
|
||||
-type response_spec() :: {data_type(), {{packageName}}:param_name()} | undefined.
|
||||
|
||||
-type error() :: #{
|
||||
type := error_type(),
|
||||
description => {{packageName}}:error_reason()
|
||||
}.
|
||||
|
||||
-type error_type() ::
|
||||
no_match |
|
||||
not_found |
|
||||
not_in_range |
|
||||
wrong_length |
|
||||
wrong_size |
|
||||
schema_violated |
|
||||
wrong_type |
|
||||
wrong_array.
|
||||
|
||||
-type validation_opts() :: #{
|
||||
schema => {{packageName}}_schema_validator:schema_validation_opts(),
|
||||
custom_validator => module()
|
||||
}.
|
||||
|
||||
-type msg_type() :: request | response.
|
||||
|
||||
-export_type([rule/0]).
|
||||
-export_type([response_spec/0]).
|
||||
-export_type([error/0]).
|
||||
-export_type([error_type/0]).
|
||||
-export_type([validation_opts/0]).
|
||||
-export_type([msg_type/0]).
|
||||
|
||||
-define(catch_error(Block),
|
||||
try
|
||||
{ok, Block}
|
||||
catch
|
||||
throw:{wrong_param, _Name, Error} ->
|
||||
{error, Error}
|
||||
end
|
||||
).
|
||||
|
||||
|
||||
%% API
|
||||
|
||||
-spec prepare_request_param(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Rules :: [rule()],
|
||||
Name :: {{packageName}}:param_name(),
|
||||
Value :: {{packageName}}:value(),
|
||||
ValidationOpts :: validation_opts()
|
||||
) ->
|
||||
{ok, Value :: {{packageName}}:value()} |
|
||||
{error, Error :: error()}.
|
||||
|
||||
prepare_request_param(OperationID, Rules, Name, Value, ValidationOpts) ->
|
||||
?catch_error(validate_param(OperationID, Rules, Name, Value, ValidationOpts)).
|
||||
|
||||
-spec validate_response(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Spec :: response_spec(),
|
||||
Resp :: {{packageName}}:object() | [{{packageName}}:object()] | undefined,
|
||||
ValidationOpts :: validation_opts()
|
||||
) ->
|
||||
ok |
|
||||
{error, Error :: error()}.
|
||||
|
||||
validate_response(OperationID, {DataType, SchemaName}, Body, ValidationOpts) ->
|
||||
Result = case DataType of
|
||||
'list' ->
|
||||
?catch_error([validate(OperationID, schema, SchemaName, Item, response, ValidationOpts) || Item <- Body]);
|
||||
_ ->
|
||||
?catch_error(validate(OperationID, schema, SchemaName, Body, response, ValidationOpts))
|
||||
end,
|
||||
case Result of
|
||||
E = {error, _} -> E;
|
||||
_ -> ok
|
||||
end;
|
||||
validate_response(_, undefined, undefined, _) ->
|
||||
ok;
|
||||
validate_response(_, undefined, _, _) ->
|
||||
{error, map_error(schema, <<"Must be empty">>)}.
|
||||
|
||||
|
||||
%% Internal
|
||||
|
||||
-spec validate_param(
|
||||
OperationID :: {{packageName}}:operation_id(),
|
||||
Rules :: [rule()],
|
||||
Name :: {{packageName}}:param_name(),
|
||||
Value :: {{packageName}}:value(),
|
||||
ValidationOpts :: validation_opts()
|
||||
) ->
|
||||
Prepared :: {{packageName}}:value() | no_return().
|
||||
|
||||
validate_param(OperationID, Rules, Name, Value, ValidationOpts) ->
|
||||
lists:foldl(
|
||||
fun(Rule, Acc) ->
|
||||
case validate(OperationID, Rule, Name, Acc, request, ValidationOpts) of
|
||||
ok -> Acc;
|
||||
{ok, Prepared} -> Prepared
|
||||
end
|
||||
end,
|
||||
Value,
|
||||
Rules
|
||||
).
|
||||
|
||||
validate(_OperationID, Rule = {required, true}, Name, undefined, _MsgType, _ValidationOpts) ->
|
||||
report_validation_error(Rule, Name);
|
||||
validate(_OperationID, {required, _}, _Name, _, _MsgType, _ValidationOpts) ->
|
||||
ok;
|
||||
validate(_OperationID, _, _Name, undefined, _MsgType, _ValidationOpts) ->
|
||||
ok;
|
||||
validate(OperationID, Rule = schema, Name, Value, MsgType, ValidationOpts) ->
|
||||
case {{packageName}}_schema_validator:validate(OperationID, Value, Name, MsgType, ValidationOpts) of
|
||||
ok ->
|
||||
ok;
|
||||
{error, Reason} ->
|
||||
report_validation_error(Rule, Name, Reason)
|
||||
end;
|
||||
validate(OperationID, Rule, Name, Value, MsgType, ValidationOpts) ->
|
||||
case {{packageName}}_param_validator:validate(OperationID, Rule, Name, Value, MsgType, ValidationOpts) of
|
||||
ok ->
|
||||
ok;
|
||||
Ok = {ok, _} ->
|
||||
Ok;
|
||||
error ->
|
||||
report_validation_error(Rule, Name);
|
||||
{error, Reason} ->
|
||||
report_validation_error(Rule, Name, Reason)
|
||||
end.
|
||||
|
||||
-spec report_validation_error(Rule :: rule(), Param :: {{packageName}}:param_name()) ->
|
||||
no_return().
|
||||
|
||||
report_validation_error(Rule, Param) ->
|
||||
report_validation_error(Rule, Param, undefined).
|
||||
|
||||
-spec report_validation_error(
|
||||
Rule :: rule(),
|
||||
Param :: {{packageName}}:param_name(),
|
||||
Description :: {{packageName}}:error_reason() | undefined
|
||||
) ->
|
||||
no_return().
|
||||
|
||||
report_validation_error(Rule, Param, Description) ->
|
||||
throw({wrong_param, Param, map_error(Rule, Description)}).
|
||||
|
||||
-spec map_error(Rule :: rule(), Description :: {{packageName}}:error_reason() | undefined) ->
|
||||
Error :: error().
|
||||
map_error(Rule, Description) ->
|
||||
Error = #{type => map_violated_rule(Rule)},
|
||||
case Description of
|
||||
undefined -> Error;
|
||||
_ -> Error#{description => Description}
|
||||
end.
|
||||
|
||||
-spec map_violated_rule(Rule :: rule()) ->
|
||||
ErrorType :: error_type().
|
||||
|
||||
map_violated_rule({type, _Type}) -> wrong_type;
|
||||
map_violated_rule({format, _Type}) -> wrong_format;
|
||||
map_violated_rule({enum, _}) -> not_in_range;
|
||||
map_violated_rule({max, _, _}) -> wrong_size;
|
||||
map_violated_rule({min, _, _}) -> wrong_size;
|
||||
map_violated_rule({max_length, _}) -> wrong_length;
|
||||
map_violated_rule({min_length, _}) -> wrong_length;
|
||||
map_violated_rule({pattern, _}) -> no_match;
|
||||
map_violated_rule(schema) -> schema_violated;
|
||||
map_violated_rule({required, _}) -> not_found;
|
||||
map_violated_rule({list, _, _}) -> wrong_array.
|
||||
|
||||
-ifdef(TEST).
|
||||
|
||||
-include_lib("eunit/include/eunit.hrl").
|
||||
|
||||
-spec test() -> _.
|
||||
-spec validate_required_test() -> _.
|
||||
|
||||
validate_required_test() ->
|
||||
?assertEqual(ok, validate(test, {required, true}, 'Name', <<"test">>, request, strict)),
|
||||
?assertEqual(ok, validate(test, {required, false}, 'Name', <<"test">>, request, strict)),
|
||||
?assertThrow({wrong_param, _, _}, validate(test, {required, true}, 'Name', undefined, request, strict)).
|
||||
|
||||
-endif.
|
Loading…
Reference in New Issue
Block a user