MSPF-555: Initial migration to OAS3 generator (#1)

This commit is contained in:
Alexey 2020-05-19 18:30:53 +03:00 committed by GitHub
parent 00ffcea6ef
commit 5b9e7db104
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
37 changed files with 4221 additions and 975 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View 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

View File

@ -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}}]}
]}.

View File

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

View File

@ -0,0 +1 @@
{{{openapi-json}}}

View 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.

View 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.

View File

@ -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"}
}
}
]}.

View 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.

View 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.

View 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().

View File

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

View 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.

View File

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

View File

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

View 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

View File

@ -9,7 +9,8 @@
inets,
jsx,
jesse,
cowboy
cowboy,
email_validator
]},
{env, [
]},

View File

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

View 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.

View 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).

View File

@ -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, #{}, #{}}.

View File

@ -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}).

View 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.

View File

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

View 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.

View File

@ -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"}
}
}
]}.

View File

@ -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").

View 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.

View 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.

View File

@ -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}).

View 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().

View File

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

View 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.