Erlang server overhaul (#19465)

* Upgrade erlang-server code generation and fix is_authorized crashes

* Introduce structured logging

* Improve general formatting

* Update generated files

* Enable erlang server on CI

* Add echo-server testing to CI

* Require OTP27 explicitly in the generated rebar.config file

* Rework handler and API

With this work, json validation becomes optional, fully implemented in
the `_api` module as it was before, but without being forcibly called by
the `_handler`. It is instead left as optional for the user to take
advantage of the exposed callbacks. Jesse also chooses draft-06 as a
default, but these can be chosen manually by the user too, as long as
jesse implements them.

`_handler` also becomes lighter, it now handles all mime types
transparently by forwarding to a user-given module that must implement
`accept_callback/4` and `provide_callback/4` as described in the
`_logic_handler` callbacks. These will simply be the return values of
cowboy_rest's `content_types_accepted` and `content_types_provided`
respectively, and should simply comply with their defined APIs. They
only get two parameters extending the behaviour, so that the user-given
callback can pattern-match on them: the path prefix of the logic
handler, and the operationID of the call.

* Fix return types for provide_callbacks

* Upgrade jesse to incur no dependencies

The less dependencies the built code requires the better.

* Fix dialyzer errors in the generated code

* Apply stronger dialyzer checks
This commit is contained in:
Nelson Vides
2024-09-07 10:45:42 +02:00
committed by GitHub
parent a98f45b4ac
commit 596d446f54
48 changed files with 5057 additions and 2677 deletions

View File

@@ -0,0 +1,23 @@
# OpenAPI Generator Ignore
# Generated by openapi-generator https://github.com/openapitools/openapi-generator
# Use this file to prevent files from being overwritten by the generator.
# The patterns follow closely to .gitignore or .dockerignore.
# As an example, the C# client generator defines ApiClient.cs.
# You can make changes and tell OpenAPI Generator to ignore just this file by uncommenting the following line:
#ApiClient.cs
# You can match any string of characters against a directory, file or extension with a single asterisk (*):
#foo/*/qux
# The above matches foo/bar/qux and foo/baz/qux, but not foo/bar/baz/qux
# You can recursively match patterns against a directory, file or extension with a double asterisk (**):
#foo/**/qux
# This matches foo/bar/qux, foo/baz/qux, and foo/bar/baz/qux
# You can also negate patterns with an exclamation (!).
# For example, you can ignore all files in a docs folder with the file extension .md:
#docs/*.md
# Then explicitly reverse the ignore rule for a single file:
#!docs/README.md

View File

@@ -0,0 +1,15 @@
README.md
priv/openapi.json
rebar.config
src/openapi.app.src
src/openapi_api.erl
src/openapi_auth.erl
src/openapi_auth_handler.erl
src/openapi_body_handler.erl
src/openapi_form_handler.erl
src/openapi_header_handler.erl
src/openapi_logic_handler.erl
src/openapi_path_handler.erl
src/openapi_query_handler.erl
src/openapi_router.erl
src/openapi_server.erl

View File

@@ -0,0 +1 @@
7.9.0-SNAPSHOT

View File

@@ -0,0 +1,36 @@
# OpenAPI server library for Erlang
## Overview
An Erlang server stub generated by [OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec.
Dependencies: Erlang OTP/27 and rebar3. Also:
- [Cowboy](https://hex.pm/packages/cowboy)
- [Ranch](https://hex.pm/packages/ranch)
- [Jesse](https://hex.pm/packages/jesse)
## Prerequisites
## Getting started
Use erlang-server with rebar3
1, Create an application by using rebar3
$ rebar3 new app http_server
2, Generate erlang-server project using openapi-generator
https://github.com/OpenAPITools/openapi-generator#2---getting-started
3, Copy erlang-server file to http_server project, and don't forget the 'priv' folder.
4, 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})
2, Compile your http_server project
$ rebar3 compile
3, Start erlang virtual machine
$ rebar3 shell
4, Start project
application:ensure_all_started(http_server).
To implement your own business logic, create a module called `http_server_logic` that implements the
behaviour `openapi_logic_handler`. Refer to `openapi_logic_handler` documentation for details.

View File

@@ -0,0 +1,46 @@
<project>
<modelVersion>4.0.0</modelVersion>
<groupId>org.openapitools</groupId>
<artifactId>ErlangServerEchoTests</artifactId>
<packaging>pom</packaging>
<version>1.0-SNAPSHOT</version>
<name>Erlang Echo Server</name>
<build>
<plugins>
<plugin>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>copy-dependencies</goal>
</goals>
<configuration>
<outputDirectory>${project.build.directory}</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>1.6.0</version>
<executions>
<execution>
<id>compile-test</id>
<phase>integration-test</phase>
<goals>
<goal>exec</goal>
</goals>
<configuration>
<executable>rebar3</executable>
<arguments>
<argument>compile</argument>
</arguments>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,15 @@
{minimum_otp_vsn, "27"}.
{deps, [
{cowboy, "2.12.0"},
{ranch, "2.1.0"},
{jesse, "1.8.1"}
]}.
{dialyzer,
[{plt_extra_apps, [cowboy, cowlib, ranch, jesse]},
{warnings, [missing_return, unknown]}
]}.
{xref_checks,
[undefined_function_calls, deprecated_function_calls, deprecated_functions]}.

View File

@@ -0,0 +1,11 @@
{application,
openapi,
[{description,
"Echo Server API"},
{vsn, "1.0.0"},
{registered, []},
{applications, [kernel, stdlib, public_key, ssl, inets, ranch, cowboy]},
{env, []},
{modules, []},
{licenses, ["Apache 2.0"]},
{links, []}]}.

View File

@@ -0,0 +1,942 @@
-module(openapi_api).
-moduledoc """
This module offers an API for JSON schema validation, using `jesse` under the hood.
If validation is desired, a jesse state can be loaded using `prepare_validator/1`,
and request and response can be validated using `populate_request/3`
and `validate_response/4` respectively.
For example, the user-defined `Module:accept_callback/4` can be implemented as follows:
```
-spec accept_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}.
accept_callback(Class, OperationID, Req, Context) ->
ValidatorState = openapi_api:prepare_validator(),
case openapi_api:populate_request(OperationID, Req0, ValidatorState) of
{ok, Populated, Req1} ->
{Code, Headers, Body} = openapi_logic_handler:handle_request(
LogicHandler,
OperationID,
Req1,
maps:merge(State#state.context, Populated)
),
_ = openapi_api:validate_response(
OperationID,
Code,
Body,
ValidatorState
),
PreparedBody = prepare_body(Code, Body),
Response = {ok, {Code, Headers, PreparedBody}},
process_response(Response, Req1, State);
{error, Reason, Req1} ->
process_response({error, Reason}, Req1, State)
end.
```
""".
-export([prepare_validator/0, prepare_validator/1, prepare_validator/2]).
-export([populate_request/3, validate_response/4]).
-ignore_xref([populate_request/3, validate_response/4]).
-ignore_xref([prepare_validator/0, prepare_validator/1, prepare_validator/2]).
-type operation_id() :: atom().
-type request_param() :: atom().
-export_type([operation_id/0]).
-dialyzer({nowarn_function, [to_binary/1, to_list/1, validate_response_body/4]}).
-type rule() ::
{type, binary} |
{type, integer} |
{type, float} |
{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.
-doc #{equiv => prepare_validator/2}.
-spec prepare_validator() -> jesse_state:state().
prepare_validator() ->
prepare_validator(<<"http://json-schema.org/draft-06/schema#">>).
-doc #{equiv => prepare_validator/2}.
-spec prepare_validator(binary()) -> jesse_state:state().
prepare_validator(SchemaVer) ->
prepare_validator(get_openapi_path(), SchemaVer).
-doc """
Loads the JSON schema and the desired validation draft into a `t:jesse_state:state()`.
""".
-spec prepare_validator(file:name_all(), binary()) -> jesse_state:state().
prepare_validator(OpenApiPath, SchemaVer) ->
{ok, FileContents} = file:read_file(OpenApiPath),
R = json:decode(FileContents),
jesse_state:new(R, [{default_schema_ver, SchemaVer}]).
-doc """
Automatically loads the entire body from the cowboy req
and validates the JSON body against the schema.
""".
-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, #{}).
-doc """
Validates that the provided `Code` and `Body` comply with the `ValidatorState` schema
for the `OperationID` operation.
""".
-spec validate_response(
OperationID :: operation_id(),
Code :: 200..599,
Body :: jesse:json_term(),
ValidatorState :: jesse_state:state()) ->
ok | {ok, term()} | [ok | {ok, term()}] | no_return().
validate_response('TestAuthHttpBasic', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestAuthHttpBearer', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestBinaryGif', 200, Body, ValidatorState) ->
validate_response_body('file', 'file', Body, ValidatorState);
validate_response('TestBodyApplicationOctetstreamBinary', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestBodyMultipartFormdataArrayOfBinary', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestBodyMultipartFormdataSingleBinary', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestEchoBodyAllOfPet', 200, Body, ValidatorState) ->
validate_response_body('Pet', 'Pet', Body, ValidatorState);
validate_response('TestEchoBodyFreeFormObjectResponseString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestEchoBodyPet', 200, Body, ValidatorState) ->
validate_response_body('Pet', 'Pet', Body, ValidatorState);
validate_response('TestEchoBodyPetResponseString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestEchoBodyStringEnum', 200, Body, ValidatorState) ->
validate_response_body('StringEnumRef', 'StringEnumRef', Body, ValidatorState);
validate_response('TestEchoBodyTagResponseString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestFormIntegerBooleanString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestFormObjectMultipart', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestFormOneof', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestHeaderIntegerBooleanStringEnums', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestEnumRefString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryDatetimeDateString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryIntegerBooleanString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleDeepObjectExplodeTrueObject', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleDeepObjectExplodeTrueObjectAllOf', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleFormExplodeFalseArrayInteger', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleFormExplodeFalseArrayString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleFormExplodeTrueArrayString', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleFormExplodeTrueObject', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response('TestQueryStyleFormExplodeTrueObjectAllOf', 200, Body, ValidatorState) ->
validate_response_body('binary', 'string', Body, ValidatorState);
validate_response(_OperationID, _Code, _Body, _ValidatorState) ->
ok.
%%%
-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()].
request_params('TestAuthHttpBasic') ->
[
];
request_params('TestAuthHttpBearer') ->
[
];
request_params('TestBinaryGif') ->
[
];
request_params('TestBodyApplicationOctetstreamBinary') ->
[
'file'
];
request_params('TestBodyMultipartFormdataArrayOfBinary') ->
[
'files'
];
request_params('TestBodyMultipartFormdataSingleBinary') ->
[
'my-file'
];
request_params('TestEchoBodyAllOfPet') ->
[
'Pet'
];
request_params('TestEchoBodyFreeFormObjectResponseString') ->
[
'object'
];
request_params('TestEchoBodyPet') ->
[
'Pet'
];
request_params('TestEchoBodyPetResponseString') ->
[
'Pet'
];
request_params('TestEchoBodyStringEnum') ->
[
'binary'
];
request_params('TestEchoBodyTagResponseString') ->
[
'Tag'
];
request_params('TestFormIntegerBooleanString') ->
[
'integer_form',
'boolean_form',
'string_form'
];
request_params('TestFormObjectMultipart') ->
[
'marker'
];
request_params('TestFormOneof') ->
[
'form1',
'form2',
'form3',
'form4',
'id',
'name'
];
request_params('TestHeaderIntegerBooleanStringEnums') ->
[
'integer_header',
'boolean_header',
'string_header',
'enum_nonref_string_header',
'enum_ref_string_header'
];
request_params('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}') ->
[
'path_string',
'path_integer',
'enum_nonref_string_path',
'enum_ref_string_path'
];
request_params('TestEnumRefString') ->
[
'enum_nonref_string_query',
'enum_ref_string_query'
];
request_params('TestQueryDatetimeDateString') ->
[
'datetime_query',
'date_query',
'string_query'
];
request_params('TestQueryIntegerBooleanString') ->
[
'integer_query',
'boolean_query',
'string_query'
];
request_params('TestQueryStyleDeepObjectExplodeTrueObject') ->
[
'query_object'
];
request_params('TestQueryStyleDeepObjectExplodeTrueObjectAllOf') ->
[
'query_object'
];
request_params('TestQueryStyleFormExplodeFalseArrayInteger') ->
[
'query_object'
];
request_params('TestQueryStyleFormExplodeFalseArrayString') ->
[
'query_object'
];
request_params('TestQueryStyleFormExplodeTrueArrayString') ->
[
'query_object'
];
request_params('TestQueryStyleFormExplodeTrueObject') ->
[
'query_object'
];
request_params('TestQueryStyleFormExplodeTrueObjectAllOf') ->
[
'query_object'
];
request_params(_) ->
error(unknown_operation).
-spec request_param_info(OperationID :: operation_id(), Name :: request_param()) ->
#{source => qs_val | binding | header | body, rules => [rule()]}.
request_param_info('TestBodyApplicationOctetstreamBinary', 'file') ->
#{
source => body,
rules => [
{type, binary},
schema,
not_required
]
};
request_param_info('TestBodyMultipartFormdataArrayOfBinary', 'files') ->
#{
source => body,
rules => [
required
]
};
request_param_info('TestBodyMultipartFormdataSingleBinary', 'my-file') ->
#{
source => body,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestEchoBodyAllOfPet', 'Pet') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestEchoBodyFreeFormObjectResponseString', 'object') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestEchoBodyPet', 'Pet') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestEchoBodyPetResponseString', 'Pet') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestEchoBodyStringEnum', 'binary') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestEchoBodyTagResponseString', 'Tag') ->
#{
source => body,
rules => [
schema,
not_required
]
};
request_param_info('TestFormIntegerBooleanString', 'integer_form') ->
#{
source => body,
rules => [
{type, integer},
not_required
]
};
request_param_info('TestFormIntegerBooleanString', 'boolean_form') ->
#{
source => body,
rules => [
{type, boolean},
not_required
]
};
request_param_info('TestFormIntegerBooleanString', 'string_form') ->
#{
source => body,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestFormObjectMultipart', 'marker') ->
#{
source => body,
rules => [
required
]
};
request_param_info('TestFormOneof', 'form1') ->
#{
source => body,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestFormOneof', 'form2') ->
#{
source => body,
rules => [
{type, integer},
not_required
]
};
request_param_info('TestFormOneof', 'form3') ->
#{
source => body,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestFormOneof', 'form4') ->
#{
source => body,
rules => [
{type, boolean},
not_required
]
};
request_param_info('TestFormOneof', 'id') ->
#{
source => body,
rules => [
{type, integer},
not_required
]
};
request_param_info('TestFormOneof', 'name') ->
#{
source => body,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestHeaderIntegerBooleanStringEnums', 'integer_header') ->
#{
source => header,
rules => [
{type, integer},
not_required
]
};
request_param_info('TestHeaderIntegerBooleanStringEnums', 'boolean_header') ->
#{
source => header,
rules => [
{type, boolean},
not_required
]
};
request_param_info('TestHeaderIntegerBooleanStringEnums', 'string_header') ->
#{
source => header,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestHeaderIntegerBooleanStringEnums', 'enum_nonref_string_header') ->
#{
source => header,
rules => [
{type, binary},
{enum, ['success', 'failure', 'unclassified'] },
not_required
]
};
request_param_info('TestHeaderIntegerBooleanStringEnums', 'enum_ref_string_header') ->
#{
source => header,
rules => [
not_required
]
};
request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'path_string') ->
#{
source => binding,
rules => [
{type, binary},
required
]
};
request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'path_integer') ->
#{
source => binding,
rules => [
{type, integer},
required
]
};
request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'enum_nonref_string_path') ->
#{
source => binding,
rules => [
{type, binary},
{enum, ['success', 'failure', 'unclassified'] },
required
]
};
request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'enum_ref_string_path') ->
#{
source => binding,
rules => [
required
]
};
request_param_info('TestEnumRefString', 'enum_nonref_string_query') ->
#{
source => qs_val,
rules => [
{type, binary},
{enum, ['success', 'failure', 'unclassified'] },
not_required
]
};
request_param_info('TestEnumRefString', 'enum_ref_string_query') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryDatetimeDateString', 'datetime_query') ->
#{
source => qs_val,
rules => [
{type, datetime},
not_required
]
};
request_param_info('TestQueryDatetimeDateString', 'date_query') ->
#{
source => qs_val,
rules => [
{type, date},
not_required
]
};
request_param_info('TestQueryDatetimeDateString', 'string_query') ->
#{
source => qs_val,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestQueryIntegerBooleanString', 'integer_query') ->
#{
source => qs_val,
rules => [
{type, integer},
not_required
]
};
request_param_info('TestQueryIntegerBooleanString', 'boolean_query') ->
#{
source => qs_val,
rules => [
{type, boolean},
not_required
]
};
request_param_info('TestQueryIntegerBooleanString', 'string_query') ->
#{
source => qs_val,
rules => [
{type, binary},
not_required
]
};
request_param_info('TestQueryStyleDeepObjectExplodeTrueObject', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleDeepObjectExplodeTrueObjectAllOf', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleFormExplodeFalseArrayInteger', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleFormExplodeFalseArrayString', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleFormExplodeTrueArrayString', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleFormExplodeTrueObject', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info('TestQueryStyleFormExplodeTrueObjectAllOf', 'query_object') ->
#{
source => qs_val,
rules => [
not_required
]
};
request_param_info(OperationID, Name) ->
error({unknown_param, OperationID, Name}).
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.
-include_lib("kernel/include/logger.hrl").
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, to_int(Value)}
catch
error:badarg ->
validation_error(Rule, Name)
end;
validate(Rule = {type, float}, Name, Value, _ValidatorState) ->
try
{ok, 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/" ++ 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) ->
?LOG_INFO(#{what => "Cannot validate rule", name => Name, rule => 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 = get_opt(to_qs(Name), QS),
{Value, Req};
get_value(header, Name, Req) ->
Headers = cowboy_req:headers(Req),
Value = maps:get(to_header(Name), Headers, undefined),
{Value, Req};
get_value(binding, Name, Req) ->
Value = cowboy_req:binding(to_binding(Name), Req),
{Value, Req}.
prepare_body(<<>>) ->
<<>>;
prepare_body(Body) ->
try
json:decode(Body)
catch
error:_ ->
{error, {invalid_body, not_json, Body}}
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.
-spec to_binary(iodata() | atom() | number()) -> binary().
to_binary(V) when is_binary(V) -> V;
to_binary(V) when is_list(V) -> iolist_to_binary(V);
to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8);
to_binary(V) when is_integer(V) -> integer_to_binary(V);
to_binary(V) when is_float(V) -> float_to_binary(V).
-spec to_list(iodata() | atom() | number()) -> binary().
to_list(V) when is_list(V) -> V;
to_list(V) when is_binary(V) -> binary_to_list(V);
to_list(V) when is_atom(V) -> atom_to_list(V);
to_list(V) when is_integer(V) -> integer_to_list(V);
to_list(V) when is_float(V) -> float_to_list(V).
-spec to_float(iodata()) -> float().
to_float(V) ->
binary_to_float(iolist_to_binary([V])).
-spec to_int(integer() | binary() | list()) -> integer().
to_int(Data) when is_integer(Data) ->
Data;
to_int(Data) when is_binary(Data) ->
binary_to_integer(Data);
to_int(Data) when is_list(Data) ->
list_to_integer(Data).
-spec to_header(iodata() | atom() | number()) -> binary().
to_header(Name) ->
to_binary(string:lowercase(to_binary(Name))).
binary_to_lower(V) when is_binary(V) ->
string:lowercase(V).
-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_existing_atom(Prepared, utf8).
-spec get_opt(any(), []) -> any().
get_opt(Key, Opts) ->
get_opt(Key, Opts, undefined).
-spec get_opt(any(), [], any()) -> any().
get_opt(Key, Opts, Default) ->
case lists:keyfind(Key, 1, Opts) of
{_, Value} -> Value;
false -> Default
end.
get_openapi_path() ->
{ok, AppName} = application:get_application(?MODULE),
filename:join(priv_dir(AppName), "openapi.json").
-include_lib("kernel/include/file.hrl").
-spec priv_dir(Application :: atom()) -> file:name_all().
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.
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.

View File

@@ -0,0 +1,45 @@
-module(openapi_auth).
-export([authorize_api_key/5]).
-spec authorize_api_key(openapi_logic_handler:api_key_callback(),
openapi_api:operation_id(),
header | qs_val,
iodata() | atom(),
cowboy_req:req()) ->
{true, openapi_logic_handler:context(), cowboy_req:req()} |
{false, binary(), cowboy_req:req()}.
authorize_api_key(Handler, OperationID, From, KeyParam, Req0) ->
{ApiKey, Req} = get_api_key(From, KeyParam, Req0),
case ApiKey of
undefined ->
AuthHeader = <<>>,
{false, AuthHeader, Req};
_ ->
case Handler(OperationID, ApiKey) of
{true, Context} ->
{true, Context, Req};
{false, AuthHeader} ->
{false, AuthHeader, Req}
end
end.
get_api_key(header, KeyParam, Req) ->
Headers = cowboy_req:headers(Req),
{maps:get(KeyParam, Headers, undefined), Req};
get_api_key(qs_val, KeyParam, Req) ->
QS = cowboy_req:parse_qs(Req),
{get_opt(KeyParam, QS), Req}.
-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.

View File

@@ -0,0 +1,126 @@
%% basic handler
-module(openapi_auth_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestAuthHttpBasic'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestAuthHttpBearer'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req0,
#state{operation_id = 'TestAuthHttpBasic' = OperationID,
api_key_handler = Handler} = State) ->
case openapi_auth:authorize_api_key(Handler, OperationID, header, "authorization", Req0) of
{true, Context, Req} ->
{true, Req, State#state{context = Context}};
{false, AuthHeader, Req} ->
{{false, AuthHeader}, Req, State}
end;
is_authorized(Req0,
#state{operation_id = 'TestAuthHttpBearer' = OperationID,
api_key_handler = Handler} = State) ->
case openapi_auth:authorize_api_key(Handler, OperationID, header, "authorization", Req0) of
{true, Context, Req} ->
{true, Req, State#state{context = Context}};
{false, AuthHeader, Req} ->
{{false, AuthHeader}, Req, State}
end;
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestAuthHttpBasic'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestAuthHttpBearer'} = State) ->
{[], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestAuthHttpBasic'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestAuthHttpBearer'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestAuthHttpBasic'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestAuthHttpBearer'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(auth, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(auth, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,206 @@
%% basic handler
-module(openapi_body_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestBinaryGif'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyPet'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyStringEnum'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestBinaryGif'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) ->
{[
{<<"application/octet-stream">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) ->
{[
{<<"multipart/form-data">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) ->
{[
{<<"multipart/form-data">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyPet'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyStringEnum'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) ->
{[
{<<"application/json">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestBinaryGif'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyPet'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyStringEnum'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestBinaryGif'} = State) ->
{[
{<<"image/gif">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) ->
{[
{<<"application/json">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyPet'} = State) ->
{[
{<<"application/json">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyStringEnum'} = State) ->
{[
{<<"application/json">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(body, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(body, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,124 @@
%% basic handler
-module(openapi_form_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestFormIntegerBooleanString'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestFormObjectMultipart'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestFormOneof'} = State) ->
{[<<"POST">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestFormIntegerBooleanString'} = State) ->
{[
{<<"application/x-www-form-urlencoded">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestFormObjectMultipart'} = State) ->
{[
{<<"multipart/form-data">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestFormOneof'} = State) ->
{[
{<<"application/x-www-form-urlencoded">>, handle_type_accepted}
], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestFormIntegerBooleanString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestFormObjectMultipart'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestFormOneof'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestFormIntegerBooleanString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestFormObjectMultipart'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestFormOneof'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(form, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(form, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,98 @@
%% basic handler
-module(openapi_header_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) ->
{[], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(header, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(header, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,56 @@
-module(openapi_logic_handler).
-include_lib("kernel/include/logger.hrl").
-type api_key_callback() ::
fun((openapi_api:operation_id(), binary()) -> {true, context()} | {false, iodata()}).
-type accept_callback() ::
fun((atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
boolean() | {created, iodata()} | {see_other, iodata()}).
-type provide_callback() ::
fun((atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
{cowboy_req:resp_body(), cowboy_req:req(), context()}).
-type context() :: #{binary() => any()}.
-export_type([context/0, api_key_callback/0, accept_callback/0, provide_callback/0]).
-optional_callbacks([api_key_callback/2]).
-callback api_key_callback(openapi_api:operation_id(), binary()) ->
{true, context()} | {false, iodata()}.
-callback accept_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
-callback provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
{cowboy_req:resp_body(), cowboy_req:req(), context()}.
-export([api_key_callback/2, accept_callback/4, provide_callback/4]).
-ignore_xref([api_key_callback/2, accept_callback/4, provide_callback/4]).
-spec api_key_callback(openapi_api:operation_id(), binary()) -> {true, #{}}.
api_key_callback(OperationID, ApiKey) ->
?LOG_ERROR(#{what => "Got not implemented api_key_callback request",
operation_id => OperationID,
api_key => ApiKey}),
{true, #{}}.
-spec accept_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}.
accept_callback(Class, OperationID, Req, Context) ->
?LOG_ERROR(#{what => "Got not implemented request to process",
class => Class,
operation_id => OperationID,
request => Req,
context => Context}),
{501, #{}, #{}}.
-spec provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) ->
{cowboy_req:resp_body(), cowboy_req:req(), context()}.
provide_callback(Class, OperationID, Req, Context) ->
?LOG_ERROR(#{what => "Got not implemented request to process",
class => Class,
operation_id => OperationID,
request => Req,
context => Context}),
{<<>>, Req, Context}.

View File

@@ -0,0 +1,98 @@
%% basic handler
-module(openapi_path_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) ->
{[], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(path, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(path, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,188 @@
%% basic handler
-module(openapi_query_handler).
-behaviour(cowboy_rest).
-include_lib("kernel/include/logger.hrl").
%% Cowboy REST callbacks
-export([init/2]).
-export([allowed_methods/2]).
-export([content_types_accepted/2]).
-export([content_types_provided/2]).
-export([delete_resource/2]).
-export([is_authorized/2]).
-export([valid_content_headers/2]).
-export([handle_type_accepted/2, handle_type_provided/2]).
-ignore_xref([handle_type_accepted/2, handle_type_provided/2]).
-record(state,
{operation_id :: openapi_api:operation_id(),
accept_callback :: openapi_logic_handler:accept_callback(),
provide_callback :: openapi_logic_handler:provide_callback(),
api_key_handler :: openapi_logic_handler:api_key_callback(),
context = #{} :: openapi_logic_handler:context()}).
-type state() :: #state{}.
-spec init(cowboy_req:req(), openapi_router:init_opts()) ->
{cowboy_rest, cowboy_req:req(), state()}.
init(Req, {Operations, Module}) ->
Method = cowboy_req:method(Req),
OperationID = maps:get(Method, Operations, undefined),
?LOG_INFO(#{what => "Attempt to process operation",
method => Method,
operation_id => OperationID}),
State = #state{operation_id = OperationID,
accept_callback = fun Module:accept_callback/4,
provide_callback = fun Module:provide_callback/4,
api_key_handler = fun Module:authorize_api_key/2},
{cowboy_rest, Req, State}.
-spec allowed_methods(cowboy_req:req(), state()) ->
{[binary()], cowboy_req:req(), state()}.
allowed_methods(Req, #state{operation_id = 'TestEnumRefString'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryDatetimeDateString'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) ->
{[<<"GET">>], Req, State};
allowed_methods(Req, State) ->
{[], Req, State}.
-spec is_authorized(cowboy_req:req(), state()) ->
{true | {false, iodata()}, cowboy_req:req(), state()}.
is_authorized(Req, State) ->
{true, Req, State}.
-spec content_types_accepted(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_accepted(Req, #state{operation_id = 'TestEnumRefString'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryDatetimeDateString'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) ->
{[], Req, State};
content_types_accepted(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) ->
{[], Req, State};
content_types_accepted(Req, State) ->
{[], Req, State}.
-spec valid_content_headers(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
valid_content_headers(Req, #state{operation_id = 'TestEnumRefString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryDatetimeDateString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) ->
{true, Req, State};
valid_content_headers(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) ->
{true, Req, State};
valid_content_headers(Req, State) ->
{false, Req, State}.
-spec content_types_provided(cowboy_req:req(), state()) ->
{[{binary(), atom()}], cowboy_req:req(), state()}.
content_types_provided(Req, #state{operation_id = 'TestEnumRefString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryDatetimeDateString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) ->
{[
{<<"text/plain">>, handle_type_provided}
], Req, State};
content_types_provided(Req, State) ->
{[], Req, State}.
-spec delete_resource(cowboy_req:req(), state()) ->
{boolean(), cowboy_req:req(), state()}.
delete_resource(Req, State) ->
case handle_type_accepted(Req, State) of
true ->
{true, Req, State};
_ ->
{false, Req, State}
end.
-spec handle_type_accepted(cowboy_req:req(), state()) ->
boolean() | {created, iodata()} | {see_other, iodata()}.
handle_type_accepted(Req, #state{operation_id = OperationID,
accept_callback = Handler} = State) ->
Handler(query, OperationID, Req, State#state.context).
-spec handle_type_provided(cowboy_req:req(), state()) ->
{cowboy_req:resp_body(), cowboy_req:req(), openapi_logic_handler:context()}.
handle_type_provided(Req, #state{operation_id = OperationID,
provide_callback = Handler} = State) ->
Handler(query, OperationID, Req, State#state.context).

View File

@@ -0,0 +1,172 @@
-module(openapi_router).
-export([get_paths/1]).
-type method() :: binary().
-type operations() :: #{method() => openapi_api:operation_id()}.
-type init_opts() :: {operations(), module()}.
-export_type([init_opts/0]).
-spec get_paths(LogicHandler :: module()) -> cowboy_router:routes().
get_paths(LogicHandler) ->
PreparedPaths = maps:fold(
fun(Path, #{operations := Operations, handler := Handler}, Acc) ->
[{Path, Handler, Operations} | Acc]
end, [], group_paths()
),
[{'_', [{P, H, {O, LogicHandler}} || {P, H, O} <- PreparedPaths]}].
group_paths() ->
maps:fold(
fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) ->
case maps:find(Path, Acc) of
{ok, PathInfo0 = #{operations := Operations0}} ->
Operations = Operations0#{Method => OperationID},
PathInfo = PathInfo0#{operations => Operations},
Acc#{Path => PathInfo};
error ->
Operations = #{Method => OperationID},
PathInfo = #{handler => Handler, operations => Operations},
Acc#{Path => PathInfo}
end
end, #{}, get_operations()).
get_operations() ->
#{
'TestAuthHttpBasic' => #{
path => "/auth/http/basic",
method => <<"POST">>,
handler => 'openapi_auth_handler'
},
'TestAuthHttpBearer' => #{
path => "/auth/http/bearer",
method => <<"POST">>,
handler => 'openapi_auth_handler'
},
'TestBinaryGif' => #{
path => "/binary/gif",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyApplicationOctetstreamBinary' => #{
path => "/body/application/octetstream/binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyMultipartFormdataArrayOfBinary' => #{
path => "/body/application/octetstream/array_of_binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestBodyMultipartFormdataSingleBinary' => #{
path => "/body/application/octetstream/single_binary",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyAllOfPet' => #{
path => "/echo/body/allOf/Pet",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyFreeFormObjectResponseString' => #{
path => "/echo/body/FreeFormObject/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyPet' => #{
path => "/echo/body/Pet",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyPetResponseString' => #{
path => "/echo/body/Pet/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyStringEnum' => #{
path => "/echo/body/string_enum",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestEchoBodyTagResponseString' => #{
path => "/echo/body/Tag/response_string",
method => <<"POST">>,
handler => 'openapi_body_handler'
},
'TestFormIntegerBooleanString' => #{
path => "/form/integer/boolean/string",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestFormObjectMultipart' => #{
path => "/form/object/multipart",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestFormOneof' => #{
path => "/form/oneof",
method => <<"POST">>,
handler => 'openapi_form_handler'
},
'TestHeaderIntegerBooleanStringEnums' => #{
path => "/header/integer/boolean/string/enums",
method => <<"GET">>,
handler => 'openapi_header_handler'
},
'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}' => #{
path => "/path/string/:path_string/integer/:path_integer/:enum_nonref_string_path/:enum_ref_string_path",
method => <<"GET">>,
handler => 'openapi_path_handler'
},
'TestEnumRefString' => #{
path => "/query/enum_ref_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryDatetimeDateString' => #{
path => "/query/datetime/date/string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryIntegerBooleanString' => #{
path => "/query/integer/boolean/string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleDeepObjectExplodeTrueObject' => #{
path => "/query/style_deepObject/explode_true/object",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleDeepObjectExplodeTrueObjectAllOf' => #{
path => "/query/style_deepObject/explode_true/object/allOf",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeFalseArrayInteger' => #{
path => "/query/style_form/explode_false/array_integer",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeFalseArrayString' => #{
path => "/query/style_form/explode_false/array_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueArrayString' => #{
path => "/query/style_form/explode_true/array_string",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueObject' => #{
path => "/query/style_form/explode_true/object",
method => <<"GET">>,
handler => 'openapi_query_handler'
},
'TestQueryStyleFormExplodeTrueObjectAllOf' => #{
path => "/query/style_form/explode_true/object/allOf",
method => <<"GET">>,
handler => 'openapi_query_handler'
}
}.

View File

@@ -0,0 +1,43 @@
-module(openapi_server).
-define(DEFAULT_LOGIC_HANDLER, openapi_logic_handler).
-export([start/2]).
-ignore_xref([start/2]).
-spec start(term(), #{transport => tcp | ssl,
transport_opts => ranch:opts(),
protocol_opts => cowboy:opts(),
logic_handler => module()}) ->
{ok, pid()} | {error, any()}.
start(ID, Params) ->
Transport = maps:get(transport, Params, tcp),
TransportOpts = maps:get(transport_opts, Params, #{}),
ProtocolOpts = maps:get(procotol_opts, Params, #{}),
LogicHandler = maps:get(logic_handler, Params, ?DEFAULT_LOGIC_HANDLER),
CowboyOpts = get_cowboy_config(LogicHandler, ProtocolOpts),
case Transport of
ssl ->
cowboy:start_tls(ID, TransportOpts, CowboyOpts);
tcp ->
cowboy:start_clear(ID, TransportOpts, CowboyOpts)
end.
get_cowboy_config(LogicHandler, ExtraOpts) ->
DefaultOpts = get_default_opts(LogicHandler),
maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts).
get_cowboy_config(env, #{dispatch := _Dispatch} = Env, AccIn) ->
maps:put(env, Env, AccIn);
get_cowboy_config(env, NewEnv, #{env := OldEnv} = AccIn) ->
Env = maps:merge(OldEnv, NewEnv),
maps:put(env, Env, AccIn);
get_cowboy_config(Key, Value, AccIn) ->
maps:put(Key, Value, AccIn).
get_default_dispatch(LogicHandler) ->
Paths = openapi_router:get_paths(LogicHandler),
#{dispatch => cowboy_router:compile(Paths)}.
get_default_opts(LogicHandler) ->
#{env => get_default_dispatch(LogicHandler)}.