diff --git a/.github/workflows/samples-erlang.yaml b/.github/workflows/samples-erlang.yaml index c9e0981e49c4..95cda5dc5a3b 100644 --- a/.github/workflows/samples-erlang.yaml +++ b/.github/workflows/samples-erlang.yaml @@ -3,33 +3,33 @@ name: Samples Erlang on: push: paths: - # comment out due to errors - # ===> Compiling src/openapi_pet_handler.erl failed - # src/openapi_pet_handler.erl:278: function is_authorized/2 already defined - #- samples/server/petstore/erlang-server/** + - samples/server/echo_api/erlang-server/** + - samples/server/petstore/erlang-server/** - samples/client/petstore/erlang-client/** - samples/client/petstore/erlang-proper/** pull_request: paths: - #- samples/server/petstore/erlang-server/** + - samples/server/echo_api/erlang-server/** + - samples/server/petstore/erlang-server/** - samples/client/petstore/erlang-client/** - samples/client/petstore/erlang-proper/** jobs: build: name: Build Erlang projects - runs-on: ubuntu-20.04 + runs-on: ubuntu-24.04 strategy: fail-fast: false matrix: sample: - #- samples/server/petstore/erlang-server/ + - samples/server/echo_api/erlang-server/ + - samples/server/petstore/erlang-server/ - samples/client/petstore/erlang-client/ - samples/client/petstore/erlang-proper/ steps: - uses: actions/checkout@v4 - uses: erlef/setup-beam@v1 with: - otp-version: '22.2' - rebar3-version: '3.14.3' + otp-version: '27' + rebar3-version: '3.23.0' - run: rebar3 compile working-directory: ${{ matrix.sample }} diff --git a/bin/configs/erlang-server-echo.yaml b/bin/configs/erlang-server-echo.yaml new file mode 100644 index 000000000000..7ed821f1c3b1 --- /dev/null +++ b/bin/configs/erlang-server-echo.yaml @@ -0,0 +1,4 @@ +generatorName: erlang-server +outputDir: samples/server/echo_api/erlang-server +inputSpec: modules/openapi-generator/src/test/resources/3_0/echo_api.yaml +templateDir: modules/openapi-generator/src/main/resources/erlang-server diff --git a/docs/generators/erlang-server.md b/docs/generators/erlang-server.md index 66692f029e7d..824149119699 100644 --- a/docs/generators/erlang-server.md +++ b/docs/generators/erlang-server.md @@ -59,6 +59,7 @@ These options may be applied as additional-properties (cli) or configOptions (pl
  • fun
  • if
  • let
  • +
  • maybe
  • not
  • of
  • or
  • diff --git a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangServerCodegen.java b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangServerCodegen.java index d41136eb6761..5f8a50a312b0 100644 --- a/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangServerCodegen.java +++ b/modules/openapi-generator/src/main/java/org/openapitools/codegen/languages/ErlangServerCodegen.java @@ -99,9 +99,9 @@ public ErlangServerCodegen() { */ setReservedWordsLowerCase( Arrays.asList( - "after", "and", "andalso", "band", "begin", "bnot", "bor", "bsl", "bsr", "bxor", "case", - "catch", "cond", "div", "end", "fun", "if", "let", "not", "of", "or", "orelse", "receive", - "rem", "try", "when", "xor" + "after", "and", "andalso", "band", "begin", "bnot", "bor", "bsl", "bsr", "bxor", + "case", "catch", "cond", "div", "end", "fun", "if", "let", "maybe", "not", + "of", "or", "orelse", "receive", "rem", "try", "when", "xor" ) ); @@ -172,10 +172,8 @@ public void processOpts() { 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("logic_handler.mustache", "", toSourceFilePath("logic_handler", "erl"))); supportingFiles.add(new SupportingFile("README.mustache", "", "README.md") .doNotOverwrite()); @@ -223,7 +221,7 @@ public String getHelp() { @Override public String toApiName(String name) { if (name.length() == 0) { - return this.packageName + "_default_handler"; + return this.packageName + "_handler"; } return this.packageName + "_" + underscore(name) + "_handler"; } diff --git a/modules/openapi-generator/src/main/resources/erlang-server/README.mustache b/modules/openapi-generator/src/main/resources/erlang-server/README.mustache index 5d36ca07576b..887f730062fe 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/README.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/README.mustache @@ -4,54 +4,33 @@ An Erlang server stub generated by [OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec. -Dependency: [Cowboy](https://github.com/ninenines/cowboy) +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 -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 +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 - 4, Copy erlang-server file to http_server project,Don't forget the 'priv' folder. + 3, Copy erlang-server file to http_server project, and don't forget the 'priv' folder. - 5, Start in the http_server project: + 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, net_opts=>[]}) - 2, Compilation http_server project - $ make + 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 - $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 + $ 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. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache index b97e4a2c5eca..ba8f13854f61 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -1,37 +1,60 @@ -module({{packageName}}_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]). --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]). +-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]). --spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. -{{#apiInfo}}{{#apis}} -{{#operations}}{{#operation}} -request_params('{{operationId}}') -> - [{{#allParams}}{{^isBodyParam}} - '{{baseName}}'{{/isBodyParam}}{{#isBodyParam}} - '{{dataType}}'{{/isBodyParam}}{{^-last}},{{/-last}}{{/allParams}} - ]; -{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} -request_params(_) -> - error(unknown_operation). +-dialyzer({nowarn_function, [to_binary/1, to_list/1, validate_response_body/4]}). -type rule() :: - {type, 'binary'} | - {type, 'integer'} | - {type, 'float'} | - {type, 'binary'} | - {type, 'boolean'} | - {type, 'date'} | - {type, 'datetime'} | + {type, binary} | + {type, integer} | + {type, float} | + {type, boolean} | + {type, date} | + {type, datetime} | {enum, [atom()]} | {max, Max :: number()} | {exclusive_max, Max :: number()} | @@ -44,59 +67,99 @@ request_params(_) -> required | not_required. --spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> #{ - source => qs_val | binding | header | body, - rules => [rule()] -}. +-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(). +{{#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. -{{#apiInfo}}{{#apis}} -{{#operations}}{{#operation}}{{#allParams}} -request_param_info('{{operationId}}', {{^isBodyParam}}'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}'{{dataType}}'{{/isBodyParam}}) -> +%%% +-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. +{{#apiInfo}}{{#apis}}{{#operations}}{{#operation}}request_params('{{operationId}}') -> + [{{#allParams}}{{^isBodyParam}} + '{{baseName}}'{{/isBodyParam}}{{#isBodyParam}} + '{{dataType}}'{{/isBodyParam}}{{^-last}},{{/-last}}{{/allParams}} + ]; +{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}request_params(_) -> + error(unknown_operation). + +-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}}, + 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}} + {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}} + {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) -> +{{/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} -> @@ -118,24 +181,9 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) -> 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}} +-include_lib("kernel/include/logger.hrl"). -validate_response(_OperationID, _Code, _Body, _ValidatorState) -> - ok. - -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ validate(schema, ReturnBaseType, Item, ValidatorState) || Item <- Body]; @@ -143,45 +191,37 @@ validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> 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) -> +validate(Rule = {type, integer}, Name, Value, _ValidatorState) -> try - {ok, {{packageName}}_utils:to_int(Value)} + {ok, to_int(Value)} catch error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, float}, Name, Value, _ValidatorState) -> try - {ok, {{packageName}}_utils:to_float(Value)} + {ok, to_float(Value)} catch error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) -> +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) -> +validate(_Rule = {type, boolean}, _Name, Value, _ValidatorState) when is_boolean(Value) -> {ok, Value}; - -validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, boolean}, Name, Value, _ValidatorState) -> V = binary_to_lower(Value), try case binary_to_existing_atom(V, utf8) of @@ -192,19 +232,16 @@ validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) -> +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) -> +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), @@ -216,52 +253,44 @@ validate(Rule = {enum, Values}, Name, Value, _ValidatorState) -> 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)), + Definition = list_to_binary("#/components/schemas/" ++ to_list(Name)), try _ = validate_with_schema(Value, Definition, ValidatorState), ok @@ -281,18 +310,15 @@ validate(Rule = schema, Name, Value, ValidatorState) -> }, validation_error(Rule, Name, Info) end; - validate(Rule, Name, _Value, _ValidatorState) -> - error_logger:info_msg("Can't validate ~p with ~p", [Name, Rule]), + ?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(). - +-spec validation_error(Rule :: any(), Name :: any(), Info :: #{_ := _}) -> no_return(). validation_error(ViolatedRule, Name, Info) -> throw({wrong_param, Name, ViolatedRule, Info}). @@ -307,31 +333,26 @@ get_value(body, _Name, Req0) -> 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 = get_opt(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 = maps:get(to_header(Name), Headers, undefined), {Value, Req}; - get_value(binding, Name, Req) -> - Value = cowboy_req:binding({{packageName}}_utils:to_binding(Name), Req), + Value = cowboy_req:binding(to_binding(Name), Req), {Value, Req}. +prepare_body(<<>>) -> + <<>>; prepare_body(Body) -> - case Body of - <<"">> -> <<"">>; - _ -> - try - jsx:decode(Body, [return_maps]) - catch - error:_ -> - {error, {invalid_body, not_json, Body}} - end + try + json:decode(Body) + catch + error:_ -> + {error, {invalid_body, not_json, Body}} end. validate_with_schema(Body, Definition, ValidatorState) -> @@ -359,5 +380,84 @@ prepare_param(Rules, Name, Value, ValidatorState) -> {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) -> - list_to_binary(string:to_lower({{packageName}}_utils:to_list(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), "{{{openAPISpecName}}}.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. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/app.src.mustache b/modules/openapi-generator/src/main/resources/erlang-server/app.src.mustache index a90c4e7cae77..a7ab1dbc1492 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/app.src.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/app.src.mustache @@ -1,19 +1,11 @@ -{application, {{packageName}}, [ - {description, {{#appDescription}}"{{.}}"{{/appDescription}}{{^appDescription}}"OpenAPI rest server library"{{/appDescription}}}, - {vsn, "{{apiVersion}}"}, - {registered, []}, - {applications, [ - kernel, - stdlib, - ssl, - inets, - jsx, - jesse, - cowboy - ]}, - {env, [ - ]}, - {modules, []}, - {licenses, [{{#licenseInfo}}"{{.}}"{{/licenseInfo}}]}, - {links, [{{#infoUrl}}"{{.}}"{{/infoUrl}}]} -]}. +{application, + {{packageName}}, + [{description, + {{#appDescription}}"{{.}}"{{/appDescription}}{{^appDescription}}"OpenAPI rest server library"{{/appDescription}}}, + {vsn, "{{apiVersion}}"}, + {registered, []}, + {applications, [kernel, stdlib, public_key, ssl, inets, ranch, cowboy]}, + {env, []}, + {modules, []}, + {licenses, [{{#licenseInfo}}"{{.}}"{{/licenseInfo}}]}, + {links, [{{#infoUrl}}"{{.}}"{{/infoUrl}}]}]}. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache b/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache index 3159e352a9c5..060cb0269bd2 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache @@ -2,51 +2,44 @@ -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) -> +-spec authorize_api_key({{packageName}}_logic_handler:api_key_callback(), + {{packageName}}_api:operation_id(), + header | qs_val, + iodata() | atom(), + cowboy_req:req()) -> + {true, {{packageName}}_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 = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req}; _ -> - Result = {{packageName}}_logic_handler:authorize_api_key( - LogicHandler, - OperationID, - ApiKey - ), - case Result of - {{#authMethods}} - {{#isApiKey}} - {true, Context} -> + case Handler(OperationID, ApiKey) of + {true, Context} -> {true, Context, Req}; - {{/isApiKey}} - {{/authMethods}} - false -> - AuthHeader = <<"">>, + {false, AuthHeader} -> {false, AuthHeader, Req} end end. get_api_key(header, KeyParam, Req) -> Headers = cowboy_req:headers(Req), - { - maps:get( - {{packageName}}_utils:to_header(KeyParam), - Headers, - undefined - ), - Req - }; - + {maps:get(KeyParam, Headers, undefined), Req}; get_api_key(qs_val, KeyParam, Req) -> QS = cowboy_req:parse_qs(Req), - { {{packageName}}_utils:get_opt(KeyParam, 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. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/default_logic_handler.mustache b/modules/openapi-generator/src/main/resources/erlang-server/default_logic_handler.mustache deleted file mode 100644 index 1b39ed1d75a9..000000000000 --- a/modules/openapi-generator/src/main/resources/erlang-server/default_logic_handler.mustache +++ /dev/null @@ -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, #{}, #{}}. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache index 70756b2549ff..833c364c9377 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -1,252 +1,129 @@ %% basic handler -module({{classname}}). +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). - -%% Handlers --export([handle_request_json/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: {{packageName}}_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type state() :: state(). +-record(state, + {operation_id :: {{packageName}}_api:operation_id(), + accept_callback :: {{packageName}}_logic_handler:accept_callback(), + provide_callback :: {{packageName}}_logic_handler:provide_callback(), + api_key_handler :: {{packageName}}_logic_handler:api_key_callback(), + context = #{} :: {{packageName}}_logic_handler:context()}). --spec init(Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. +-type state() :: #state{}. -init(Req, {Operations, LogicHandler, ValidatorMod}) -> +-spec init(cowboy_req:req(), {{packageName}}_router:init_opts()) -> + {cowboy_rest, cowboy_req:req(), state()}. +init(Req, {Operations, Module}) -> Method = cowboy_req:method(Req), OperationID = maps:get(Method, Operations, undefined), - - ValidatorState = ValidatorMod:get_validator_state(), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, + ?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(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. - -{{#operations}}{{#operation}} -allowed_methods( - Req, - State = #state{ - operation_id = '{{operationId}}' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +{{#operations}}{{#operation}}allowed_methods(Req, #state{operation_id = '{{operationId}}'} = State) -> {[<<"{{httpMethod}}">>], Req, State}; -{{/operation}}{{/operations}} -allowed_methods(Req, State) -> +{{/operation}}{{/operations}}allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. {{#operations}} {{#operation}} -{{#authMethods}} -is_authorized( - Req0, - State = #state{ - operation_id = '{{operationId}}' = OperationID, - logic_handler = LogicHandler - } -) -> - {{#isApiKey}} - From = {{#isKeyInQuery}}qs_val{{/isKeyInQuery}}{{#isKeyInHeader}}header{{/isKeyInHeader}}, - Result = {{packageName}}_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "{{keyParamName}}", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} - end; - {{/isApiKey}} - {{#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} +{{#authMethods.size}} +is_authorized(Req0, + #state{operation_id = '{{operationId}}' = OperationID, + api_key_handler = Handler} = State) -> + case {{packageName}}_auth:authorize_api_key(Handler, OperationID, {{#isApiKey.isKeyInQuery}}qs_val, {{/isApiKey.isKeyInQuery}}{{^isApiKey.isKeyInQuery}}header, {{/isApiKey.isKeyInQuery}}{{#isApiKey}}"{{keyParamName}}", {{/isApiKey}}{{^isApiKey}}"authorization", {{/isApiKey}}Req0) of + {true, Context, Req} -> + {true, Req, State#state{context = Context}}; + {false, AuthHeader, Req} -> + {{false, AuthHeader}, Req, State} end; - {{/isOAuth}} -{{/authMethods}} +{{/authMethods.size}} {{/operation}} {{/operations}} -{{^authMethods}} is_authorized(Req, State) -> {true, Req, State}. -{{/authMethods}} -{{#authMethods}} -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(), - State :: state() - }. -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +{{#operations}}{{#operation}}content_types_accepted(Req, #state{operation_id = '{{operationId}}'} = State) -> + {{^consumes.size}} + {[], Req, State}; + {{/consumes.size}} + {{#consumes.size}} {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. + {{#consumes}} + {<<"{{mediaType}}">>, handle_type_accepted}{{^-last}}{{#consumes.size}},{{/consumes.size}}{{/-last}} + {{/consumes}} + ], Req, State}; + {{/consumes.size}} +{{/operation}}{{/operations}}content_types_accepted(Req, State) -> + {[], Req, State}. --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. -{{#operations}}{{#operation}} -valid_content_headers( - Req0, - State = #state{ - operation_id = '{{operationId}}' - } -) -> - Headers = [{{#headerParams}}"{{baseName}}"{{^-last}},{{/-last}}{{/headerParams}}], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; -{{/operation}}{{/operations}} -valid_content_headers(Req, State) -> +-spec valid_content_headers(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. +{{#operations}}{{#operation}}valid_content_headers(Req, #state{operation_id = '{{operationId}}'} = State) -> + {true, Req, State}; +{{/operation}}{{/operations}}valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +{{#operations}}{{#operation}}content_types_provided(Req, #state{operation_id = '{{operationId}}'} = State) -> + {{^produces.size}} + {[], Req, State}; + {{/produces.size}} + {{#produces.size}} {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {{#produces}} + {<<"{{mediaType}}">>, handle_type_provided}{{^-last}}{{#produces.size}},{{/produces.size}}{{/-last}} + {{/produces}} + ], Req, State}; + {{/produces.size}} +{{/operation}}{{/operations}}content_types_provided(Req, State) -> + {[], Req, State}. +-spec delete_resource(cowboy_req:req(), state()) -> + {boolean(), cowboy_req:req(), state()}. delete_resource(Req, State) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - 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} + case handle_type_accepted(Req, State) of + true -> + {true, Req, State}; + _ -> + {false, Req, State} end. --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - case {{packageName}}_api:populate_request(OperationID, Req0, ValidatorState) 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 = prepare_body(Code, Body), - Response = {ok, {Code, Headers, PreparedBody}}, - process_response(Response, Req1, State); - {error, Reason, Req1} -> - process_response({error, Reason}, Req1, State) - end. - -validate_headers(_, Req) -> {true, Req}. - -prepare_body(204, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(304, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(_Code, Body) -> - jsx:encode(Body). +-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({{operations.pathPrefix}}, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + {cowboy_req:resp_body(), cowboy_req:req(), {{packageName}}_logic_handler:context()}. +handle_type_provided(Req, #state{operation_id = OperationID, + provide_callback = Handler} = State) -> + Handler({{operations.pathPrefix}}, OperationID, Req, State#state.context). diff --git a/modules/openapi-generator/src/main/resources/erlang-server/logic_handler.mustache b/modules/openapi-generator/src/main/resources/erlang-server/logic_handler.mustache index eb0688e682ce..13fd39434907 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/logic_handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/logic_handler.mustache @@ -1,60 +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}} +-include_lib("kernel/include/logger.hrl"). + +-type api_key_callback() :: + fun(({{packageName}}_api:operation_id(), binary()) -> {true, context()} | {false, iodata()}). +-type accept_callback() :: + fun((atom(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> + boolean() | {created, iodata()} | {see_other, iodata()}). +-type provide_callback() :: + fun((atom(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> + {cowboy_req:resp_body(), cowboy_req:req(), context()}). -type context() :: #{binary() => any()}. --type handler_response() ::{ - Status :: cowboy:http_status(), - Headers :: cowboy:http_headers(), - Body :: jsx:json_term()}. - --export_type([handler_response/0]). - -{{#authMethods}} - {{#isApiKey}} --callback authorize_api_key( - OperationID :: {{packageName}}_api:operation_id(), - ApiKey :: binary() -) -> - Result :: boolean() | {boolean(), context()}. - {{/isApiKey}} -{{/authMethods}} - - --callback handle_request(OperationID :: {{packageName}}_api:operation_id(), cowboy_req:req(), Context :: context()) -> - handler_response(). - --spec handle_request( - Handler :: atom(), - OperationID :: {{packageName}}_api:operation_id(), - Request :: cowboy_req:req(), - Context :: context() -) -> - handler_response(). - -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). - -{{#authMethods}} - {{#isApiKey}} --spec authorize_api_key(Handler :: atom(), OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) -> - Result :: false | {true, context()}. -authorize_api_key(Handler, OperationID, ApiKey) -> - Handler:authorize_api_key(OperationID, ApiKey). - {{/isApiKey}} -{{/authMethods}} -{{^authMethods}} --spec authorize_api_key(Handler :: atom(), OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) -> - Result :: false. -authorize_api_key(_Handler, _OperationID, _ApiKey) -> - false. -{{/authMethods}} + +-export_type([context/0, api_key_callback/0, accept_callback/0, provide_callback/0]). + +-optional_callbacks([api_key_callback/2]). + +-callback api_key_callback({{packageName}}_api:operation_id(), binary()) -> + {true, context()} | {false, iodata()}. + +-callback accept_callback(atom(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> + boolean() | {created, iodata()} | {see_other, iodata()}. + +-callback provide_callback(atom(), {{packageName}}_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({{packageName}}_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(), {{packageName}}_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(), {{packageName}}_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}. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/rebar.config.mustache b/modules/openapi-generator/src/main/resources/erlang-server/rebar.config.mustache index 743b108f384a..50cd482ca39b 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/rebar.config.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/rebar.config.mustache @@ -1,6 +1,15 @@ +{minimum_otp_vsn, "27"}. + {deps, [ - {cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.8.0"}}}, - {rfc3339, {git, "https://github.com/talentdeficit/rfc3339.git", {tag, "master"}}}, - {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "v3.1.0"}}}, - {jesse, {git, "https://github.com/for-GET/jesse.git", {tag, "1.5.6"}}} + {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]}. diff --git a/modules/openapi-generator/src/main/resources/erlang-server/router.mustache b/modules/openapi-generator/src/main/resources/erlang-server/router.mustache index e2efc2206bc1..2bc70bdac73c 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/router.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/router.mustache @@ -1,57 +1,36 @@ -module({{packageName}}_router). --export([get_paths/1, get_validator_state/0]). +-export([get_paths/1]). --type operations() :: #{ - Method :: binary() => {{packageName}}_api:operation_id() -}. - --type init_opts() :: { - Operations :: operations(), - LogicHandler :: atom(), - ValidatorMod :: module() -}. +-type method() :: binary(). +-type operations() :: #{method() => {{packageName}}_api:operation_id()}. +-type init_opts() :: {operations(), module()}. -export_type([init_opts/0]). --spec get_paths(LogicHandler :: atom()) -> [{'_',[{ - Path :: string(), - Handler :: atom(), - InitOpts :: init_opts() -}]}]. - +-spec get_paths(LogicHandler :: module()) -> cowboy_router:routes(). get_paths(LogicHandler) -> - ValidatorState = prepare_validator(), PreparedPaths = maps:fold( - fun(Path, #{operations := Operations, handler := Handler}, Acc) -> - [{Path, Handler, Operations} | Acc] - end, - [], - group_paths() - ), - [ - {'_', - [{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths] - } - ]. + 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() - ). + fun(OperationID, #{path := Path, method := Method, handler := Handler}, Acc) -> + case maps:find(Path, Acc) of + {ok, PathInfo0 = #{operations := Operations0}} -> + Operations = Operations0#{Method => OperationID}, + PathInfo = PathInfo0#{operations => Operations}, + Acc#{Path => PathInfo}; + error -> + Operations = #{Method => OperationID}, + PathInfo = #{handler => Handler, operations => Operations}, + Acc#{Path => PathInfo} + end + end, #{}, get_operations()). get_operations() -> #{ {{#apiInfo}}{{#apis}}{{#operations}}{{#operation}} @@ -61,18 +40,3 @@ get_operations() -> handler => '{{classname}}' }{{^-last}},{{/-last}}{{/operation}}{{^-last}},{{/-last}}{{/operations}}{{/apis}}{{/apiInfo}} }. - -get_validator_state() -> - persistent_term:get({?MODULE, validator_state}). - - -prepare_validator() -> - R = jsx:decode(element(2, file:read_file(get_openapi_path()))), - JesseState = jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]), - persistent_term:put({?MODULE, validator_state}, JesseState), - ?MODULE. - - -get_openapi_path() -> - {ok, AppName} = application:get_application(?MODULE), - filename:join({{packageName}}_utils:priv_dir(AppName), "{{{openAPISpecName}}}.json"). diff --git a/modules/openapi-generator/src/main/resources/erlang-server/server.mustache b/modules/openapi-generator/src/main/resources/erlang-server/server.mustache index cca0af57a84c..43ed54e5d230 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/server.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/server.mustache @@ -1,26 +1,21 @@ -module({{packageName}}_server). - --define(DEFAULT_LOGIC_HANDLER, {{packageName}}_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, {{packageName}}_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), +-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), - ExtraOpts = maps:get(cowboy_extra_opts, Params, []), - CowboyOpts = get_cowboy_config(LogicHandler, ExtraOpts), + CowboyOpts = get_cowboy_config(LogicHandler, ProtocolOpts), case Transport of ssl -> cowboy:start_tls(ID, TransportOpts, CowboyOpts); @@ -28,33 +23,17 @@ start(ID, #{ 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; + DefaultOpts = get_default_opts(LogicHandler), + maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts). -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_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 = {{packageName}}_router:get_paths(LogicHandler), @@ -62,6 +41,3 @@ get_default_dispatch(LogicHandler) -> get_default_opts(LogicHandler) -> #{env => get_default_dispatch(LogicHandler)}. - -store_key(Key, Value, Opts) -> - maps:put(Key, Value, Opts). diff --git a/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache b/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache deleted file mode 100644 index b6701add7fc2..000000000000 --- a/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache +++ /dev/null @@ -1,173 +0,0 @@ --module({{packageName}}_utils). - --export([to_binary/1]). --export([to_list/1]). --export([to_float/1]). --export([to_int/1]). --export([to_lower/1]). --export([to_upper/1]). --export([set_resp_headers/2]). --export([to_header/1]). --export([to_qs/1]). --export([to_binding/1]). --export([get_opt/2]). --export([get_opt/3]). --export([priv_dir/0]). --export([priv_dir/1]). --export([priv_path/1]). - - --spec to_binary(iodata() | atom() | number()) -> binary(). - -to_binary(V) when is_binary(V) -> V; -to_binary(V) when is_list(V) -> iolist_to_binary(V); -to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8); -to_binary(V) when is_integer(V) -> integer_to_binary(V); -to_binary(V) when is_float(V) -> float_to_binary(V). - --spec to_list(iodata() | atom() | number()) -> string(). - -to_list(V) when is_list(V) -> V; -to_list(V) -> binary_to_list(to_binary(V)). - --spec to_float(iodata()) -> number(). - -to_float(V) -> - Data = iolist_to_binary([V]), - case binary:split(Data, <<$.>>) of - [Data] -> - binary_to_integer(Data); - [<<>>, _] -> - binary_to_float(<<$0, Data/binary>>); - _ -> - binary_to_float(Data) - end. - -%% - --spec to_int(integer() | binary() | list()) -> integer(). - -to_int(Data) when is_integer(Data) -> - Data; -to_int(Data) when is_binary(Data) -> - binary_to_integer(Data); -to_int(Data) when is_list(Data) -> - list_to_integer(Data). - --spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req(). - -set_resp_headers([], Req) -> - Req; -set_resp_headers([{K, V} | T], Req0) -> - Req = cowboy_req:set_resp_header(K, V, Req0), - set_resp_headers(T, Req). - --spec to_header(iodata() | atom() | number()) -> binary(). - -to_header(Name) -> - Prepared = to_binary(Name), - to_lower(Prepared). - --spec to_qs(iodata() | atom() | number()) -> binary(). - -to_qs(Name) -> - to_binary(Name). - --spec to_binding(iodata() | atom() | number()) -> atom(). - -to_binding(Name) -> - Prepared = to_binary(Name), - binary_to_atom(Prepared, utf8). - --spec get_opt(any(), []) -> any(). - -get_opt(Key, Opts) -> - get_opt(Key, Opts, undefined). - --spec get_opt(any(), [], any()) -> any(). - -get_opt(Key, Opts, Default) -> - case lists:keyfind(Key, 1, Opts) of - {_, Value} -> Value; - false -> Default - end. - --spec priv_dir() -> file:filename(). - -priv_dir() -> - {ok, AppName} = application:get_application(), - priv_dir(AppName). - --spec priv_dir(Application :: atom()) -> file:filename(). - -priv_dir(AppName) -> - case code:priv_dir(AppName) of - Value when is_list(Value) -> - Value ++ "/"; - _Error -> - select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"]) - end. - --spec priv_path(Relative :: file:filename()) -> file:filename(). - -priv_path(Relative) -> - filename:join(priv_dir(), Relative). - --include_lib("kernel/include/file.hrl"). - -select_priv_dir(Paths) -> - case lists:dropwhile(fun test_priv_dir/1, Paths) of - [Path | _] -> Path; - _ -> exit(no_priv_dir) - end. - -test_priv_dir(Path) -> - case file:read_file_info(Path) of - {ok, #file_info{type = directory}} -> - false; - _ -> - true - end. - - -%% - --spec to_lower(binary()) -> binary(). - -to_lower(S) -> - to_case(lower, S, <<>>). - --spec to_upper(binary()) -> binary(). - -to_upper(S) -> - to_case(upper, S, <<>>). - -to_case(_Case, <<>>, Acc) -> - Acc; - -to_case(_Case, <>, _Acc) when C > 127 -> - error(badarg); - -to_case(Case = lower, <>, Acc) -> - to_case(Case, Rest, <>); - -to_case(Case = upper, <>, Acc) -> - to_case(Case, Rest, <>). - -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. diff --git a/samples/server/echo_api/erlang-server/.openapi-generator-ignore b/samples/server/echo_api/erlang-server/.openapi-generator-ignore new file mode 100644 index 000000000000..7484ee590a38 --- /dev/null +++ b/samples/server/echo_api/erlang-server/.openapi-generator-ignore @@ -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 diff --git a/samples/server/echo_api/erlang-server/.openapi-generator/FILES b/samples/server/echo_api/erlang-server/.openapi-generator/FILES new file mode 100644 index 000000000000..3aa7956b7dab --- /dev/null +++ b/samples/server/echo_api/erlang-server/.openapi-generator/FILES @@ -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 diff --git a/samples/server/echo_api/erlang-server/.openapi-generator/VERSION b/samples/server/echo_api/erlang-server/.openapi-generator/VERSION new file mode 100644 index 000000000000..17f2442ff3bc --- /dev/null +++ b/samples/server/echo_api/erlang-server/.openapi-generator/VERSION @@ -0,0 +1 @@ +7.9.0-SNAPSHOT diff --git a/samples/server/echo_api/erlang-server/README.md b/samples/server/echo_api/erlang-server/README.md new file mode 100644 index 000000000000..887f730062fe --- /dev/null +++ b/samples/server/echo_api/erlang-server/README.md @@ -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. diff --git a/samples/server/echo_api/erlang-server/pom.xml b/samples/server/echo_api/erlang-server/pom.xml new file mode 100644 index 000000000000..69d236d7cda2 --- /dev/null +++ b/samples/server/echo_api/erlang-server/pom.xml @@ -0,0 +1,46 @@ + + 4.0.0 + org.openapitools + ErlangServerEchoTests + pom + 1.0-SNAPSHOT + Erlang Echo Server + + + + maven-dependency-plugin + + + package + + copy-dependencies + + + ${project.build.directory} + + + + + + org.codehaus.mojo + exec-maven-plugin + 1.6.0 + + + compile-test + integration-test + + exec + + + rebar3 + + compile + + + + + + + + diff --git a/samples/server/echo_api/erlang-server/priv/openapi.json b/samples/server/echo_api/erlang-server/priv/openapi.json new file mode 100644 index 000000000000..be217ad12e5d --- /dev/null +++ b/samples/server/echo_api/erlang-server/priv/openapi.json @@ -0,0 +1,1282 @@ +{ + "openapi" : "3.0.3", + "info" : { + "contact" : { + "email" : "team@openapitools.org" + }, + "description" : "Echo Server API", + "license" : { + "name" : "Apache 2.0", + "url" : "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "title" : "Echo Server API", + "version" : "0.1.0" + }, + "servers" : [ { + "url" : "http://localhost:3000/" + } ], + "paths" : { + "/path/string/{path_string}/integer/{path_integer}/{enum_nonref_string_path}/{enum_ref_string_path}" : { + "get" : { + "description" : "Test path parameter(s)", + "operationId" : "tests/path/string/{path_string}/integer/{path_integer}/{enum_nonref_string_path}/{enum_ref_string_path}", + "parameters" : [ { + "explode" : false, + "in" : "path", + "name" : "path_string", + "required" : true, + "schema" : { + "type" : "string" + }, + "style" : "simple" + }, { + "explode" : false, + "in" : "path", + "name" : "path_integer", + "required" : true, + "schema" : { + "type" : "integer" + }, + "style" : "simple" + }, { + "explode" : false, + "in" : "path", + "name" : "enum_nonref_string_path", + "required" : true, + "schema" : { + "enum" : [ "success", "failure", "unclassified" ], + "type" : "string" + }, + "style" : "simple" + }, { + "explode" : false, + "in" : "path", + "name" : "enum_ref_string_path", + "required" : true, + "schema" : { + "$ref" : "#/components/schemas/StringEnumRef" + }, + "style" : "simple" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test path parameter(s)", + "tags" : [ "path" ] + } + }, + "/form/integer/boolean/string" : { + "post" : { + "description" : "Test form parameter(s)", + "operationId" : "test/form/integer/boolean/string", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/test_form_integer_boolean_string_request" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test form parameter(s)", + "tags" : [ "form" ] + } + }, + "/form/oneof" : { + "post" : { + "description" : "Test form parameter(s) for oneOf schema", + "operationId" : "test/form/oneof", + "requestBody" : { + "content" : { + "application/x-www-form-urlencoded" : { + "schema" : { + "$ref" : "#/components/schemas/test_form_oneof_request" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test form parameter(s) for oneOf schema", + "tags" : [ "form" ] + } + }, + "/form/object/multipart" : { + "post" : { + "description" : "Test form parameter(s) for multipart schema", + "operationId" : "test/form/object/multipart", + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "$ref" : "#/components/schemas/test_form_object_multipart_request" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test form parameter(s) for multipart schema", + "tags" : [ "form" ] + } + }, + "/header/integer/boolean/string/enums" : { + "get" : { + "description" : "Test header parameter(s)", + "operationId" : "test/header/integer/boolean/string/enums", + "parameters" : [ { + "explode" : true, + "in" : "header", + "name" : "integer_header", + "required" : false, + "schema" : { + "type" : "integer" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "header", + "name" : "boolean_header", + "required" : false, + "schema" : { + "type" : "boolean" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "header", + "name" : "string_header", + "required" : false, + "schema" : { + "type" : "string" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "header", + "name" : "enum_nonref_string_header", + "required" : false, + "schema" : { + "enum" : [ "success", "failure", "unclassified" ], + "type" : "string" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "header", + "name" : "enum_ref_string_header", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/StringEnumRef" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test header parameter(s)", + "tags" : [ "header" ] + } + }, + "/query/enum_ref_string" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/enum_ref_string", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "enum_nonref_string_query", + "required" : false, + "schema" : { + "enum" : [ "success", "failure", "unclassified" ], + "type" : "string" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "query", + "name" : "enum_ref_string_query", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/StringEnumRef" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/datetime/date/string" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/datetime/date/string", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "datetime_query", + "required" : false, + "schema" : { + "format" : "date-time", + "type" : "string" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "query", + "name" : "date_query", + "required" : false, + "schema" : { + "format" : "date", + "type" : "string" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "query", + "name" : "string_query", + "required" : false, + "schema" : { + "type" : "string" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/integer/boolean/string" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/integer/boolean/string", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "integer_query", + "required" : false, + "schema" : { + "type" : "integer" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "query", + "name" : "boolean_query", + "required" : false, + "schema" : { + "type" : "boolean" + }, + "style" : "form" + }, { + "explode" : true, + "in" : "query", + "name" : "string_query", + "required" : false, + "schema" : { + "type" : "string" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_form/explode_true/array_string" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_form/explode_true/array_string", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/test_query_style_form_explode_true_array_string_query_object_parameter" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_form/explode_false/array_integer" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_form/explode_false/array_integer", + "parameters" : [ { + "explode" : false, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "items" : { + "type" : "integer" + }, + "type" : "array" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_form/explode_false/array_string" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_form/explode_false/array_string", + "parameters" : [ { + "explode" : false, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_form/explode_true/object" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_form/explode_true/object", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/Pet" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_form/explode_true/object/allOf" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_form/explode_true/object/allOf", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/DataQuery" + }, + "style" : "form" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_deepObject/explode_true/object" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_deepObject/explode_true/object", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/Pet" + }, + "style" : "deepObject" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/query/style_deepObject/explode_true/object/allOf" : { + "get" : { + "description" : "Test query parameter(s)", + "operationId" : "test/query/style_deepObject/explode_true/object/allOf", + "parameters" : [ { + "explode" : true, + "in" : "query", + "name" : "query_object", + "required" : false, + "schema" : { + "$ref" : "#/components/schemas/test_query_style_deepObject_explode_true_object_allOf_query_object_parameter" + }, + "style" : "deepObject" + } ], + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test query parameter(s)", + "tags" : [ "query" ] + } + }, + "/body/application/octetstream/binary" : { + "post" : { + "description" : "Test body parameter(s)", + "operationId" : "test/body/application/octetstream/binary", + "requestBody" : { + "content" : { + "application/octet-stream" : { + "schema" : { + "format" : "binary", + "type" : "string" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test body parameter(s)", + "tags" : [ "body" ] + } + }, + "/echo/body/Pet" : { + "post" : { + "description" : "Test body parameter(s)", + "operationId" : "test/echo/body/Pet", + "requestBody" : { + "$ref" : "#/components/requestBodies/Pet" + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Pet" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test body parameter(s)", + "tags" : [ "body" ] + } + }, + "/echo/body/allOf/Pet" : { + "post" : { + "description" : "Test body parameter(s)", + "operationId" : "test/echo/body/allOf/Pet", + "requestBody" : { + "$ref" : "#/components/requestBodies/AllOfPet" + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Pet" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test body parameter(s)", + "tags" : [ "body" ] + } + }, + "/echo/body/Pet/response_string" : { + "post" : { + "description" : "Test empty response body", + "operationId" : "test/echo/body/Pet/response_string", + "requestBody" : { + "$ref" : "#/components/requestBodies/Pet" + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test empty response body", + "tags" : [ "body" ] + } + }, + "/echo/body/Tag/response_string" : { + "post" : { + "description" : "Test empty json (request body)", + "operationId" : "test/echo/body/Tag/response_string", + "requestBody" : { + "$ref" : "#/components/requestBodies/Tag" + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test empty json (request body)", + "tags" : [ "body" ] + } + }, + "/echo/body/FreeFormObject/response_string" : { + "post" : { + "description" : "Test free form object", + "operationId" : "test/echo/body/FreeFormObject/response_string", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "type" : "object" + } + } + }, + "description" : "Free form object" + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test free form object", + "tags" : [ "body" ] + } + }, + "/echo/body/string_enum" : { + "post" : { + "description" : "Test string enum response body", + "operationId" : "test/echo/body/string_enum", + "requestBody" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/StringEnumRef" + } + } + }, + "description" : "String enum" + }, + "responses" : { + "200" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/StringEnumRef" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test string enum response body", + "tags" : [ "body" ] + } + }, + "/binary/gif" : { + "post" : { + "description" : "Test binary (gif) response body", + "operationId" : "test/binary/gif", + "responses" : { + "200" : { + "content" : { + "image/gif" : { + "schema" : { + "format" : "binary", + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test binary (gif) response body", + "tags" : [ "body" ] + } + }, + "/body/application/octetstream/single_binary" : { + "post" : { + "description" : "Test single binary in multipart mime", + "operationId" : "test/body/multipart/formdata/single_binary", + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "$ref" : "#/components/schemas/test_body_multipart_formdata_single_binary_request" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test single binary in multipart mime", + "tags" : [ "body" ] + } + }, + "/body/application/octetstream/array_of_binary" : { + "post" : { + "description" : "Test array of binary in multipart mime", + "operationId" : "test/body/multipart/formdata/array_of_binary", + "requestBody" : { + "content" : { + "multipart/form-data" : { + "schema" : { + "$ref" : "#/components/schemas/test_body_multipart_formdata_array_of_binary_request" + } + } + } + }, + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "summary" : "Test array of binary in multipart mime", + "tags" : [ "body" ] + } + }, + "/auth/http/basic" : { + "post" : { + "description" : "To test HTTP basic authentication", + "operationId" : "test/auth/http/basic", + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "security" : [ { + "http_auth" : [ ] + } ], + "summary" : "To test HTTP basic authentication", + "tags" : [ "auth" ] + } + }, + "/auth/http/bearer" : { + "post" : { + "description" : "To test HTTP bearer authentication", + "operationId" : "test/auth/http/bearer", + "responses" : { + "200" : { + "content" : { + "text/plain" : { + "schema" : { + "type" : "string" + } + } + }, + "description" : "Successful operation" + } + }, + "security" : [ { + "http_bearer_auth" : [ ] + } ], + "summary" : "To test HTTP bearer authentication", + "tags" : [ "auth" ] + } + } + }, + "components" : { + "requestBodies" : { + "Pet" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Pet" + } + } + }, + "description" : "Pet object that needs to be added to the store" + }, + "AllOfPet" : { + "content" : { + "application/json" : { + "schema" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Pet" + } ] + } + } + }, + "description" : "Pet object that needs to be added to the store" + }, + "Tag" : { + "content" : { + "application/json" : { + "schema" : { + "$ref" : "#/components/schemas/Tag" + } + } + }, + "description" : "Tag object" + } + }, + "schemas" : { + "Category" : { + "example" : { + "name" : "Dogs", + "id" : 1 + }, + "properties" : { + "id" : { + "example" : 1, + "format" : "int64", + "type" : "integer" + }, + "name" : { + "example" : "Dogs", + "type" : "string" + } + }, + "type" : "object", + "xml" : { + "name" : "category" + } + }, + "Tag" : { + "example" : { + "name" : "name", + "id" : 0 + }, + "properties" : { + "id" : { + "format" : "int64", + "type" : "integer" + }, + "name" : { + "type" : "string" + } + }, + "type" : "object", + "xml" : { + "name" : "tag" + } + }, + "Pet" : { + "example" : { + "photoUrls" : [ "photoUrls", "photoUrls" ], + "name" : "doggie", + "id" : 10, + "category" : { + "name" : "Dogs", + "id" : 1 + }, + "tags" : [ { + "name" : "name", + "id" : 0 + }, { + "name" : "name", + "id" : 0 + } ], + "status" : "available" + }, + "properties" : { + "id" : { + "example" : 10, + "format" : "int64", + "type" : "integer" + }, + "name" : { + "example" : "doggie", + "type" : "string" + }, + "category" : { + "$ref" : "#/components/schemas/Category" + }, + "photoUrls" : { + "items" : { + "type" : "string", + "xml" : { + "name" : "photoUrl" + } + }, + "type" : "array", + "xml" : { + "wrapped" : true + } + }, + "tags" : { + "items" : { + "$ref" : "#/components/schemas/Tag" + }, + "type" : "array", + "xml" : { + "wrapped" : true + } + }, + "status" : { + "description" : "pet status in the store", + "enum" : [ "available", "pending", "sold" ], + "type" : "string" + } + }, + "required" : [ "name", "photoUrls" ], + "type" : "object", + "xml" : { + "name" : "pet" + } + }, + "StringEnumRef" : { + "enum" : [ "success", "failure", "unclassified" ], + "type" : "string" + }, + "DefaultValue" : { + "description" : "to test the default value of properties", + "properties" : { + "array_string_enum_ref_default" : { + "default" : [ "success", "failure" ], + "items" : { + "$ref" : "#/components/schemas/StringEnumRef" + }, + "type" : "array" + }, + "array_string_enum_default" : { + "default" : [ "success", "failure" ], + "items" : { + "enum" : [ "success", "failure", "unclassified" ], + "type" : "string" + }, + "type" : "array" + }, + "array_string_default" : { + "default" : [ "failure", "skipped" ], + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "array_integer_default" : { + "default" : [ 1, 3 ], + "items" : { + "type" : "integer" + }, + "type" : "array" + }, + "array_string" : { + "items" : { + "type" : "string" + }, + "type" : "array" + }, + "array_string_nullable" : { + "items" : { + "type" : "string" + }, + "nullable" : true, + "type" : "array" + }, + "array_string_extension_nullable" : { + "items" : { + "type" : "string" + }, + "type" : "array", + "x-nullable" : true + }, + "string_nullable" : { + "nullable" : true, + "type" : "string" + } + }, + "type" : "object" + }, + "Bird" : { + "properties" : { + "size" : { + "type" : "string" + }, + "color" : { + "type" : "string" + } + }, + "type" : "object" + }, + "Query" : { + "properties" : { + "id" : { + "description" : "Query", + "format" : "int64", + "type" : "integer" + }, + "outcomes" : { + "default" : [ "SUCCESS", "FAILURE" ], + "items" : { + "enum" : [ "SUCCESS", "FAILURE", "SKIPPED" ], + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object", + "x-parent" : true + }, + "DataQuery" : { + "allOf" : [ { + "properties" : { + "suffix" : { + "description" : "test suffix", + "type" : "string" + }, + "text" : { + "description" : "Some text containing white spaces", + "example" : "Some text", + "type" : "string" + }, + "date" : { + "description" : "A date", + "format" : "date-time", + "type" : "string" + } + }, + "type" : "object" + }, { + "$ref" : "#/components/schemas/Query" + } ] + }, + "NumberPropertiesOnly" : { + "properties" : { + "number" : { + "type" : "number" + }, + "float" : { + "format" : "float", + "type" : "number" + }, + "double" : { + "format" : "double", + "maximum" : 50.2, + "minimum" : 0.8, + "type" : "number" + } + }, + "type" : "object" + }, + "test_form_integer_boolean_string_request" : { + "properties" : { + "integer_form" : { + "type" : "integer" + }, + "boolean_form" : { + "type" : "boolean" + }, + "string_form" : { + "type" : "string" + } + }, + "type" : "object" + }, + "test_form_oneof_request_oneOf" : { + "properties" : { + "form1" : { + "type" : "string" + }, + "form2" : { + "type" : "integer" + } + }, + "type" : "object" + }, + "test_form_oneof_request_oneOf_1" : { + "properties" : { + "form3" : { + "type" : "string" + }, + "form4" : { + "type" : "boolean" + } + }, + "type" : "object" + }, + "test_form_oneof_request" : { + "oneOf" : [ { + "$ref" : "#/components/schemas/test_form_oneof_request_oneOf" + }, { + "$ref" : "#/components/schemas/test_form_oneof_request_oneOf_1" + }, { + "$ref" : "#/components/schemas/Tag" + } ], + "type" : "object" + }, + "test_form_object_multipart_request_marker" : { + "properties" : { + "name" : { + "type" : "string" + } + }, + "type" : "object" + }, + "test_form_object_multipart_request" : { + "properties" : { + "marker" : { + "$ref" : "#/components/schemas/test_form_object_multipart_request_marker" + } + }, + "required" : [ "marker" ], + "type" : "object" + }, + "test_query_style_form_explode_true_array_string_query_object_parameter" : { + "properties" : { + "values" : { + "items" : { + "type" : "string" + }, + "type" : "array" + } + }, + "type" : "object" + }, + "test_query_style_deepObject_explode_true_object_allOf_query_object_parameter" : { + "allOf" : [ { + "$ref" : "#/components/schemas/Bird" + }, { + "$ref" : "#/components/schemas/Category" + } ] + }, + "test_body_multipart_formdata_single_binary_request" : { + "properties" : { + "my-file" : { + "format" : "binary", + "type" : "string" + } + }, + "type" : "object" + }, + "test_body_multipart_formdata_array_of_binary_request" : { + "properties" : { + "files" : { + "items" : { + "format" : "binary", + "type" : "string" + }, + "type" : "array" + } + }, + "required" : [ "files" ], + "type" : "object" + } + }, + "securitySchemes" : { + "http_auth" : { + "scheme" : "basic", + "type" : "http" + }, + "http_bearer_auth" : { + "scheme" : "bearer", + "type" : "http" + } + } + } +} diff --git a/samples/server/echo_api/erlang-server/rebar.config b/samples/server/echo_api/erlang-server/rebar.config new file mode 100644 index 000000000000..50cd482ca39b --- /dev/null +++ b/samples/server/echo_api/erlang-server/rebar.config @@ -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]}. diff --git a/samples/server/echo_api/erlang-server/src/openapi.app.src b/samples/server/echo_api/erlang-server/src/openapi.app.src new file mode 100644 index 000000000000..95507ce40e9c --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi.app.src @@ -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, []}]}. diff --git a/samples/server/echo_api/erlang-server/src/openapi_api.erl b/samples/server/echo_api/erlang-server/src/openapi_api.erl new file mode 100644 index 000000000000..b796f2df4a36 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_api.erl @@ -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. diff --git a/samples/server/echo_api/erlang-server/src/openapi_auth.erl b/samples/server/echo_api/erlang-server/src/openapi_auth.erl new file mode 100644 index 000000000000..0e7beb1132c4 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_auth.erl @@ -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. diff --git a/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl new file mode 100644 index 000000000000..9b8e9b18352f --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl new file mode 100644 index 000000000000..de4fd624e10f --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl new file mode 100644 index 000000000000..2d6fff400073 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl new file mode 100644 index 000000000000..8077a9ca8826 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl new file mode 100644 index 000000000000..28d45ab65113 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl @@ -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}. diff --git a/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl new file mode 100644 index 000000000000..0d08034f3932 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl new file mode 100644 index 000000000000..5abfddcf7826 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl @@ -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). diff --git a/samples/server/echo_api/erlang-server/src/openapi_router.erl b/samples/server/echo_api/erlang-server/src/openapi_router.erl new file mode 100644 index 000000000000..84cee5a6256d --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_router.erl @@ -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' + } + }. diff --git a/samples/server/echo_api/erlang-server/src/openapi_server.erl b/samples/server/echo_api/erlang-server/src/openapi_server.erl new file mode 100644 index 000000000000..0cd992fe69d2 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_server.erl @@ -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)}. diff --git a/samples/server/petstore/erlang-server/.openapi-generator/FILES b/samples/server/petstore/erlang-server/.openapi-generator/FILES index 9c50202b9359..ac7922db6682 100644 --- a/samples/server/petstore/erlang-server/.openapi-generator/FILES +++ b/samples/server/petstore/erlang-server/.openapi-generator/FILES @@ -4,11 +4,9 @@ rebar.config src/openapi.app.src src/openapi_api.erl src/openapi_auth.erl -src/openapi_default_logic_handler.erl src/openapi_logic_handler.erl src/openapi_pet_handler.erl src/openapi_router.erl src/openapi_server.erl src/openapi_store_handler.erl src/openapi_user_handler.erl -src/openapi_utils.erl diff --git a/samples/server/petstore/erlang-server/README.md b/samples/server/petstore/erlang-server/README.md index 5d36ca07576b..887f730062fe 100644 --- a/samples/server/petstore/erlang-server/README.md +++ b/samples/server/petstore/erlang-server/README.md @@ -4,54 +4,33 @@ An Erlang server stub generated by [OpenAPI Generator](https://openapi-generator.tech) given an OpenAPI spec. -Dependency: [Cowboy](https://github.com/ninenines/cowboy) +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 -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 +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 - 4, Copy erlang-server file to http_server project,Don't forget the 'priv' folder. + 3, Copy erlang-server file to http_server project, and don't forget the 'priv' folder. - 5, Start in the http_server project: + 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, net_opts=>[]}) - 2, Compilation http_server project - $ make + 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 - $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 + $ 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. diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index 743b108f384a..50cd482ca39b 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -1,6 +1,15 @@ +{minimum_otp_vsn, "27"}. + {deps, [ - {cowboy, {git, "https://github.com/ninenines/cowboy.git", {tag, "2.8.0"}}}, - {rfc3339, {git, "https://github.com/talentdeficit/rfc3339.git", {tag, "master"}}}, - {jsx, {git, "https://github.com/talentdeficit/jsx.git", {tag, "v3.1.0"}}}, - {jesse, {git, "https://github.com/for-GET/jesse.git", {tag, "1.5.6"}}} + {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]}. diff --git a/samples/server/petstore/erlang-server/src/openapi.app.src b/samples/server/petstore/erlang-server/src/openapi.app.src index 99859823b68e..0172627719b7 100644 --- a/samples/server/petstore/erlang-server/src/openapi.app.src +++ b/samples/server/petstore/erlang-server/src/openapi.app.src @@ -1,19 +1,11 @@ -{application, openapi, [ - {description, "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters."}, - {vsn, "1.0.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib, - ssl, - inets, - jsx, - jesse, - cowboy - ]}, - {env, [ - ]}, - {modules, []}, - {licenses, ["Apache-2.0"]}, - {links, []} -]}. +{application, + openapi, + [{description, + "This is a sample server Petstore server. For this sample, you can use the api key `special-key` to test the authorization filters."}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, [kernel, stdlib, public_key, ssl, inets, ranch, cowboy]}, + {env, []}, + {modules, []}, + {licenses, ["Apache-2.0"]}, + {links, []}]}. diff --git a/samples/server/petstore/erlang-server/src/openapi_api.erl b/samples/server/petstore/erlang-server/src/openapi_api.erl index 8b9cb84eaa49..16ad18e97823 100644 --- a/samples/server/petstore/erlang-server/src/openapi_api.erl +++ b/samples/server/petstore/erlang-server/src/openapi_api.erl @@ -1,404 +1,491 @@ -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]). --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]). +-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]). --spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. +-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('AddPet', 200, Body, ValidatorState) -> + validate_response_body('Pet', 'Pet', Body, ValidatorState); +validate_response('AddPet', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeletePet', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('FindPetsByStatus', 200, Body, ValidatorState) -> + validate_response_body('list', 'Pet', Body, ValidatorState); +validate_response('FindPetsByStatus', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('FindPetsByTags', 200, Body, ValidatorState) -> + validate_response_body('list', 'Pet', Body, ValidatorState); +validate_response('FindPetsByTags', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetPetById', 200, Body, ValidatorState) -> + validate_response_body('Pet', 'Pet', Body, ValidatorState); +validate_response('GetPetById', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetPetById', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePet', 200, Body, ValidatorState) -> + validate_response_body('Pet', 'Pet', Body, ValidatorState); +validate_response('UpdatePet', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePet', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePet', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdatePetWithForm', 405, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UploadFile', 200, Body, ValidatorState) -> + validate_response_body('ApiResponse', 'ApiResponse', Body, ValidatorState); +validate_response('DeleteOrder', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeleteOrder', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetInventory', 200, Body, ValidatorState) -> + validate_response_body('map', 'integer', Body, ValidatorState); +validate_response('GetOrderById', 200, Body, ValidatorState) -> + validate_response_body('Order', 'Order', Body, ValidatorState); +validate_response('GetOrderById', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetOrderById', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('PlaceOrder', 200, Body, ValidatorState) -> + validate_response_body('Order', 'Order', Body, ValidatorState); +validate_response('PlaceOrder', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('CreateUser', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('CreateUsersWithArrayInput', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('CreateUsersWithListInput', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeleteUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('DeleteUser', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetUserByName', 200, Body, ValidatorState) -> + validate_response_body('User', 'User', Body, ValidatorState); +validate_response('GetUserByName', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('GetUserByName', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('LoginUser', 200, Body, ValidatorState) -> + validate_response_body('binary', 'string', Body, ValidatorState); +validate_response('LoginUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('LogoutUser', 0, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdateUser', 400, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response('UpdateUser', 404, Body, ValidatorState) -> + validate_response_body('', '', Body, ValidatorState); +validate_response(_OperationID, _Code, _Body, _ValidatorState) -> + ok. + +%%% +-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. request_params('AddPet') -> [ 'Pet' ]; - request_params('DeletePet') -> [ 'petId', 'api_key' ]; - request_params('FindPetsByStatus') -> [ 'status' ]; - request_params('FindPetsByTags') -> [ 'tags' ]; - request_params('GetPetById') -> [ 'petId' ]; - request_params('UpdatePet') -> [ 'Pet' ]; - request_params('UpdatePetWithForm') -> [ 'petId', 'name', 'status' ]; - request_params('UploadFile') -> [ 'petId', 'additionalMetadata', 'file' ]; - - request_params('DeleteOrder') -> [ 'orderId' ]; - request_params('GetInventory') -> [ ]; - request_params('GetOrderById') -> [ 'orderId' ]; - request_params('PlaceOrder') -> [ 'Order' ]; - - request_params('CreateUser') -> [ 'User' ]; - request_params('CreateUsersWithArrayInput') -> [ 'list' ]; - request_params('CreateUsersWithListInput') -> [ 'list' ]; - request_params('DeleteUser') -> [ 'username' ]; - request_params('GetUserByName') -> [ 'username' ]; - request_params('LoginUser') -> [ 'username', 'password' ]; - request_params('LogoutUser') -> [ ]; - request_params('UpdateUser') -> [ 'username', 'User' ]; - 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()] -}. - - - +-spec request_param_info(OperationID :: operation_id(), Name :: request_param()) -> + #{source => qs_val | binding | header | body, rules => [rule()]}. request_param_info('AddPet', 'Pet') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - request_param_info('DeletePet', 'petId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, + {type, integer}, required ] }; - request_param_info('DeletePet', 'api_key') -> #{ - source => header, + source => header, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('FindPetsByStatus', 'status') -> #{ - source => qs_val , + source => qs_val, rules => [ {enum, ['available', 'pending', 'sold'] }, required ] }; - request_param_info('FindPetsByTags', 'tags') -> #{ - source => qs_val , + source => qs_val, rules => [ required ] }; - request_param_info('GetPetById', 'petId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, + {type, integer}, required ] }; - request_param_info('UpdatePet', 'Pet') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - request_param_info('UpdatePetWithForm', 'petId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, + {type, integer}, required ] }; - request_param_info('UpdatePetWithForm', 'name') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('UpdatePetWithForm', 'status') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('UploadFile', 'petId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, + {type, integer}, required ] }; - request_param_info('UploadFile', 'additionalMetadata') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('UploadFile', 'file') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - - request_param_info('DeleteOrder', 'orderId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('GetOrderById', 'orderId') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, - {max, 5 }, - {min, 1 }, + {type, integer}, + {max, 5}, + {min, 1}, required ] }; - request_param_info('PlaceOrder', 'Order') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - - request_param_info('CreateUser', 'User') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - request_param_info('CreateUsersWithArrayInput', 'list') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - request_param_info('CreateUsersWithListInput', 'list') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - request_param_info('DeleteUser', 'username') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('GetUserByName', 'username') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('LoginUser', 'username') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'binary'}, - {pattern, "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$" }, + {type, binary}, + {pattern, "^[a-zA-Z0-9]+[a-zA-Z0-9\\.\\-_]*[a-zA-Z0-9]+$"}, required ] }; - request_param_info('LoginUser', 'password') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('UpdateUser', 'username') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('UpdateUser', 'User') -> #{ - source => body, + source => body, rules => [ schema, required ] }; - 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} -> @@ -420,115 +507,9 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) -> end end. --spec validate_response( - OperationID :: operation_id(), - Code :: 200..599, - Body :: jesse:json_term(), - ValidatorState :: jesse_state:state() -) -> ok | no_return(). - - -validate_response('AddPet', 200, Body, ValidatorState) -> - validate_response_body('Pet', 'Pet', Body, ValidatorState); -validate_response('AddPet', 405, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('DeletePet', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('FindPetsByStatus', 200, Body, ValidatorState) -> - validate_response_body('list', 'Pet', Body, ValidatorState); -validate_response('FindPetsByStatus', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('FindPetsByTags', 200, Body, ValidatorState) -> - validate_response_body('list', 'Pet', Body, ValidatorState); -validate_response('FindPetsByTags', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); +-include_lib("kernel/include/logger.hrl"). -validate_response('GetPetById', 200, Body, ValidatorState) -> - validate_response_body('Pet', 'Pet', Body, ValidatorState); -validate_response('GetPetById', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('GetPetById', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('UpdatePet', 200, Body, ValidatorState) -> - validate_response_body('Pet', 'Pet', Body, ValidatorState); -validate_response('UpdatePet', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('UpdatePet', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('UpdatePet', 405, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('UpdatePetWithForm', 405, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('UploadFile', 200, Body, ValidatorState) -> - validate_response_body('ApiResponse', 'ApiResponse', Body, ValidatorState); - - -validate_response('DeleteOrder', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('DeleteOrder', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('GetInventory', 200, Body, ValidatorState) -> - validate_response_body('map', 'integer', Body, ValidatorState); - -validate_response('GetOrderById', 200, Body, ValidatorState) -> - validate_response_body('Order', 'Order', Body, ValidatorState); -validate_response('GetOrderById', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('GetOrderById', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('PlaceOrder', 200, Body, ValidatorState) -> - validate_response_body('Order', 'Order', Body, ValidatorState); -validate_response('PlaceOrder', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - - -validate_response('CreateUser', 0, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('CreateUsersWithArrayInput', 0, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('CreateUsersWithListInput', 0, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('DeleteUser', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('DeleteUser', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('GetUserByName', 200, Body, ValidatorState) -> - validate_response_body('User', 'User', Body, ValidatorState); -validate_response('GetUserByName', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('GetUserByName', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('LoginUser', 200, Body, ValidatorState) -> - validate_response_body('binary', 'string', Body, ValidatorState); -validate_response('LoginUser', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('LogoutUser', 0, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - -validate_response('UpdateUser', 400, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); -validate_response('UpdateUser', 404, Body, ValidatorState) -> - validate_response_body('', '', Body, ValidatorState); - - -validate_response(_OperationID, _Code, _Body, _ValidatorState) -> - ok. - -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ validate(schema, ReturnBaseType, Item, ValidatorState) || Item <- Body]; @@ -536,45 +517,37 @@ validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> 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) -> +validate(Rule = {type, integer}, Name, Value, _ValidatorState) -> try - {ok, openapi_utils:to_int(Value)} + {ok, to_int(Value)} catch error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, float}, Name, Value, _ValidatorState) -> try - {ok, openapi_utils:to_float(Value)} + {ok, to_float(Value)} catch error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) -> +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) -> +validate(_Rule = {type, boolean}, _Name, Value, _ValidatorState) when is_boolean(Value) -> {ok, Value}; - -validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> +validate(Rule = {type, boolean}, Name, Value, _ValidatorState) -> V = binary_to_lower(Value), try case binary_to_existing_atom(V, utf8) of @@ -585,19 +558,16 @@ validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> error:badarg -> validation_error(Rule, Name) end; - -validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) -> +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) -> +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), @@ -609,52 +579,44 @@ validate(Rule = {enum, Values}, Name, Value, _ValidatorState) -> 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/" ++ openapi_utils:to_list(Name)), + Definition = list_to_binary("#/components/schemas/" ++ to_list(Name)), try _ = validate_with_schema(Value, Definition, ValidatorState), ok @@ -674,18 +636,15 @@ validate(Rule = schema, Name, Value, ValidatorState) -> }, validation_error(Rule, Name, Info) end; - validate(Rule, Name, _Value, _ValidatorState) -> - error_logger:info_msg("Can't validate ~p with ~p", [Name, Rule]), + ?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(). - +-spec validation_error(Rule :: any(), Name :: any(), Info :: #{_ := _}) -> no_return(). validation_error(ViolatedRule, Name, Info) -> throw({wrong_param, Name, ViolatedRule, Info}). @@ -700,31 +659,26 @@ get_value(body, _Name, Req0) -> Value -> {Value, Req} end; - get_value(qs_val, Name, Req) -> QS = cowboy_req:parse_qs(Req), - Value = openapi_utils:get_opt(openapi_utils:to_qs(Name), QS), + Value = get_opt(to_qs(Name), QS), {Value, Req}; - get_value(header, Name, Req) -> Headers = cowboy_req:headers(Req), - Value = maps:get(openapi_utils:to_header(Name), Headers, undefined), + Value = maps:get(to_header(Name), Headers, undefined), {Value, Req}; - get_value(binding, Name, Req) -> - Value = cowboy_req:binding(openapi_utils:to_binding(Name), Req), + Value = cowboy_req:binding(to_binding(Name), Req), {Value, Req}. +prepare_body(<<>>) -> + <<>>; prepare_body(Body) -> - case Body of - <<"">> -> <<"">>; - _ -> - try - jsx:decode(Body, [return_maps]) - catch - error:_ -> - {error, {invalid_body, not_json, Body}} - end + try + json:decode(Body) + catch + error:_ -> + {error, {invalid_body, not_json, Body}} end. validate_with_schema(Body, Definition, ValidatorState) -> @@ -752,5 +706,84 @@ prepare_param(Rules, Name, Value, ValidatorState) -> {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) -> - list_to_binary(string:to_lower(openapi_utils:to_list(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. diff --git a/samples/server/petstore/erlang-server/src/openapi_auth.erl b/samples/server/petstore/erlang-server/src/openapi_auth.erl index b84a4f98dc89..0e7beb1132c4 100644 --- a/samples/server/petstore/erlang-server/src/openapi_auth.erl +++ b/samples/server/petstore/erlang-server/src/openapi_auth.erl @@ -2,47 +2,44 @@ -export([authorize_api_key/5]). --spec authorize_api_key( - LogicHandler :: atom(), - OperationID :: openapi_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) -> +-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 = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req}; _ -> - Result = openapi_logic_handler:authorize_api_key( - LogicHandler, - OperationID, - ApiKey - ), - case Result of - {true, Context} -> + case Handler(OperationID, ApiKey) of + {true, Context} -> {true, Context, Req}; - false -> - AuthHeader = <<"">>, + {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 - }; - + {maps:get(KeyParam, Headers, undefined), Req}; get_api_key(qs_val, KeyParam, Req) -> QS = cowboy_req:parse_qs(Req), - { openapi_utils:get_opt(KeyParam, 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. diff --git a/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl b/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl deleted file mode 100644 index da6e79a74ebd..000000000000 --- a/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl +++ /dev/null @@ -1,24 +0,0 @@ --module(openapi_default_logic_handler). - --behaviour(openapi_logic_handler). - --export([handle_request/3]). --export([authorize_api_key/2]). - --spec authorize_api_key(OperationID :: openapi_api:operation_id(), ApiKey :: binary()) -> {true, #{}}. - -authorize_api_key(_, _) -> {true, #{}}. - --spec handle_request( - OperationID :: openapi_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, #{}, #{}}. diff --git a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl index 817c1a49a830..28d45ab65113 100644 --- a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl @@ -1,36 +1,56 @@ -module(openapi_logic_handler). --export([handle_request/4]). +-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()}. --type handler_response() ::{ - Status :: cowboy:http_status(), - Headers :: cowboy:http_headers(), - Body :: jsx:json_term()}. - --export_type([handler_response/0]). - --callback authorize_api_key( - OperationID :: openapi_api:operation_id(), - ApiKey :: binary() -) -> - Result :: boolean() | {boolean(), context()}. - - --callback handle_request(OperationID :: openapi_api:operation_id(), cowboy_req:req(), Context :: context()) -> - handler_response(). - --spec handle_request( - Handler :: atom(), - OperationID :: openapi_api:operation_id(), - Request :: cowboy_req:req(), - Context :: context() -) -> - handler_response(). - -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). - --spec authorize_api_key(Handler :: atom(), OperationID :: openapi_api:operation_id(), ApiKey :: binary()) -> - Result :: false | {true, context()}. -authorize_api_key(Handler, OperationID, ApiKey) -> - Handler:authorize_api_key(OperationID, ApiKey). + +-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}. diff --git a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl index dc72a63c5d26..70edd6fb0010 100644 --- a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl @@ -1,485 +1,251 @@ %% basic handler -module(openapi_pet_handler). +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). - -%% Handlers --export([handle_request_json/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type state() :: state(). +-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()}). --spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. +-type state() :: #state{}. -init(Req, {Operations, LogicHandler, ValidatorMod}) -> +-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), - - ValidatorState = ValidatorMod:get_validator_state(), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, + ?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(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. - - -allowed_methods( - Req, - State = #state{ - operation_id = 'AddPet' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'AddPet'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'DeletePet' - } -) -> +allowed_methods(Req, #state{operation_id = 'DeletePet'} = State) -> {[<<"DELETE">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'FindPetsByStatus' - } -) -> +allowed_methods(Req, #state{operation_id = 'FindPetsByStatus'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'FindPetsByTags' - } -) -> +allowed_methods(Req, #state{operation_id = 'FindPetsByTags'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'GetPetById' - } -) -> +allowed_methods(Req, #state{operation_id = 'GetPetById'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdatePet' - } -) -> +allowed_methods(Req, #state{operation_id = 'UpdatePet'} = State) -> {[<<"PUT">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdatePetWithForm' - } -) -> +allowed_methods(Req, #state{operation_id = 'UpdatePetWithForm'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'UploadFile' - } -) -> +allowed_methods(Req, #state{operation_id = 'UploadFile'} = State) -> {[<<"POST">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. -is_authorized( - Req0, - State = #state{ - operation_id = 'AddPet' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. +is_authorized(Req0, + #state{operation_id = 'AddPet' = 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 = #state{ - operation_id = 'DeletePet' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'DeletePet' = 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 = #state{ - operation_id = 'FindPetsByStatus' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'FindPetsByStatus' = 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 = #state{ - operation_id = 'FindPetsByTags' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'FindPetsByTags' = 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 = #state{ - operation_id = 'GetPetById' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'GetPetById' = 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 = #state{ - operation_id = 'UpdatePet' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'UpdatePet' = 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 = #state{ - operation_id = 'UpdatePetWithForm' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'UpdatePetWithForm' = 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 = #state{ - operation_id = 'UploadFile' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_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} +is_authorized(Req0, + #state{operation_id = 'UploadFile' = 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) -> - {{false, <<"">>}, Req, State}. -is_authorized(Req, State) -> - {{false, <<"">>}, Req, State}. - --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. + {true, Req, State}. -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'AddPet'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'AddPet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'DeletePet' - } -) -> - Headers = ["api_key"], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'FindPetsByStatus' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'FindPetsByTags' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetPetById' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdatePet' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdatePetWithForm' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UploadFile' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; + {<<"application/json">>, handle_type_accepted}, + {<<"application/xml">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'DeletePet'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'FindPetsByStatus'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'FindPetsByTags'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'GetPetById'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'UpdatePet'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted}, + {<<"application/xml">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'UpdatePetWithForm'} = State) -> + {[ + {<<"application/x-www-form-urlencoded">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'UploadFile'} = State) -> + {[ + {<<"multipart/form-data">>, 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 = 'AddPet'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'DeletePet'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'FindPetsByStatus'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'FindPetsByTags'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'GetPetById'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'UpdatePet'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'UpdatePetWithForm'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'UploadFile'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'AddPet'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'DeletePet'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'FindPetsByStatus'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'FindPetsByTags'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'GetPetById'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'UpdatePet'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'UpdatePetWithForm'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'UploadFile'} = State) -> + {[ + {<<"application/json">>, 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) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - 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} + case handle_type_accepted(Req, State) of + true -> + {true, Req, State}; + _ -> + {false, Req, State} end. --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - 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. - -validate_headers(_, Req) -> {true, Req}. - -prepare_body(204, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(304, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(_Code, Body) -> - jsx:encode(Body). +-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(pet, 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(pet, OperationID, Req, State#state.context). diff --git a/samples/server/petstore/erlang-server/src/openapi_router.erl b/samples/server/petstore/erlang-server/src/openapi_router.erl index c32f2e5ba1d6..618d2024794b 100644 --- a/samples/server/petstore/erlang-server/src/openapi_router.erl +++ b/samples/server/petstore/erlang-server/src/openapi_router.erl @@ -1,57 +1,36 @@ -module(openapi_router). --export([get_paths/1, get_validator_state/0]). +-export([get_paths/1]). --type operations() :: #{ - Method :: binary() => openapi_api:operation_id() -}. - --type init_opts() :: { - Operations :: operations(), - LogicHandler :: atom(), - ValidatorMod :: module() -}. +-type method() :: binary(). +-type operations() :: #{method() => openapi_api:operation_id()}. +-type init_opts() :: {operations(), module()}. -export_type([init_opts/0]). --spec get_paths(LogicHandler :: atom()) -> [{'_',[{ - Path :: string(), - Handler :: atom(), - InitOpts :: init_opts() -}]}]. - +-spec get_paths(LogicHandler :: module()) -> cowboy_router:routes(). get_paths(LogicHandler) -> - ValidatorState = prepare_validator(), PreparedPaths = maps:fold( - fun(Path, #{operations := Operations, handler := Handler}, Acc) -> - [{Path, Handler, Operations} | Acc] - end, - [], - group_paths() - ), - [ - {'_', - [{P, H, {O, LogicHandler, ValidatorState}} || {P, H, O} <- PreparedPaths] - } - ]. + 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() - ). + 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() -> #{ @@ -156,18 +135,3 @@ get_operations() -> handler => 'openapi_user_handler' } }. - -get_validator_state() -> - persistent_term:get({?MODULE, validator_state}). - - -prepare_validator() -> - R = jsx:decode(element(2, file:read_file(get_openapi_path()))), - JesseState = jesse_state:new(R, [{default_schema_ver, <<"http://json-schema.org/draft-04/schema#">>}]), - persistent_term:put({?MODULE, validator_state}, JesseState), - ?MODULE. - - -get_openapi_path() -> - {ok, AppName} = application:get_application(?MODULE), - filename:join(openapi_utils:priv_dir(AppName), "openapi.json"). diff --git a/samples/server/petstore/erlang-server/src/openapi_server.erl b/samples/server/petstore/erlang-server/src/openapi_server.erl index 02500173821f..0cd992fe69d2 100644 --- a/samples/server/petstore/erlang-server/src/openapi_server.erl +++ b/samples/server/petstore/erlang-server/src/openapi_server.erl @@ -1,26 +1,21 @@ -module(openapi_server). - --define(DEFAULT_LOGIC_HANDLER, openapi_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, openapi_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), +-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), - ExtraOpts = maps:get(cowboy_extra_opts, Params, []), - CowboyOpts = get_cowboy_config(LogicHandler, ExtraOpts), + CowboyOpts = get_cowboy_config(LogicHandler, ProtocolOpts), case Transport of ssl -> cowboy:start_tls(ID, TransportOpts, CowboyOpts); @@ -28,33 +23,17 @@ start(ID, #{ cowboy:start_clear(ID, TransportOpts, CowboyOpts) end. -get_socket_transport(IP, Port, Options) -> - Opts = [ - {ip, IP}, - {port, Port} - ], - case openapi_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; + DefaultOpts = get_default_opts(LogicHandler), + maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts). -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_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), @@ -62,6 +41,3 @@ get_default_dispatch(LogicHandler) -> get_default_opts(LogicHandler) -> #{env => get_default_dispatch(LogicHandler)}. - -store_key(Key, Value, Opts) -> - maps:put(Key, Value, Opts). diff --git a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl index 09245deb509b..89952c4cc04e 100644 --- a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl @@ -1,280 +1,139 @@ %% basic handler -module(openapi_store_handler). +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). -%% Handlers --export([handle_request_json/2]). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-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(). +-type state() :: #state{}. --spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - -init(Req, {Operations, LogicHandler, ValidatorMod}) -> +-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), - - ValidatorState = ValidatorMod:get_validator_state(), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, + ?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(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. - - -allowed_methods( - Req, - State = #state{ - operation_id = 'DeleteOrder' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'DeleteOrder'} = State) -> {[<<"DELETE">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'GetInventory' - } -) -> +allowed_methods(Req, #state{operation_id = 'GetInventory'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'GetOrderById' - } -) -> +allowed_methods(Req, #state{operation_id = 'GetOrderById'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'PlaceOrder' - } -) -> +allowed_methods(Req, #state{operation_id = 'PlaceOrder'} = State) -> {[<<"POST">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. -is_authorized( - Req0, - State = #state{ - operation_id = 'GetInventory' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. +is_authorized(Req0, + #state{operation_id = 'GetInventory' = 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) -> - {{false, <<"">>}, Req, State}. -is_authorized(Req, State) -> - {{false, <<"">>}, Req, State}. - --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. + {true, Req, State}. -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'DeleteOrder'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'GetInventory'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'GetOrderById'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'PlaceOrder'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'DeleteOrder' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetInventory' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetOrderById' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'PlaceOrder' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, 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 = 'DeleteOrder'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'GetInventory'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'GetOrderById'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'PlaceOrder'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'DeleteOrder'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'GetInventory'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'GetOrderById'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'PlaceOrder'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, 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) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - 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()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - 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) + case handle_type_accepted(Req, State) of + true -> + {true, Req, State}; + _ -> + {false, Req, State} end. -validate_headers(_, Req) -> {true, Req}. - -prepare_body(204, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(304, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(_Code, Body) -> - jsx:encode(Body). +-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(store, 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(store, OperationID, Req, State#state.context). diff --git a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl index aad43103ac66..3fedf1c9fd56 100644 --- a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl @@ -1,447 +1,220 @@ %% basic handler -module(openapi_user_handler). +-behaviour(cowboy_rest). + +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks --export([allowed_methods/2]). -export([init/2]). --export([allow_missing_post/2]). +-export([allowed_methods/2]). -export([content_types_accepted/2]). -export([content_types_provided/2]). -export([delete_resource/2]). -export([is_authorized/2]). --export([known_content_type/2]). --export([malformed_request/2]). -export([valid_content_headers/2]). --export([valid_entity_length/2]). +-export([handle_type_accepted/2, handle_type_provided/2]). -%% Handlers --export([handle_request_json/2]). +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), - validator_state :: jesse_state:state(), - context=#{} :: #{} -}). +-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(). +-type state() :: #state{}. --spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - -init(Req, {Operations, LogicHandler, ValidatorMod}) -> +-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), - - ValidatorState = ValidatorMod:get_validator_state(), - - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), - - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - }, + ?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(Req :: cowboy_req:req(), State :: state()) -> - {Value :: [binary()], Req :: cowboy_req:req(), State :: state()}. - - -allowed_methods( - Req, - State = #state{ - operation_id = 'CreateUser' - } -) -> +-spec allowed_methods(cowboy_req:req(), state()) -> + {[binary()], cowboy_req:req(), state()}. +allowed_methods(Req, #state{operation_id = 'CreateUser'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'CreateUsersWithArrayInput' - } -) -> +allowed_methods(Req, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'CreateUsersWithListInput' - } -) -> +allowed_methods(Req, #state{operation_id = 'CreateUsersWithListInput'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'DeleteUser' - } -) -> +allowed_methods(Req, #state{operation_id = 'DeleteUser'} = State) -> {[<<"DELETE">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'GetUserByName' - } -) -> +allowed_methods(Req, #state{operation_id = 'GetUserByName'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'LoginUser' - } -) -> +allowed_methods(Req, #state{operation_id = 'LoginUser'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'LogoutUser' - } -) -> +allowed_methods(Req, #state{operation_id = 'LogoutUser'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdateUser' - } -) -> +allowed_methods(Req, #state{operation_id = 'UpdateUser'} = State) -> {[<<"PUT">>], Req, State}; - allowed_methods(Req, State) -> {[], Req, State}. --spec is_authorized(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: true | {false, AuthHeader :: iodata()}, - Req :: cowboy_req:req(), - State :: state() - }. -is_authorized( - Req0, - State = #state{ - operation_id = 'CreateUser' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +-spec is_authorized(cowboy_req:req(), state()) -> + {true | {false, iodata()}, cowboy_req:req(), state()}. +is_authorized(Req0, + #state{operation_id = 'CreateUser' = 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 = #state{ - operation_id = 'CreateUsersWithArrayInput' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'CreateUsersWithArrayInput' = 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 = #state{ - operation_id = 'CreateUsersWithListInput' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'CreateUsersWithListInput' = 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 = #state{ - operation_id = 'DeleteUser' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'DeleteUser' = 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 = #state{ - operation_id = 'LogoutUser' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'LogoutUser' = 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 = #state{ - operation_id = 'UpdateUser' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), - case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} +is_authorized(Req0, + #state{operation_id = 'UpdateUser' = 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) -> - {{false, <<"">>}, Req, State}. -is_authorized(Req, State) -> - {{false, <<"">>}, Req, State}. - --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. + {true, Req, State}. -content_types_accepted(Req, State) -> +-spec content_types_accepted(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_accepted(Req, #state{operation_id = 'CreateUser'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> - {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'CreateUser' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'CreateUsersWithArrayInput' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'CreateUsersWithListInput' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'DeleteUser' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetUserByName' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'LoginUser' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'LogoutUser' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdateUser' - } -) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'CreateUsersWithListInput'} = State) -> + {[ + {<<"application/json">>, handle_type_accepted} + ], Req, State}; +content_types_accepted(Req, #state{operation_id = 'DeleteUser'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'GetUserByName'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'LoginUser'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'LogoutUser'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'UpdateUser'} = 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 = 'CreateUser'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'CreateUsersWithListInput'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'DeleteUser'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'GetUserByName'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'LoginUser'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'LogoutUser'} = State) -> + {true, Req, State}; +valid_content_headers(Req, #state{operation_id = 'UpdateUser'} = State) -> + {true, Req, State}; valid_content_headers(Req, State) -> {false, Req, State}. --spec content_types_provided(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), ProvideResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_provided(Req, State) -> +-spec content_types_provided(cowboy_req:req(), state()) -> + {[{binary(), atom()}], cowboy_req:req(), state()}. +content_types_provided(Req, #state{operation_id = 'CreateUser'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'CreateUsersWithListInput'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'DeleteUser'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'GetUserByName'} = State) -> {[ - {<<"application/json">>, handle_request_json} - ], Req, State}. - --spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -malformed_request(Req, State) -> - {false, Req, State}. - --spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> - {Value :: false, Req :: cowboy_req:req(), State :: state()}. - -allow_missing_post(Req, State) -> - {false, Req, State}. - --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'LoginUser'} = State) -> + {[ + {<<"application/xml">>, handle_type_provided}, + {<<"application/json">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, #state{operation_id = 'LogoutUser'} = State) -> + {[], Req, State}; +content_types_provided(Req, #state{operation_id = 'UpdateUser'} = State) -> + {[], 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) -> - handle_request_json(Req, State). - --spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -known_content_type(Req, State) -> - {true, Req, State}. - --spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> - {Value :: true, Req :: cowboy_req:req(), State :: state()}. - -valid_entity_length(Req, State) -> - %% @TODO check the length - {true, Req, State}. - -%%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). - -process_response(Response, Req0, State = #state{operation_id = OperationID}) -> - case Response of - {ok, {Code, Headers, Body}} -> - 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} + case handle_type_accepted(Req, State) of + true -> + {true, Req, State}; + _ -> + {false, Req, State} end. --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). - -handle_request_json( - Req0, - State = #state{ - operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState - } -) -> - 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. - -validate_headers(_, Req) -> {true, Req}. - -prepare_body(204, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(304, Body) when map_size(Body) == 0; length(Body) == 0 -> - <<>>; -prepare_body(_Code, Body) -> - jsx:encode(Body). +-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(user, 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(user, OperationID, Req, State#state.context). diff --git a/samples/server/petstore/erlang-server/src/openapi_utils.erl b/samples/server/petstore/erlang-server/src/openapi_utils.erl deleted file mode 100644 index 58eee3a48e0f..000000000000 --- a/samples/server/petstore/erlang-server/src/openapi_utils.erl +++ /dev/null @@ -1,173 +0,0 @@ --module(openapi_utils). - --export([to_binary/1]). --export([to_list/1]). --export([to_float/1]). --export([to_int/1]). --export([to_lower/1]). --export([to_upper/1]). --export([set_resp_headers/2]). --export([to_header/1]). --export([to_qs/1]). --export([to_binding/1]). --export([get_opt/2]). --export([get_opt/3]). --export([priv_dir/0]). --export([priv_dir/1]). --export([priv_path/1]). - - --spec to_binary(iodata() | atom() | number()) -> binary(). - -to_binary(V) when is_binary(V) -> V; -to_binary(V) when is_list(V) -> iolist_to_binary(V); -to_binary(V) when is_atom(V) -> atom_to_binary(V, utf8); -to_binary(V) when is_integer(V) -> integer_to_binary(V); -to_binary(V) when is_float(V) -> float_to_binary(V). - --spec to_list(iodata() | atom() | number()) -> string(). - -to_list(V) when is_list(V) -> V; -to_list(V) -> binary_to_list(to_binary(V)). - --spec to_float(iodata()) -> number(). - -to_float(V) -> - Data = iolist_to_binary([V]), - case binary:split(Data, <<$.>>) of - [Data] -> - binary_to_integer(Data); - [<<>>, _] -> - binary_to_float(<<$0, Data/binary>>); - _ -> - binary_to_float(Data) - end. - -%% - --spec to_int(integer() | binary() | list()) -> integer(). - -to_int(Data) when is_integer(Data) -> - Data; -to_int(Data) when is_binary(Data) -> - binary_to_integer(Data); -to_int(Data) when is_list(Data) -> - list_to_integer(Data). - --spec set_resp_headers([{binary(), iodata()}], cowboy_req:req()) -> cowboy_req:req(). - -set_resp_headers([], Req) -> - Req; -set_resp_headers([{K, V} | T], Req0) -> - Req = cowboy_req:set_resp_header(K, V, Req0), - set_resp_headers(T, Req). - --spec to_header(iodata() | atom() | number()) -> binary(). - -to_header(Name) -> - Prepared = to_binary(Name), - to_lower(Prepared). - --spec to_qs(iodata() | atom() | number()) -> binary(). - -to_qs(Name) -> - to_binary(Name). - --spec to_binding(iodata() | atom() | number()) -> atom(). - -to_binding(Name) -> - Prepared = to_binary(Name), - binary_to_atom(Prepared, utf8). - --spec get_opt(any(), []) -> any(). - -get_opt(Key, Opts) -> - get_opt(Key, Opts, undefined). - --spec get_opt(any(), [], any()) -> any(). - -get_opt(Key, Opts, Default) -> - case lists:keyfind(Key, 1, Opts) of - {_, Value} -> Value; - false -> Default - end. - --spec priv_dir() -> file:filename(). - -priv_dir() -> - {ok, AppName} = application:get_application(), - priv_dir(AppName). - --spec priv_dir(Application :: atom()) -> file:filename(). - -priv_dir(AppName) -> - case code:priv_dir(AppName) of - Value when is_list(Value) -> - Value ++ "/"; - _Error -> - select_priv_dir([filename:join(["apps", atom_to_list(AppName), "priv"]), "priv"]) - end. - --spec priv_path(Relative :: file:filename()) -> file:filename(). - -priv_path(Relative) -> - filename:join(priv_dir(), Relative). - --include_lib("kernel/include/file.hrl"). - -select_priv_dir(Paths) -> - case lists:dropwhile(fun test_priv_dir/1, Paths) of - [Path | _] -> Path; - _ -> exit(no_priv_dir) - end. - -test_priv_dir(Path) -> - case file:read_file_info(Path) of - {ok, #file_info{type = directory}} -> - false; - _ -> - true - end. - - -%% - --spec to_lower(binary()) -> binary(). - -to_lower(S) -> - to_case(lower, S, <<>>). - --spec to_upper(binary()) -> binary(). - -to_upper(S) -> - to_case(upper, S, <<>>). - -to_case(_Case, <<>>, Acc) -> - Acc; - -to_case(_Case, <>, _Acc) when C > 127 -> - error(badarg); - -to_case(Case = lower, <>, Acc) -> - to_case(Case, Rest, <>); - -to_case(Case = upper, <>, Acc) -> - to_case(Case, Rest, <>). - -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.