From 73c83623bac618827d3f35b64cfa93f4084dd64f Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 09:57:17 +0200 Subject: [PATCH 01/12] Upgrade erlang-server code generation and fix is_authorized crashes --- docs/generators/erlang-server.md | 1 + .../languages/ErlangServerCodegen.java | 6 +- .../resources/erlang-server/README.mustache | 54 ++-- .../main/resources/erlang-server/api.mustache | 4 +- .../resources/erlang-server/app.src.mustache | 2 +- .../resources/erlang-server/auth.mustache | 19 +- .../default_logic_handler.mustache | 16 +- .../resources/erlang-server/handler.mustache | 70 ++---- .../erlang-server/logic_handler.mustache | 28 +-- .../erlang-server/rebar.config.mustache | 7 +- .../resources/erlang-server/router.mustache | 12 +- .../resources/erlang-server/server.mustache | 60 ++--- .../resources/erlang-server/utils.mustache | 15 -- .../server/petstore/erlang-server/README.md | 54 ++-- .../petstore/erlang-server/rebar.config | 7 +- .../erlang-server/src/openapi.app.src | 2 +- .../erlang-server/src/openapi_api.erl | 4 +- .../erlang-server/src/openapi_auth.erl | 19 +- .../src/openapi_default_logic_handler.erl | 16 +- .../src/openapi_logic_handler.erl | 25 +- .../erlang-server/src/openapi_pet_handler.erl | 234 ++++++++---------- .../erlang-server/src/openapi_router.erl | 12 +- .../erlang-server/src/openapi_server.erl | 60 ++--- .../src/openapi_store_handler.erl | 38 ++- .../src/openapi_user_handler.erl | 178 ++++++------- .../erlang-server/src/openapi_utils.erl | 15 -- 26 files changed, 354 insertions(+), 604 deletions(-) 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..c0f7e4fb36de 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" ) ); 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..e1e260bc1628 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,30 @@ 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=>[]}) + openapi_server:start(http_server, #{ip => {127,0,0,1}, port => 8080, net_opts => []}) 2, Compilation http_server project - $ make + $ 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). 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..10506b5aba78 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -324,10 +324,10 @@ get_value(binding, Name, Req) -> prepare_body(Body) -> case Body of - <<"">> -> <<"">>; + <<>> -> <<>>; _ -> try - jsx:decode(Body, [return_maps]) + json:decode(Body, [return_maps]) catch error:_ -> {error, {invalid_body, not_json, Body}} 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..e4c812a76dd2 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 @@ -7,8 +7,8 @@ stdlib, ssl, inets, - jsx, jesse, + ranch, cowboy ]}, {env, [ 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..94b9da0aed17 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/auth.mustache @@ -3,19 +3,18 @@ -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()}. - + LogicHandler :: atom(), + OperationID :: {{packageName}}_api:operation_id(), + From :: header | qs_val, + KeyParam :: iodata() | atom(), + Req ::cowboy_req:req()) -> + {true, Context :: #{binary() => any()}, Req :: cowboy_req:req()} | + {false, AuthHeader :: binary(), Req :: cowboy_req:req()}. authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) -> {ApiKey, Req} = get_api_key(From, KeyParam, Req0), case ApiKey of undefined -> - AuthHeader = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req}; _ -> Result = {{packageName}}_logic_handler:authorize_api_key( @@ -31,7 +30,7 @@ authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) -> {{/isApiKey}} {{/authMethods}} false -> - AuthHeader = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req} end 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 index 1b39ed1d75a9..2d9cb456820c 100644 --- 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 @@ -11,18 +11,18 @@ {{#authMethods}} {{#isApiKey}} --spec authorize_api_key(OperationID :: {{packageName}}_api:operation_id(), ApiKey :: binary()) -> {true, #{}}. - -authorize_api_key(_, _) -> {true, #{}}. +-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()}. + OperationID :: {{packageName}}_api:operation_id(), + Req :: cowboy_req:req(), + Context :: #{}) -> + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. handle_request(OperationID, Req, Context) -> error_logger:error_msg( 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..ed555810bc94 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -19,9 +19,9 @@ -record(state, { operation_id :: {{packageName}}_api:operation_id(), - logic_handler :: atom(), + logic_handler :: module(), validator_state :: jesse_state:state(), - context=#{} :: #{} + context = #{} :: #{} }). -type state() :: state(). @@ -67,53 +67,37 @@ allowed_methods(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}}, +{{#authMethods.size}} +is_authorized(Req0, + #state{operation_id = '{{operationId}}' = OperationID, + logic_handler = LogicHandler} = State) -> Result = {{packageName}}_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "{{keyParamName}}", - Req0 - ), + LogicHandler, + OperationID, +{{#isApiKey.isKeyInQuery}} + qs_val, +{{/isApiKey.isKeyInQuery}} +{{^isApiKey.isKeyInQuery}} + header, +{{/isApiKey.isKeyInQuery}} +{{#isApiKey}} + "{{keyParamName}}", +{{/isApiKey}} +{{^isApiKey}} + "authorization", +{{/isApiKey}} + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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} - 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()) -> { @@ -249,4 +233,4 @@ 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). + json:encode(Body). 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..09439b6c9e30 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 @@ -12,49 +12,35 @@ -export([authorize_api_key/3]). {{/authMethods}} -type context() :: #{binary() => any()}. --type handler_response() ::{ - Status :: cowboy:http_status(), - Headers :: cowboy:http_headers(), - Body :: jsx:json_term()}. +-type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. -export_type([handler_response/0]). {{#authMethods}} {{#isApiKey}} --callback authorize_api_key( - OperationID :: {{packageName}}_api:operation_id(), - ApiKey :: binary() -) -> - Result :: boolean() | {boolean(), context()}. +-callback authorize_api_key({{packageName}}_api:operation_id(), binary()) -> + boolean() | {boolean(), context()}. {{/isApiKey}} {{/authMethods}} - --callback handle_request(OperationID :: {{packageName}}_api:operation_id(), cowboy_req:req(), Context :: context()) -> +-callback handle_request({{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> handler_response(). --spec handle_request( - Handler :: atom(), - OperationID :: {{packageName}}_api:operation_id(), - Request :: cowboy_req:req(), - Context :: context() -) -> +-spec handle_request(module(), {{packageName}}_api:operation_id(), cowboy_req:req(), 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()) -> +-spec authorize_api_key(module(), {{packageName}}_api:operation_id(), 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. +-spec authorize_api_key(module(), {{packageName}}_api:operation_id(), binary()) -> false. authorize_api_key(_Handler, _OperationID, _ApiKey) -> false. {{/authMethods}} 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..fba9fe53e3e2 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,5 @@ {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.0"} ]}. 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..4f548859b752 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/router.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/router.mustache @@ -14,12 +14,7 @@ -export_type([init_opts/0]). --spec get_paths(LogicHandler :: atom()) -> [{'_',[{ - Path :: string(), - Handler :: atom(), - InitOpts :: init_opts() -}]}]. - +-spec get_paths(LogicHandler :: atom()) -> cowboy_router:routes(). get_paths(LogicHandler) -> ValidatorState = prepare_validator(), PreparedPaths = maps:fold( @@ -65,14 +60,13 @@ get_operations() -> get_validator_state() -> persistent_term:get({?MODULE, validator_state}). - prepare_validator() -> - R = jsx:decode(element(2, file:read_file(get_openapi_path()))), + {ok, FileContents} = file:read_file(get_openapi_path()), + R = json:decode(FileContents), 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..9e69f86dedc9 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). -export([start/2]). --spec start( ID :: any(), #{ - ip => inet:ip_address(), - port => inet:port_number(), - logic_handler => module(), - net_opts => [] +-spec start(ID :: term(), #{ + transport := tcp | ssl, + transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), + 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), +start(ID, #{transport := Transport, + transport_opts := TransportOpts, + protocol_opts := ProtocolOpts} = 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)). + DefaultOpts = get_default_opts(LogicHandler), + maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts). -get_cowboy_config(_LogicHandler, [], Opts) -> - Opts; - -get_cowboy_config(LogicHandler, [{env, Env} | Rest], Opts) -> - NewEnv = case proplists:get_value(dispatch, Env) of - undefined -> [get_default_dispatch(LogicHandler) | Env]; - _ -> Env - end, - get_cowboy_config(LogicHandler, Rest, store_key(env, NewEnv, Opts)); - -get_cowboy_config(LogicHandler, [{Key, Value}| Rest], Opts) -> - get_cowboy_config(LogicHandler, Rest, store_key(Key, Value, Opts)). +get_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 index b6701add7fc2..97fc713e0fad 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache @@ -18,7 +18,6 @@ -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); @@ -26,12 +25,10 @@ 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 @@ -46,7 +43,6 @@ to_float(V) -> %% -spec to_int(integer() | binary() | list()) -> integer(). - to_int(Data) when is_integer(Data) -> Data; to_int(Data) when is_binary(Data) -> @@ -55,7 +51,6 @@ 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) -> @@ -63,29 +58,24 @@ set_resp_headers([{K, V} | T], 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; @@ -93,13 +83,11 @@ get_opt(Key, Opts, 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) -> @@ -109,7 +97,6 @@ priv_dir(AppName) -> end. -spec priv_path(Relative :: file:filename()) -> file:filename(). - priv_path(Relative) -> filename:join(priv_dir(), Relative). @@ -133,12 +120,10 @@ test_priv_dir(Path) -> %% -spec to_lower(binary()) -> binary(). - to_lower(S) -> to_case(lower, S, <<>>). -spec to_upper(binary()) -> binary(). - to_upper(S) -> to_case(upper, S, <<>>). diff --git a/samples/server/petstore/erlang-server/README.md b/samples/server/petstore/erlang-server/README.md index 5d36ca07576b..e1e260bc1628 100644 --- a/samples/server/petstore/erlang-server/README.md +++ b/samples/server/petstore/erlang-server/README.md @@ -4,54 +4,30 @@ 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=>[]}) + openapi_server:start(http_server, #{ip => {127,0,0,1}, port => 8080, net_opts => []}) 2, Compilation http_server project - $ make + $ 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). diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index 743b108f384a..fba9fe53e3e2 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -1,6 +1,5 @@ {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.0"} ]}. diff --git a/samples/server/petstore/erlang-server/src/openapi.app.src b/samples/server/petstore/erlang-server/src/openapi.app.src index 99859823b68e..043e811f22eb 100644 --- a/samples/server/petstore/erlang-server/src/openapi.app.src +++ b/samples/server/petstore/erlang-server/src/openapi.app.src @@ -7,8 +7,8 @@ stdlib, ssl, inets, - jsx, jesse, + ranch, cowboy ]}, {env, [ diff --git a/samples/server/petstore/erlang-server/src/openapi_api.erl b/samples/server/petstore/erlang-server/src/openapi_api.erl index 8b9cb84eaa49..74a125a2a3ba 100644 --- a/samples/server/petstore/erlang-server/src/openapi_api.erl +++ b/samples/server/petstore/erlang-server/src/openapi_api.erl @@ -717,10 +717,10 @@ get_value(binding, Name, Req) -> prepare_body(Body) -> case Body of - <<"">> -> <<"">>; + <<>> -> <<>>; _ -> try - jsx:decode(Body, [return_maps]) + json:decode(Body, [return_maps]) catch error:_ -> {error, {invalid_body, not_json, Body}} diff --git a/samples/server/petstore/erlang-server/src/openapi_auth.erl b/samples/server/petstore/erlang-server/src/openapi_auth.erl index b84a4f98dc89..6d6f85069a04 100644 --- a/samples/server/petstore/erlang-server/src/openapi_auth.erl +++ b/samples/server/petstore/erlang-server/src/openapi_auth.erl @@ -3,19 +3,18 @@ -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()}. - + 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) -> {ApiKey, Req} = get_api_key(From, KeyParam, Req0), case ApiKey of undefined -> - AuthHeader = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req}; _ -> Result = openapi_logic_handler:authorize_api_key( @@ -27,7 +26,7 @@ authorize_api_key(LogicHandler, OperationID, From, KeyParam, Req0) -> {true, Context} -> {true, Context, Req}; false -> - AuthHeader = <<"">>, + AuthHeader = <<>>, {false, AuthHeader, Req} end 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 index da6e79a74ebd..b3d3e99613d9 100644 --- a/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl @@ -5,16 +5,16 @@ -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 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()}. + OperationID :: openapi_api:operation_id(), + Req :: cowboy_req:req(), + Context :: #{}) -> + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. handle_request(OperationID, Req, Context) -> error_logger:error_msg( 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..f28c93ca326c 100644 --- a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl @@ -2,35 +2,22 @@ -export([handle_request/4]). -type context() :: #{binary() => any()}. --type handler_response() ::{ - Status :: cowboy:http_status(), - Headers :: cowboy:http_headers(), - Body :: jsx:json_term()}. +-type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. -export_type([handler_response/0]). --callback authorize_api_key( - OperationID :: openapi_api:operation_id(), - ApiKey :: binary() -) -> - Result :: boolean() | {boolean(), context()}. +-callback authorize_api_key(openapi_api:operation_id(), binary()) -> + boolean() | {boolean(), context()}. - --callback handle_request(OperationID :: openapi_api:operation_id(), cowboy_req:req(), Context :: context()) -> +-callback handle_request(openapi_api:operation_id(), cowboy_req:req(), context()) -> handler_response(). --spec handle_request( - Handler :: atom(), - OperationID :: openapi_api:operation_id(), - Request :: cowboy_req:req(), - Context :: context() -) -> +-spec handle_request(module(), openapi_api:operation_id(), cowboy_req:req(), 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()) -> +-spec authorize_api_key(module(), openapi_api:operation_id(), binary()) -> Result :: false | {true, context()}. authorize_api_key(Handler, OperationID, ApiKey) -> Handler:authorize_api_key(OperationID, ApiKey). 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..c2c16092e6b7 100644 --- a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl @@ -19,9 +19,9 @@ -record(state, { operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), + logic_handler :: module(), validator_state :: jesse_state:state(), - context=#{} :: #{} + context = #{} :: #{} }). -type state() :: state(). @@ -121,162 +121,128 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized( - Req0, - State = #state{ - operation_id = 'AddPet' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, +is_authorized(Req0, + #state{operation_id = 'AddPet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'DeletePet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'FindPetsByStatus' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'FindPetsByTags' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'GetPetById' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'UpdatePet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'UpdatePetWithForm' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'UploadFile' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "Authorization", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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}. + {true, Req, State}. -spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> { @@ -482,4 +448,4 @@ 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). + json:encode(Body). diff --git a/samples/server/petstore/erlang-server/src/openapi_router.erl b/samples/server/petstore/erlang-server/src/openapi_router.erl index c32f2e5ba1d6..fa3360a5b1ec 100644 --- a/samples/server/petstore/erlang-server/src/openapi_router.erl +++ b/samples/server/petstore/erlang-server/src/openapi_router.erl @@ -14,12 +14,7 @@ -export_type([init_opts/0]). --spec get_paths(LogicHandler :: atom()) -> [{'_',[{ - Path :: string(), - Handler :: atom(), - InitOpts :: init_opts() -}]}]. - +-spec get_paths(LogicHandler :: atom()) -> cowboy_router:routes(). get_paths(LogicHandler) -> ValidatorState = prepare_validator(), PreparedPaths = maps:fold( @@ -160,14 +155,13 @@ get_operations() -> get_validator_state() -> persistent_term:get({?MODULE, validator_state}). - prepare_validator() -> - R = jsx:decode(element(2, file:read_file(get_openapi_path()))), + {ok, FileContents} = file:read_file(get_openapi_path()), + R = json:decode(FileContents), 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..d0299408d519 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). -export([start/2]). --spec start( ID :: any(), #{ - ip => inet:ip_address(), - port => inet:port_number(), - logic_handler => module(), - net_opts => [] +-spec start(ID :: term(), #{ + transport := tcp | ssl, + transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), + 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), +start(ID, #{transport := Transport, + transport_opts := TransportOpts, + protocol_opts := ProtocolOpts} = 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)). + DefaultOpts = get_default_opts(LogicHandler), + maps:fold(fun get_cowboy_config/3, DefaultOpts, ExtraOpts). -get_cowboy_config(_LogicHandler, [], Opts) -> - Opts; - -get_cowboy_config(LogicHandler, [{env, Env} | Rest], Opts) -> - NewEnv = case proplists:get_value(dispatch, Env) of - undefined -> [get_default_dispatch(LogicHandler) | Env]; - _ -> Env - end, - get_cowboy_config(LogicHandler, Rest, store_key(env, NewEnv, Opts)); - -get_cowboy_config(LogicHandler, [{Key, Value}| Rest], Opts) -> - get_cowboy_config(LogicHandler, Rest, store_key(Key, Value, Opts)). +get_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..0e7cd9173d27 100644 --- a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl @@ -19,9 +19,9 @@ -record(state, { operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), + logic_handler :: module(), validator_state :: jesse_state:state(), - context=#{} :: #{} + context = #{} :: #{} }). -type state() :: state(). @@ -89,29 +89,23 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized( - Req0, - State = #state{ - operation_id = 'GetInventory' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, +is_authorized(Req0, + #state{operation_id = 'GetInventory' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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}. + {true, Req, State}. -spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> { @@ -277,4 +271,4 @@ 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). + json:encode(Body). 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..0153cb73f7b1 100644 --- a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl @@ -19,9 +19,9 @@ -record(state, { operation_id :: openapi_api:operation_id(), - logic_handler :: atom(), + logic_handler :: module(), validator_state :: jesse_state:state(), - context=#{} :: #{} + context = #{} :: #{} }). -type state() :: state(). @@ -121,124 +121,98 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized( - Req0, - State = #state{ - operation_id = 'CreateUser' = OperationID, - logic_handler = LogicHandler - } -) -> - From = header, +is_authorized(Req0, + #state{operation_id = 'CreateUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'CreateUsersWithArrayInput' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'CreateUsersWithListInput' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'DeleteUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'LogoutUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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, +is_authorized(Req0, + #state{operation_id = 'UpdateUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - From, - "api_key", - Req0 - ), + LogicHandler, + OperationID, + header, + "authorization", + Req0), case Result of - {true, Context, Req} -> {true, Req, State#state{context = Context}}; - {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} + {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}. + {true, Req, State}. -spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> { @@ -444,4 +418,4 @@ 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). + json:encode(Body). diff --git a/samples/server/petstore/erlang-server/src/openapi_utils.erl b/samples/server/petstore/erlang-server/src/openapi_utils.erl index 58eee3a48e0f..22c7867236be 100644 --- a/samples/server/petstore/erlang-server/src/openapi_utils.erl +++ b/samples/server/petstore/erlang-server/src/openapi_utils.erl @@ -18,7 +18,6 @@ -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); @@ -26,12 +25,10 @@ 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 @@ -46,7 +43,6 @@ to_float(V) -> %% -spec to_int(integer() | binary() | list()) -> integer(). - to_int(Data) when is_integer(Data) -> Data; to_int(Data) when is_binary(Data) -> @@ -55,7 +51,6 @@ 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) -> @@ -63,29 +58,24 @@ set_resp_headers([{K, V} | T], 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; @@ -93,13 +83,11 @@ get_opt(Key, Opts, 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) -> @@ -109,7 +97,6 @@ priv_dir(AppName) -> end. -spec priv_path(Relative :: file:filename()) -> file:filename(). - priv_path(Relative) -> filename:join(priv_dir(), Relative). @@ -133,12 +120,10 @@ test_priv_dir(Path) -> %% -spec to_lower(binary()) -> binary(). - to_lower(S) -> to_case(lower, S, <<>>). -spec to_upper(binary()) -> binary(). - to_upper(S) -> to_case(upper, S, <<>>). From 8ca26df5a88569d2cb873a5c007507faaf51b17f Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 16:21:26 +0200 Subject: [PATCH 02/12] Introduce structured logging --- .../main/resources/erlang-server/api.mustache | 4 +++- .../default_logic_handler.mustache | 8 +++---- .../resources/erlang-server/handler.mustache | 23 ++++++++----------- 3 files changed, 17 insertions(+), 18 deletions(-) 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 10506b5aba78..fa4b808aa4b9 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -1,5 +1,7 @@ -module({{packageName}}_api). +-include_lib("kernel/include/logger.hrl"). + -export([request_params/1]). -export([request_param_info/2]). -export([populate_request/3]). @@ -283,7 +285,7 @@ validate(Rule = schema, Name, Value, ValidatorState) -> 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(). 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 index 2d9cb456820c..e958505ee7f2 100644 --- 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 @@ -2,6 +2,8 @@ -behaviour({{packageName}}_logic_handler). +-include_lib("kernel/include/logger.hrl"). + -export([handle_request/3]). {{#authMethods}} {{#isApiKey}} @@ -25,8 +27,6 @@ authorize_api_key(_, _) -> {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. handle_request(OperationID, Req, Context) -> - error_logger:error_msg( - "Got not implemented request to process: ~p~n", - [{OperationID, Req, Context}] - ), + ?LOG_ERROR(#{what => "Got not implemented request to process", + operation_id => OperationID, request => Req, context => 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 ed555810bc94..102f92be83f7 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -1,6 +1,8 @@ %% basic handler -module({{classname}}). +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks -export([allowed_methods/2]). -export([init/2]). @@ -35,7 +37,7 @@ init(Req, {Operations, LogicHandler, ValidatorMod}) -> ValidatorState = ValidatorMod:get_validator_state(), - error_logger:info_msg("Attempt to process operation: ~p", [OperationID]), + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), State = #state{ operation_id = OperationID, @@ -182,18 +184,13 @@ valid_entity_length(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. +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. -spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). From cd03aefa99431493aab026294ad1cef61cf450ba Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 16:23:43 +0200 Subject: [PATCH 03/12] Improve general formatting --- .../main/resources/erlang-server/api.mustache | 34 ++--------- .../default_logic_handler.mustache | 2 +- .../resources/erlang-server/handler.mustache | 57 +++++-------------- 3 files changed, 20 insertions(+), 73 deletions(-) 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 fa4b808aa4b9..f12c0b81feeb 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -85,20 +85,17 @@ request_param_info(OperationID, Name) -> error({unknown_param, OperationID, Name}). -spec populate_request( - OperationID :: operation_id(), - Req :: cowboy_req:req(), - ValidatorState :: jesse_state:state() -) -> + 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} -> @@ -124,8 +121,7 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) -> OperationID :: operation_id(), Code :: 200..599, Body :: jesse:json_term(), - ValidatorState :: jesse_state:state() -) -> ok | no_return(). + ValidatorState :: jesse_state:state()) -> ok | no_return(). {{#apiInfo}}{{#apis}} {{#operations}}{{#operation}} {{#responses}} @@ -151,13 +147,10 @@ validate(Rule = required, Name, Value, _ValidatorState) -> undefined -> validation_error(Rule, Name); _ -> ok end; - validate(not_required, _Name, _Value, _ValidatorState) -> ok; - validate(_, _Name, undefined, _ValidatorState) -> ok; - validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) -> try {ok, {{packageName}}_utils:to_int(Value)} @@ -165,7 +158,6 @@ validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) -> error:badarg -> validation_error(Rule, Name) end; - validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> try {ok, {{packageName}}_utils:to_float(Value)} @@ -173,16 +165,13 @@ validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> 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 @@ -194,19 +183,16 @@ validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> 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), @@ -218,50 +204,42 @@ 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)), try @@ -283,7 +261,6 @@ validate(Rule = schema, Name, Value, ValidatorState) -> }, 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}). @@ -309,17 +286,14 @@ 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, Req}; - get_value(header, Name, Req) -> Headers = cowboy_req:headers(Req), Value = maps:get({{packageName}}_utils:to_header(Name), Headers, undefined), {Value, Req}; - get_value(binding, Name, Req) -> Value = cowboy_req:binding({{packageName}}_utils:to_binding(Name), Req), {Value, Req}. 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 index e958505ee7f2..ce638658e42c 100644 --- 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 @@ -23,7 +23,7 @@ authorize_api_key(_, _) -> -spec handle_request( OperationID :: {{packageName}}_api:operation_id(), Req :: cowboy_req:req(), - Context :: #{}) -> + Context :: #{_ => _}) -> {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. handle_request(OperationID, Req, Context) -> 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 102f92be83f7..0b1d5e6525b1 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -16,8 +16,14 @@ -export([valid_content_headers/2]). -export([valid_entity_length/2]). -%% Handlers --export([handle_request_json/2]). +-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()}. -record(state, { operation_id :: {{packageName}}_api:operation_id(), @@ -30,7 +36,6 @@ -spec init(Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) -> {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - init(Req, {Operations, LogicHandler, ValidatorMod}) -> Method = cowboy_req:method(Req), OperationID = maps:get(Method, Operations, undefined), @@ -48,14 +53,8 @@ init(Req, {Operations, LogicHandler, ValidatorMod}) -> -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}}' - } -) -> +allowed_methods(Req, State = #state{operation_id = '{{operationId}}'}) -> {[<<"{{httpMethod}}">>], Req, State}; {{/operation}}{{/operations}} allowed_methods(Req, State) -> @@ -70,9 +69,8 @@ allowed_methods(Req, State) -> {{#operations}} {{#operation}} {{#authMethods.size}} -is_authorized(Req0, - #state{operation_id = '{{operationId}}' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = '{{operationId}}' = OperationID, + logic_handler = LogicHandler} = State) -> Result = {{packageName}}_auth:authorize_api_key( LogicHandler, OperationID, @@ -116,12 +114,7 @@ content_types_accepted(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}}' - } -) -> +valid_content_headers(Req0, #state{operation_id = '{{operationId}}'} = State) -> Headers = [{{#headerParams}}"{{baseName}}"{{^-last}},{{/-last}}{{/headerParams}}], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; @@ -143,45 +136,31 @@ content_types_provided(Req, State) -> -spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - malformed_request(Req, State) -> {false, Req, State}. -spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - allow_missing_post(Req, State) -> {false, Req, State}. -spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> processed_response(). - delete_resource(Req, State) -> handle_request_json(Req, State). -spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - known_content_type(Req, State) -> {true, Req, State}. -spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - valid_entity_length(Req, State) -> %% @TODO check the length {true, Req, State}. %%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {stop, cowboy_req:req(), state()}. - -spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> processed_response(). process_response({ok, {Code, Headers, Body}}, Req0, State) -> @@ -193,15 +172,9 @@ process_response({error, Reason}, Req0, #state{operation_id = OperationID} = Sta {stop, Req, State}. -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 - } -) -> +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> case {{packageName}}_api:populate_request(OperationID, Req0, ValidatorState) of {ok, Populated, Req1} -> {Code, Headers, Body} = {{packageName}}_logic_handler:handle_request( From 3765bfbac9f5eba9d62db4ec5279e97f63a82ba3 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 17:46:14 +0200 Subject: [PATCH 04/12] Update generated files --- .../erlang-server/src/openapi_api.erl | 38 +--- .../src/openapi_default_logic_handler.erl | 10 +- .../erlang-server/src/openapi_pet_handler.erl | 213 +++++------------- .../src/openapi_store_handler.erl | 122 +++------- .../src/openapi_user_handler.erl | 203 ++++------------- 5 files changed, 145 insertions(+), 441 deletions(-) diff --git a/samples/server/petstore/erlang-server/src/openapi_api.erl b/samples/server/petstore/erlang-server/src/openapi_api.erl index 74a125a2a3ba..f1e06fee2ffb 100644 --- a/samples/server/petstore/erlang-server/src/openapi_api.erl +++ b/samples/server/petstore/erlang-server/src/openapi_api.erl @@ -1,5 +1,7 @@ -module(openapi_api). +-include_lib("kernel/include/logger.hrl"). + -export([request_params/1]). -export([request_param_info/2]). -export([populate_request/3]). @@ -385,20 +387,17 @@ request_param_info(OperationID, Name) -> error({unknown_param, OperationID, Name}). -spec populate_request( - OperationID :: operation_id(), - Req :: cowboy_req:req(), - ValidatorState :: jesse_state:state() -) -> + 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} -> @@ -424,8 +423,7 @@ populate_request_param(OperationID, Name, Req0, ValidatorState) -> OperationID :: operation_id(), Code :: 200..599, Body :: jesse:json_term(), - ValidatorState :: jesse_state:state() -) -> ok | no_return(). + ValidatorState :: jesse_state:state()) -> ok | no_return(). validate_response('AddPet', 200, Body, ValidatorState) -> @@ -542,13 +540,10 @@ validate(Rule = required, Name, Value, _ValidatorState) -> 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, openapi_utils:to_int(Value)} @@ -556,7 +551,6 @@ validate(Rule = {type, 'integer'}, Name, Value, _ValidatorState) -> error:badarg -> validation_error(Rule, Name) end; - validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> try {ok, openapi_utils:to_float(Value)} @@ -564,16 +558,13 @@ validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> 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 @@ -585,19 +576,16 @@ validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> 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), @@ -609,50 +597,42 @@ 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)), try @@ -674,9 +654,8 @@ 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(). @@ -700,17 +679,14 @@ 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, Req}; - get_value(header, Name, Req) -> Headers = cowboy_req:headers(Req), Value = maps:get(openapi_utils:to_header(Name), Headers, undefined), {Value, Req}; - get_value(binding, Name, Req) -> Value = cowboy_req:binding(openapi_utils:to_binding(Name), Req), {Value, Req}. 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 index b3d3e99613d9..45c191f005aa 100644 --- a/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl @@ -2,6 +2,8 @@ -behaviour(openapi_logic_handler). +-include_lib("kernel/include/logger.hrl"). + -export([handle_request/3]). -export([authorize_api_key/2]). @@ -13,12 +15,10 @@ authorize_api_key(_, _) -> -spec handle_request( OperationID :: openapi_api:operation_id(), Req :: cowboy_req:req(), - Context :: #{}) -> + Context :: #{_ => _}) -> {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. handle_request(OperationID, Req, Context) -> - error_logger:error_msg( - "Got not implemented request to process: ~p~n", - [{OperationID, Req, Context}] - ), + ?LOG_ERROR(#{what => "Got not implemented request to process", + operation_id => OperationID, request => Req, context => Context}), {501, #{}, #{}}. 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 c2c16092e6b7..6caa86ba5293 100644 --- a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl @@ -1,6 +1,8 @@ %% basic handler -module(openapi_pet_handler). +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks -export([allowed_methods/2]). -export([init/2]). @@ -14,8 +16,14 @@ -export([valid_content_headers/2]). -export([valid_entity_length/2]). -%% Handlers --export([handle_request_json/2]). +-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()}. -record(state, { operation_id :: openapi_api:operation_id(), @@ -28,14 +36,13 @@ -spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - init(Req, {Operations, LogicHandler, ValidatorMod}) -> 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]), + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), State = #state{ operation_id = OperationID, @@ -47,69 +54,28 @@ init(Req, {Operations, LogicHandler, ValidatorMod}) -> -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' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'AddPet'}) -> {[<<"POST">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'DeletePet' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'DeletePet'}) -> {[<<"DELETE">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'FindPetsByStatus' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'FindPetsByStatus'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'FindPetsByTags' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'FindPetsByTags'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'GetPetById' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'GetPetById'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdatePet' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'UpdatePet'}) -> {[<<"PUT">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdatePetWithForm' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'UpdatePetWithForm'}) -> {[<<"POST">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'UploadFile' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'UploadFile'}) -> {[<<"POST">>], Req, State}; allowed_methods(Req, State) -> @@ -121,9 +87,8 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized(Req0, - #state{operation_id = 'AddPet' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'AddPet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -136,9 +101,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'DeletePet' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'DeletePet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -151,9 +115,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'FindPetsByStatus' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'FindPetsByStatus' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -166,9 +129,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'FindPetsByTags' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'FindPetsByTags' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -181,9 +143,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'GetPetById' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'GetPetById' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -196,9 +157,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'UpdatePet' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'UpdatePet' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -211,9 +171,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'UpdatePetWithForm' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'UpdatePetWithForm' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -226,9 +185,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'UploadFile' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'UploadFile' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -259,82 +217,42 @@ content_types_accepted(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' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'AddPet'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'DeletePet' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'DeletePet'} = State) -> Headers = ["api_key"], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'FindPetsByStatus' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'FindPetsByStatus'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'FindPetsByTags' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'FindPetsByTags'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetPetById' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'GetPetById'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdatePet' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'UpdatePet'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdatePetWithForm' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'UpdatePetWithForm'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UploadFile' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'UploadFile'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; @@ -356,70 +274,45 @@ content_types_provided(Req, State) -> -spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - malformed_request(Req, State) -> {false, Req, State}. -spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - allow_missing_post(Req, State) -> {false, Req, State}. -spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> processed_response(). - delete_resource(Req, State) -> handle_request_json(Req, State). -spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - known_content_type(Req, State) -> {true, Req, State}. -spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - valid_entity_length(Req, State) -> %% @TODO check the length {true, Req, State}. %%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {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. +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. -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 - } -) -> +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> case openapi_api:populate_request(OperationID, Req0, ValidatorState) of {ok, Populated, Req1} -> {Code, Headers, Body} = openapi_logic_handler:handle_request( 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 0e7cd9173d27..a0d74f76b6b6 100644 --- a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl @@ -1,6 +1,8 @@ %% basic handler -module(openapi_store_handler). +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks -export([allowed_methods/2]). -export([init/2]). @@ -14,8 +16,14 @@ -export([valid_content_headers/2]). -export([valid_entity_length/2]). -%% Handlers --export([handle_request_json/2]). +-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()}. -record(state, { operation_id :: openapi_api:operation_id(), @@ -28,14 +36,13 @@ -spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - init(Req, {Operations, LogicHandler, ValidatorMod}) -> 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]), + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), State = #state{ operation_id = OperationID, @@ -47,37 +54,16 @@ init(Req, {Operations, LogicHandler, ValidatorMod}) -> -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' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'DeleteOrder'}) -> {[<<"DELETE">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'GetInventory' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'GetInventory'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'GetOrderById' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'GetOrderById'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'PlaceOrder' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'PlaceOrder'}) -> {[<<"POST">>], Req, State}; allowed_methods(Req, State) -> @@ -89,9 +75,8 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized(Req0, - #state{operation_id = 'GetInventory' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'GetInventory' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -122,42 +107,22 @@ content_types_accepted(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' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'DeleteOrder'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetInventory' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'GetInventory'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetOrderById' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'GetOrderById'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'PlaceOrder' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'PlaceOrder'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; @@ -179,70 +144,45 @@ content_types_provided(Req, State) -> -spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - malformed_request(Req, State) -> {false, Req, State}. -spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - allow_missing_post(Req, State) -> {false, Req, State}. -spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> processed_response(). - delete_resource(Req, State) -> handle_request_json(Req, State). -spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - known_content_type(Req, State) -> {true, Req, State}. -spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - valid_entity_length(Req, State) -> %% @TODO check the length {true, Req, State}. %%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {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. +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. -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 - } -) -> +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> case openapi_api:populate_request(OperationID, Req0, ValidatorState) of {ok, Populated, Req1} -> {Code, Headers, Body} = openapi_logic_handler:handle_request( 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 0153cb73f7b1..60364311126a 100644 --- a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl @@ -1,6 +1,8 @@ %% basic handler -module(openapi_user_handler). +-include_lib("kernel/include/logger.hrl"). + %% Cowboy REST callbacks -export([allowed_methods/2]). -export([init/2]). @@ -14,8 +16,14 @@ -export([valid_content_headers/2]). -export([valid_entity_length/2]). -%% Handlers --export([handle_request_json/2]). +-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()}. -record(state, { operation_id :: openapi_api:operation_id(), @@ -28,14 +36,13 @@ -spec init(Req :: cowboy_req:req(), Opts :: openapi_router:init_opts()) -> {cowboy_rest, Req :: cowboy_req:req(), State :: state()}. - init(Req, {Operations, LogicHandler, ValidatorMod}) -> 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]), + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), State = #state{ operation_id = OperationID, @@ -47,69 +54,28 @@ init(Req, {Operations, LogicHandler, ValidatorMod}) -> -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' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'CreateUser'}) -> {[<<"POST">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'CreateUsersWithArrayInput' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'CreateUsersWithArrayInput'}) -> {[<<"POST">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'CreateUsersWithListInput' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'CreateUsersWithListInput'}) -> {[<<"POST">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'DeleteUser' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'DeleteUser'}) -> {[<<"DELETE">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'GetUserByName' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'GetUserByName'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'LoginUser' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'LoginUser'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'LogoutUser' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'LogoutUser'}) -> {[<<"GET">>], Req, State}; -allowed_methods( - Req, - State = #state{ - operation_id = 'UpdateUser' - } -) -> +allowed_methods(Req, State = #state{operation_id = 'UpdateUser'}) -> {[<<"PUT">>], Req, State}; allowed_methods(Req, State) -> @@ -121,9 +87,8 @@ allowed_methods(Req, State) -> Req :: cowboy_req:req(), State :: state() }. -is_authorized(Req0, - #state{operation_id = 'CreateUser' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'CreateUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -136,9 +101,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'CreateUsersWithArrayInput' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'CreateUsersWithArrayInput' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -151,9 +115,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'CreateUsersWithListInput' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'CreateUsersWithListInput' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -166,9 +129,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'DeleteUser' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'DeleteUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -181,9 +143,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'LogoutUser' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'LogoutUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -196,9 +157,8 @@ is_authorized(Req0, {false, AuthHeader, Req} -> {{false, AuthHeader}, Req, State} end; -is_authorized(Req0, - #state{operation_id = 'UpdateUser' = OperationID, - logic_handler = LogicHandler} = State) -> +is_authorized(Req0, #state{operation_id = 'UpdateUser' = OperationID, + logic_handler = LogicHandler} = State) -> Result = openapi_auth:authorize_api_key( LogicHandler, OperationID, @@ -229,82 +189,42 @@ content_types_accepted(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' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'CreateUser'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'CreateUsersWithArrayInput' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'CreateUsersWithListInput' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'CreateUsersWithListInput'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'DeleteUser' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'DeleteUser'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'GetUserByName' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'GetUserByName'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'LoginUser' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'LoginUser'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'LogoutUser' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'LogoutUser'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; -valid_content_headers( - Req0, - State = #state{ - operation_id = 'UpdateUser' - } -) -> +valid_content_headers(Req0, #state{operation_id = 'UpdateUser'} = State) -> Headers = [], {Result, Req} = validate_headers(Headers, Req0), {Result, Req, State}; @@ -326,70 +246,45 @@ content_types_provided(Req, State) -> -spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - malformed_request(Req, State) -> {false, Req, State}. -spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> {Value :: false, Req :: cowboy_req:req(), State :: state()}. - allow_missing_post(Req, State) -> {false, Req, State}. -spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> processed_response(). - delete_resource(Req, State) -> handle_request_json(Req, State). -spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - known_content_type(Req, State) -> {true, Req, State}. -spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> {Value :: true, Req :: cowboy_req:req(), State :: state()}. - valid_entity_length(Req, State) -> %% @TODO check the length {true, Req, State}. %%%% --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. - --type processed_response() :: {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. +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. -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 - } -) -> +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> case openapi_api:populate_request(OperationID, Req0, ValidatorState) of {ok, Populated, Req1} -> {Code, Headers, Body} = openapi_logic_handler:handle_request( From 2e9720e6be379e5059e4ec27d6bac4775dca80dc Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 20:25:26 +0200 Subject: [PATCH 05/12] Enable erlang server on CI --- .github/workflows/samples-erlang.yaml | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/.github/workflows/samples-erlang.yaml b/.github/workflows/samples-erlang.yaml index c9e0981e49c4..04d5a38c7513 100644 --- a/.github/workflows/samples-erlang.yaml +++ b/.github/workflows/samples-erlang.yaml @@ -3,33 +3,30 @@ 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/petstore/erlang-server/** - samples/client/petstore/erlang-client/** - samples/client/petstore/erlang-proper/** pull_request: paths: - #- samples/server/petstore/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/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 }} From 1260ed54d5cb1996fda1f272cba30ee1dbdb8680 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 20:44:27 +0200 Subject: [PATCH 06/12] Add echo-server testing to CI --- .github/workflows/samples-erlang.yaml | 3 + bin/configs/erlang-server-echo.yaml | 4 + .../erlang-server/.openapi-generator-ignore | 23 + .../erlang-server/.openapi-generator/FILES | 17 + .../erlang-server/.openapi-generator/VERSION | 1 + .../server/echo_api/erlang-server/README.md | 33 + samples/server/echo_api/erlang-server/pom.xml | 46 + .../echo_api/erlang-server/priv/openapi.json | 1282 +++++++++++++++++ .../echo_api/erlang-server/rebar.config | 7 + .../server/echo_api/erlang-server/rebar.lock | 23 + .../erlang-server/src/openapi.app.src | 19 + .../erlang-server/src/openapi_api.erl | 926 ++++++++++++ .../erlang-server/src/openapi_auth.erl | 45 + .../src/openapi_auth_handler.erl | 212 +++ .../src/openapi_body_handler.erl | 248 ++++ .../src/openapi_default_logic_handler.erl | 19 + .../src/openapi_form_handler.erl | 192 +++ .../src/openapi_header_handler.erl | 176 +++ .../src/openapi_logic_handler.erl | 17 + .../src/openapi_path_handler.erl | 176 +++ .../src/openapi_query_handler.erl | 248 ++++ .../erlang-server/src/openapi_router.erl | 202 +++ .../erlang-server/src/openapi_server.erl | 43 + .../erlang-server/src/openapi_utils.erl | 158 ++ 24 files changed, 4120 insertions(+) create mode 100644 bin/configs/erlang-server-echo.yaml create mode 100644 samples/server/echo_api/erlang-server/.openapi-generator-ignore create mode 100644 samples/server/echo_api/erlang-server/.openapi-generator/FILES create mode 100644 samples/server/echo_api/erlang-server/.openapi-generator/VERSION create mode 100644 samples/server/echo_api/erlang-server/README.md create mode 100644 samples/server/echo_api/erlang-server/pom.xml create mode 100644 samples/server/echo_api/erlang-server/priv/openapi.json create mode 100644 samples/server/echo_api/erlang-server/rebar.config create mode 100644 samples/server/echo_api/erlang-server/rebar.lock create mode 100644 samples/server/echo_api/erlang-server/src/openapi.app.src create mode 100644 samples/server/echo_api/erlang-server/src/openapi_api.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_auth.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_body_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_form_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_header_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_path_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_query_handler.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_router.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_server.erl create mode 100644 samples/server/echo_api/erlang-server/src/openapi_utils.erl diff --git a/.github/workflows/samples-erlang.yaml b/.github/workflows/samples-erlang.yaml index 04d5a38c7513..95cda5dc5a3b 100644 --- a/.github/workflows/samples-erlang.yaml +++ b/.github/workflows/samples-erlang.yaml @@ -3,11 +3,13 @@ name: Samples Erlang on: push: paths: + - 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/echo_api/erlang-server/** - samples/server/petstore/erlang-server/** - samples/client/petstore/erlang-client/** - samples/client/petstore/erlang-proper/** @@ -19,6 +21,7 @@ jobs: fail-fast: false matrix: sample: + - samples/server/echo_api/erlang-server/ - samples/server/petstore/erlang-server/ - samples/client/petstore/erlang-client/ - samples/client/petstore/erlang-proper/ 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/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..8c0dcafb5a67 --- /dev/null +++ b/samples/server/echo_api/erlang-server/.openapi-generator/FILES @@ -0,0 +1,17 @@ +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_default_logic_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 +src/openapi_utils.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..e1e260bc1628 --- /dev/null +++ b/samples/server/echo_api/erlang-server/README.md @@ -0,0 +1,33 @@ +# 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, net_opts => []}) + 2, Compilation http_server project + $ rebar3 compile + 3, Start erlang virtual machine + $ rebar3 shell + 4, Start project + application:ensure_all_started(http_server). 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..c9a7f6675052 --- /dev/null +++ b/samples/server/echo_api/erlang-server/rebar.config @@ -0,0 +1,7 @@ +{minimum_otp_vsn, "27"}. + +{deps, [ + {cowboy, "2.12.0"}, + {ranch, "2.1.0"}, + {jesse, "1.8.0"} +]}. diff --git a/samples/server/echo_api/erlang-server/rebar.lock b/samples/server/echo_api/erlang-server/rebar.lock new file mode 100644 index 000000000000..189addacd65e --- /dev/null +++ b/samples/server/echo_api/erlang-server/rebar.lock @@ -0,0 +1,23 @@ +{"1.2.0", +[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},0}, + {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},1}, + {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.0">>},0}, + {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, + {<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}, + {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.9.0">>},1}]}. +[ +{pkg_hash,[ + {<<"cowboy">>, <<"F276D521A1FF88B2B9B4C54D0E753DA6C66DD7BE6C9FCA3D9418B561828A3731">>}, + {<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>}, + {<<"jesse">>, <<"CF7615C3F2BE892F77BCCF736F23B4BD54A0FC686C7040431AEBA5EF7932CC4D">>}, + {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, + {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}, + {<<"rfc3339">>, <<"2075653DC9407541C84B1E15F8BDA2ABE95FB17C9694025E079583F2D19C1060">>}]}, +{pkg_hash_ext,[ + {<<"cowboy">>, <<"8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E">>}, + {<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>}, + {<<"jesse">>, <<"860EF4621DDBFB72792668929BE127E45E8B07CF19EEA264B0A9D48D36CCA41B">>}, + {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, + {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}, + {<<"rfc3339">>, <<"182314DE35C9F4180B22EB5F22916D8D7A799C1109A060C752970273A9332AD6">>}]} +]. 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..028beb782cc8 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi.app.src @@ -0,0 +1,19 @@ +{application, openapi, [ + {description, "Echo Server API"}, + {vsn, "1.0.0"}, + {registered, []}, + {applications, [ + kernel, + stdlib, + ssl, + inets, + jesse, + 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..0e532d72fdcf --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_api.erl @@ -0,0 +1,926 @@ +-module(openapi_api). + +-include_lib("kernel/include/logger.hrl"). + +-export([request_params/1]). +-export([request_param_info/2]). +-export([populate_request/3]). +-export([validate_response/4]). +%% exported to silence openapi complains +-export([get_value/3, validate_response_body/4]). + +-type operation_id() :: atom(). +-type request_param() :: atom(). + +-export_type([operation_id/0]). + +-spec request_params(OperationID :: operation_id()) -> [Param :: request_param()]. + + +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). + +-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()] +}. + + + + +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}). + +-spec populate_request( + OperationID :: operation_id(), + Req :: cowboy_req:req(), + ValidatorState :: jesse_state:state()) -> + {ok, Model :: #{}, Req :: cowboy_req:req()} | + {error, Reason :: any(), Req :: cowboy_req:req()}. +populate_request(OperationID, Req, ValidatorState) -> + Params = request_params(OperationID), + populate_request_params(OperationID, Params, Req, ValidatorState, #{}). + +populate_request_params(_, [], Req, _, Model) -> + {ok, Model, Req}; +populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) -> + case populate_request_param(OperationID, FieldParams, Req0, ValidatorState) of + {ok, K, V, Req} -> + populate_request_params(OperationID, T, Req, ValidatorState, maps:put(K, V, Model)); + Error -> + Error + end. + +populate_request_param(OperationID, Name, Req0, ValidatorState) -> + #{rules := Rules, source := Source} = request_param_info(OperationID, Name), + case get_value(Source, Name, Req0) of + {error, Reason, Req} -> + {error, Reason, Req}; + {Value, Req} -> + case prepare_param(Rules, Name, Value, ValidatorState) of + {ok, Result} -> {ok, Name, Result, Req}; + {error, Reason} -> + {error, Reason, Req} + end + end. + +-spec validate_response( + OperationID :: operation_id(), + Code :: 200..599, + Body :: jesse:json_term(), + ValidatorState :: jesse_state:state()) -> ok | no_return(). + + +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. + +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, openapi_utils:to_int(Value)} + catch + error:badarg -> + validation_error(Rule, Name) + end; +validate(Rule = {type, 'float'}, Name, Value, _ValidatorState) -> + try + {ok, openapi_utils:to_float(Value)} + catch + error:badarg -> + validation_error(Rule, Name) + end; +validate(Rule = {type, 'binary'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(_Rule = {type, 'boolean'}, _Name, Value, _ValidatorState) when is_boolean(Value) -> + {ok, Value}; +validate(Rule = {type, 'boolean'}, Name, Value, _ValidatorState) -> + V = binary_to_lower(Value), + try + case binary_to_existing_atom(V, utf8) of + B when is_boolean(B) -> {ok, B}; + _ -> validation_error(Rule, Name) + end + catch + error:badarg -> + validation_error(Rule, Name) + end; +validate(Rule = {type, 'date'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {type, 'datetime'}, Name, Value, _ValidatorState) -> + case is_binary(Value) of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {enum, Values}, Name, Value, _ValidatorState) -> + try + FormattedValue = erlang:binary_to_existing_atom(Value, utf8), + case lists:member(FormattedValue, Values) of + true -> {ok, FormattedValue}; + false -> validation_error(Rule, Name) + end + catch + error:badarg -> + validation_error(Rule, Name) + end; +validate(Rule = {max, Max}, Name, Value, _ValidatorState) -> + case Value =< Max of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {exclusive_max, ExclusiveMax}, Name, Value, _ValidatorState) -> + case Value > ExclusiveMax of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {min, Min}, Name, Value, _ValidatorState) -> + case Value >= Min of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {exclusive_min, ExclusiveMin}, Name, Value, _ValidatorState) -> + case Value =< ExclusiveMin of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {max_length, MaxLength}, Name, Value, _ValidatorState) -> + case size(Value) =< MaxLength of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {min_length, MinLength}, Name, Value, _ValidatorState) -> + case size(Value) >= MinLength of + true -> ok; + false -> validation_error(Rule, Name) + end; +validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> + {ok, MP} = re:compile(Pattern), + case re:run(Value, MP) of + {match, _} -> ok; + _ -> validation_error(Rule, Name) + end; +validate(Rule = schema, Name, Value, ValidatorState) -> + Definition = list_to_binary("#/components/schemas/" ++ openapi_utils:to_list(Name)), + try + _ = validate_with_schema(Value, Definition, ValidatorState), + ok + catch + throw:[{schema_invalid, _, Error} | _] -> + Info = #{ + type => schema_invalid, + error => Error + }, + validation_error(Rule, Name, Info); + throw:[{data_invalid, Schema, Error, _, Path} | _] -> + Info = #{ + type => data_invalid, + error => Error, + schema => Schema, + path => Path + }, + validation_error(Rule, Name, Info) + end; +validate(Rule, Name, _Value, _ValidatorState) -> + ?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 = openapi_utils:get_opt(openapi_utils: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, Req}; +get_value(binding, Name, Req) -> + Value = cowboy_req:binding(openapi_utils:to_binding(Name), Req), + {Value, Req}. + +prepare_body(Body) -> + case Body of + <<>> -> <<>>; + _ -> + try + json:decode(Body, [return_maps]) + catch + error:_ -> + {error, {invalid_body, not_json, Body}} + end + end. + +validate_with_schema(Body, Definition, ValidatorState) -> + jesse_schema_validator:validate_with_state( + [{<<"$ref">>, Definition}], + Body, + ValidatorState + ). + +prepare_param(Rules, Name, Value, ValidatorState) -> + try + Result = lists:foldl( + fun(Rule, Acc) -> + case validate(Rule, Name, Acc, ValidatorState) of + ok -> Acc; + {ok, Prepared} -> Prepared + end + end, + Value, + Rules + ), + {ok, Result} + catch + throw:Reason -> + {error, Reason} + end. + +binary_to_lower(V) when is_binary(V) -> + list_to_binary(string:to_lower(openapi_utils:to_list(V))). 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..db4a96d85875 --- /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( + 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) -> + {ApiKey, Req} = get_api_key(From, KeyParam, Req0), + case ApiKey of + undefined -> + AuthHeader = <<>>, + {false, AuthHeader, Req}; + _ -> + Result = openapi_logic_handler:authorize_api_key( + LogicHandler, + OperationID, + ApiKey + ), + case Result of + false -> + AuthHeader = <<>>, + {false, AuthHeader, Req} + end + end. + +get_api_key(header, KeyParam, Req) -> + Headers = cowboy_req:headers(Req), + { + maps:get( + openapi_utils:to_header(KeyParam), + Headers, + undefined + ), + Req + }; + +get_api_key(qs_val, KeyParam, Req) -> + QS = cowboy_req:parse_qs(Req), + { openapi_utils:get_opt(KeyParam, QS), Req}. 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..9a6acedeedfc --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl @@ -0,0 +1,212 @@ +%% basic handler +-module(openapi_auth_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestAuthHttpBasic'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestAuthHttpBearer'}) -> + {[<<"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{operation_id = 'TestAuthHttpBasic' = OperationID, + logic_handler = LogicHandler} = State) -> + Result = openapi_auth:authorize_api_key( + LogicHandler, + OperationID, + header, + "authorization", + Req0), + case Result 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, + logic_handler = LogicHandler} = State) -> + Result = openapi_auth:authorize_api_key( + LogicHandler, + OperationID, + header, + "authorization", + Req0), + case Result 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(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestAuthHttpBasic'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestAuthHttpBearer'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). 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..4d87ce93529b --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl @@ -0,0 +1,248 @@ +%% basic handler +-module(openapi_body_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestBinaryGif'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestBodyApplicationOctetstreamBinary'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyAllOfPet'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyPet'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyPetResponseString'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyStringEnum'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyTagResponseString'}) -> + {[<<"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(Req, State) -> + {true, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestBinaryGif'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyPet'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyStringEnum'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). diff --git a/samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl new file mode 100644 index 000000000000..a2f82fce981b --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl @@ -0,0 +1,19 @@ +-module(openapi_default_logic_handler). + +-behaviour(openapi_logic_handler). + +-include_lib("kernel/include/logger.hrl"). + +-export([handle_request/3]). + + +-spec handle_request( + OperationID :: openapi_api:operation_id(), + Req :: cowboy_req:req(), + Context :: #{_ => _}) -> + {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. + +handle_request(OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + operation_id => OperationID, request => Req, context => Context}), + {501, #{}, #{}}. 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..6da4aed4f1b9 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl @@ -0,0 +1,192 @@ +%% basic handler +-module(openapi_form_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestFormIntegerBooleanString'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestFormObjectMultipart'}) -> + {[<<"POST">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestFormOneof'}) -> + {[<<"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(Req, State) -> + {true, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestFormIntegerBooleanString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestFormObjectMultipart'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestFormOneof'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). 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..049e266a13b1 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl @@ -0,0 +1,176 @@ +%% basic handler +-module(openapi_header_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestHeaderIntegerBooleanStringEnums'}) -> + {[<<"GET">>], 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(Req, State) -> + {true, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) -> + Headers = ["integer_header","boolean_header","string_header","enum_nonref_string_header","enum_ref_string_header"], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). 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..2ebe9f76c4f7 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl @@ -0,0 +1,17 @@ +-module(openapi_logic_handler). + +-export([handle_request/4]). +-type context() :: #{binary() => any()}. +-type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. + +-export_type([handler_response/0]). + + +-callback handle_request(openapi_api:operation_id(), cowboy_req:req(), context()) -> + handler_response(). + +-spec handle_request(module(), openapi_api:operation_id(), cowboy_req:req(), context()) -> + handler_response(). +handle_request(Handler, OperationID, Req, Context) -> + Handler:handle_request(OperationID, 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..15d28351d34a --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl @@ -0,0 +1,176 @@ +%% basic handler +-module(openapi_path_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'}) -> + {[<<"GET">>], 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(Req, State) -> + {true, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). 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..41d2e9dc6d67 --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl @@ -0,0 +1,248 @@ +%% basic handler +-module(openapi_query_handler). + +-include_lib("kernel/include/logger.hrl"). + +%% Cowboy REST callbacks +-export([allowed_methods/2]). +-export([init/2]). +-export([allow_missing_post/2]). +-export([content_types_accepted/2]). +-export([content_types_provided/2]). +-export([delete_resource/2]). +-export([is_authorized/2]). +-export([known_content_type/2]). +-export([malformed_request/2]). +-export([valid_content_headers/2]). +-export([valid_entity_length/2]). + +-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()}. + +-record(state, { + operation_id :: openapi_api:operation_id(), + logic_handler :: module(), + validator_state :: jesse_state:state(), + context = #{} :: #{} +}). + +-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}) -> + Method = cowboy_req:method(Req), + OperationID = maps:get(Method, Operations, undefined), + + ValidatorState = ValidatorMod:get_validator_state(), + + ?LOG_INFO(#{what => "Attempt to process operation", operation_id => OperationID}), + + State = #state{ + operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState + }, + {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 = 'TestEnumRefString'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryDatetimeDateString'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryIntegerBooleanString'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'}) -> + {[<<"GET">>], Req, State}; + +allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'}) -> + {[<<"GET">>], 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(Req, State) -> + {true, Req, State}. + +-spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> + { + Value :: [{binary(), AcceptResource :: atom()}], + Req :: cowboy_req:req(), + State :: state() + }. + +content_types_accepted(Req, State) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec valid_content_headers(Req :: cowboy_req:req(), State :: state()) -> + {Value :: boolean(), Req :: cowboy_req:req(), State :: state()}. + +valid_content_headers(Req0, #state{operation_id = 'TestEnumRefString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryDatetimeDateString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, Req, State}; + +valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) -> + Headers = [], + {Result, Req} = validate_headers(Headers, Req0), + {Result, 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) -> + {[ + {<<"application/json">>, handle_request_json} + ], Req, State}. + +-spec malformed_request(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +malformed_request(Req, State) -> + {false, Req, State}. + +-spec allow_missing_post(Req :: cowboy_req:req(), State :: state()) -> + {Value :: false, Req :: cowboy_req:req(), State :: state()}. +allow_missing_post(Req, State) -> + {false, Req, State}. + +-spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> + processed_response(). +delete_resource(Req, State) -> + handle_request_json(Req, State). + +-spec known_content_type(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +known_content_type(Req, State) -> + {true, Req, State}. + +-spec valid_entity_length(Req :: cowboy_req:req(), State :: state()) -> + {Value :: true, Req :: cowboy_req:req(), State :: state()}. +valid_entity_length(Req, State) -> + %% @TODO check the length + {true, Req, State}. + +%%%% +-spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> + processed_response(). +process_response({ok, {Code, Headers, Body}}, Req0, State) -> + Req = cowboy_req:reply(Code, Headers, Body, Req0), + {stop, Req, State}; +process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> + ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), + Req = cowboy_req:reply(400, Req0), + {stop, Req, State}. + +-spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). +handle_request_json(Req0, #state{operation_id = OperationID, + logic_handler = LogicHandler, + validator_state = ValidatorState} = State) -> + 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) -> + json:encode(Body). 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..355df8d46f4f --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_router.erl @@ -0,0 +1,202 @@ +-module(openapi_router). + +-export([get_paths/1, get_validator_state/0]). + +-type operations() :: #{ + Method :: binary() => openapi_api:operation_id() +}. + +-type init_opts() :: { + Operations :: operations(), + LogicHandler :: atom(), + ValidatorMod :: module() +}. + +-export_type([init_opts/0]). + +-spec get_paths(LogicHandler :: atom()) -> 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] + } + ]. + +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' + } + }. + +get_validator_state() -> + persistent_term:get({?MODULE, validator_state}). + +prepare_validator() -> + {ok, FileContents} = file:read_file(get_openapi_path()), + R = json:decode(FileContents), + 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/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..d0299408d519 --- /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_default_logic_handler). + +-export([start/2]). + +-spec start(ID :: term(), #{ + transport := tcp | ssl, + transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), + logic_handler => module(), + net_opts => [] +}) -> {ok, pid()} | {error, any()}. + +start(ID, #{transport := Transport, + transport_opts := TransportOpts, + protocol_opts := ProtocolOpts} = 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/echo_api/erlang-server/src/openapi_utils.erl b/samples/server/echo_api/erlang-server/src/openapi_utils.erl new file mode 100644 index 000000000000..22c7867236be --- /dev/null +++ b/samples/server/echo_api/erlang-server/src/openapi_utils.erl @@ -0,0 +1,158 @@ +-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. From bb1cb46c830420ca0111d1f09c23a3e72ca4f918 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 27 Aug 2024 20:47:57 +0200 Subject: [PATCH 07/12] Require OTP27 explicitly in the generated rebar.config file --- .../src/main/resources/erlang-server/rebar.config.mustache | 2 ++ samples/server/petstore/erlang-server/rebar.config | 2 ++ 2 files changed, 4 insertions(+) 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 fba9fe53e3e2..c9a7f6675052 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,3 +1,5 @@ +{minimum_otp_vsn, "27"}. + {deps, [ {cowboy, "2.12.0"}, {ranch, "2.1.0"}, diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index fba9fe53e3e2..c9a7f6675052 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -1,3 +1,5 @@ +{minimum_otp_vsn, "27"}. + {deps, [ {cowboy, "2.12.0"}, {ranch, "2.1.0"}, From 21b013000b93bd0035031df1f5139ac67f5ab529 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Thu, 29 Aug 2024 11:57:11 +0200 Subject: [PATCH 08/12] Rework handler and API With this work, json validation becomes optional, fully implemented in the `_api` module as it was before, but without being forcibly called by the `_handler`. It is instead left as optional for the user to take advantage of the exposed callbacks. Jesse also chooses draft-06 as a default, but these can be chosen manually by the user too, as long as jesse implements them. `_handler` also becomes lighter, it now handles all mime types transparently by forwarding to a user-given module that must implement `accept_callback/4` and `provide_callback/4` as described in the `_logic_handler` callbacks. These will simply be the return values of cowboy_rest's `content_types_accepted` and `content_types_provided` respectively, and should simply comply with their defined APIs. They only get two parameters extending the behaviour, so that the user-given callback can pattern-match on them: the path prefix of the logic handler, and the operationID of the call. --- .../languages/ErlangServerCodegen.java | 4 +- .../resources/erlang-server/README.mustache | 7 +- .../main/resources/erlang-server/api.mustache | 331 ++++++--- .../resources/erlang-server/app.src.mustache | 30 +- .../resources/erlang-server/auth.mustache | 58 +- .../default_logic_handler.mustache | 32 - .../resources/erlang-server/handler.mustache | 243 +++---- .../erlang-server/logic_handler.mustache | 95 +-- .../erlang-server/rebar.config.mustache | 5 + .../resources/erlang-server/router.mustache | 74 +- .../resources/erlang-server/server.mustache | 26 +- .../resources/erlang-server/utils.mustache | 158 ----- .../erlang-server/.openapi-generator/FILES | 2 - .../server/echo_api/erlang-server/README.md | 7 +- .../echo_api/erlang-server/rebar.config | 5 + .../server/echo_api/erlang-server/rebar.lock | 23 - .../erlang-server/src/openapi.app.src | 30 +- .../erlang-server/src/openapi_api.erl | 649 +++++++++--------- .../erlang-server/src/openapi_auth.erl | 54 +- .../src/openapi_auth_handler.erl | 242 +++---- .../src/openapi_body_handler.erl | 362 +++++----- .../src/openapi_default_logic_handler.erl | 19 - .../src/openapi_form_handler.erl | 238 +++---- .../src/openapi_header_handler.erl | 200 ++---- .../src/openapi_logic_handler.erl | 56 +- .../src/openapi_path_handler.erl | 200 ++---- .../src/openapi_query_handler.erl | 344 ++++------ .../erlang-server/src/openapi_router.erl | 74 +- .../erlang-server/src/openapi_server.erl | 26 +- .../erlang-server/src/openapi_utils.erl | 158 ----- .../erlang-server/.openapi-generator/FILES | 2 - .../server/petstore/erlang-server/README.md | 7 +- .../petstore/erlang-server/rebar.config | 5 + .../erlang-server/src/openapi.app.src | 30 +- .../erlang-server/src/openapi_api.erl | 600 ++++++++-------- .../erlang-server/src/openapi_auth.erl | 54 +- .../src/openapi_default_logic_handler.erl | 24 - .../src/openapi_logic_handler.erl | 62 +- .../erlang-server/src/openapi_pet_handler.erl | 427 +++++------- .../erlang-server/src/openapi_router.erl | 74 +- .../erlang-server/src/openapi_server.erl | 26 +- .../src/openapi_store_handler.erl | 263 +++---- .../src/openapi_user_handler.erl | 388 ++++------- .../erlang-server/src/openapi_utils.erl | 158 ----- 44 files changed, 2358 insertions(+), 3514 deletions(-) delete mode 100644 modules/openapi-generator/src/main/resources/erlang-server/default_logic_handler.mustache delete mode 100644 modules/openapi-generator/src/main/resources/erlang-server/utils.mustache delete mode 100644 samples/server/echo_api/erlang-server/rebar.lock delete mode 100644 samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl delete mode 100644 samples/server/echo_api/erlang-server/src/openapi_utils.erl delete mode 100644 samples/server/petstore/erlang-server/src/openapi_default_logic_handler.erl delete mode 100644 samples/server/petstore/erlang-server/src/openapi_utils.erl 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 c0f7e4fb36de..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 @@ -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 e1e260bc1628..887f730062fe 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/README.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/README.mustache @@ -24,10 +24,13 @@ Use erlang-server with rebar3 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 + 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/modules/openapi-generator/src/main/resources/erlang-server/api.mustache b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache index f12c0b81feeb..f876b3a5361f 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -1,39 +1,60 @@ -module({{packageName}}_api). +-moduledoc """ +This module offers an API for JSON schema validation, using `jesse` under the hood. --include_lib("kernel/include/logger.hrl"). +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([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]). +-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]). --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()} | @@ -46,44 +67,29 @@ 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#">>). -{{#apiInfo}}{{#apis}} -{{#operations}}{{#operation}}{{#allParams}} -request_param_info('{{operationId}}', {{^isBodyParam}}'{{baseName}}'{{/isBodyParam}}{{#isBodyParam}}'{{dataType}}'{{/isBodyParam}}) -> - #{ - source => {{#isQueryParam}}qs_val{{/isQueryParam}} {{#isPathParam}}binding{{/isPathParam}} {{#isHeaderParam}}header{{/isHeaderParam}}{{#isBodyParam}}body{{/isBodyParam}}{{#isFormParam}}body{{/isFormParam}}, - rules => [{{#isString}} - {type, 'binary'},{{/isString}}{{#isInteger}} - {type, 'integer'},{{/isInteger}}{{#isLong}} - {type, 'integer'},{{/isLong}}{{#isFloat}} - {type, 'float'},{{/isFloat}}{{#isDouble}} - {type, 'float'},{{/isDouble}}{{#isByteArray}} - {type, 'binary'},{{/isByteArray}}{{#isBinary}} - {type, 'binary'},{{/isBinary}}{{#isBoolean}} - {type, 'boolean'},{{/isBoolean}}{{#isDate}} - {type, 'date'},{{/isDate}}{{#isDateTime}} - {type, 'datetime'},{{/isDateTime}}{{#isEnum}} - {enum, [{{#allowableValues}}{{#values}}'{{.}}'{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}] },{{/isEnum}}{{#maximum}} - {max, {{maximum}} }, {{/maximum}}{{#exclusiveMaximum}} - {exclusive_max, {{exclusiveMaximum}} },{{/exclusiveMaximum}}{{#minimum}} - {min, {{minimum}} },{{/minimum}}{{#exclusiveMinimum}} - {exclusive_min, {{exclusiveMinimum}} },{{/exclusiveMinimum}}{{#maxLength}} - {max_length, {{maxLength}} },{{/maxLength}}{{#minLength}} - {min_length, {{minLength}} },{{/minLength}}{{#pattern}} - {pattern, "{{{pattern}}}" },{{/pattern}}{{#isBodyParam}} - schema,{{/isBodyParam}}{{#required}} - required{{/required}}{{^required}} - not_required{{/required}} - ] - }; -{{/allParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}} -request_param_info(OperationID, Name) -> - error({unknown_param, OperationID, Name}). +-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(), @@ -94,6 +100,63 @@ 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 | 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. + +%%% +-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}}, + rules => [{{#isString}} + {type, binary},{{/isString}}{{#isInteger}} + {type, integer},{{/isInteger}}{{#isLong}} + {type, integer},{{/isLong}}{{#isFloat}} + {type, float},{{/isFloat}}{{#isDouble}} + {type, float},{{/isDouble}}{{#isByteArray}} + {type, binary},{{/isByteArray}}{{#isBinary}} + {type, binary},{{/isBinary}}{{#isBoolean}} + {type, boolean},{{/isBoolean}}{{#isDate}} + {type, date},{{/isDate}}{{#isDateTime}} + {type, datetime},{{/isDateTime}}{{#isEnum}} + {enum, [{{#allowableValues}}{{#values}}'{{.}}'{{^-last}}, {{/-last}}{{/values}}{{/allowableValues}}] },{{/isEnum}}{{#maximum}} + {max, {{maximum}}},{{/maximum}}{{#exclusiveMaximum}} + {exclusive_max, {{exclusiveMaximum}}},{{/exclusiveMaximum}}{{#minimum}} + {min, {{minimum}}},{{/minimum}}{{#exclusiveMinimum}} + {exclusive_min, {{exclusiveMinimum}}},{{/exclusiveMinimum}}{{#maxLength}} + {max_length, {{maxLength}}},{{/maxLength}}{{#minLength}} + {min_length, {{minLength}}},{{/minLength}}{{#pattern}} + {pattern, "{{{pattern}}}"},{{/pattern}}{{#isBodyParam}} + schema,{{/isBodyParam}}{{#required}} + required{{/required}}{{^required}} + not_required{{/required}} + ] + }; +{{/allParams}}{{/operation}}{{/operations}}{{/apis}}{{/apiInfo}}request_param_info(OperationID, Name) -> + error({unknown_param, OperationID, Name}). + populate_request_params(_, [], Req, _, Model) -> {ok, Model, Req}; populate_request_params(OperationID, [FieldParams | T], Req0, ValidatorState, Model) -> @@ -117,23 +180,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}} - -validate_response(_OperationID, _Code, _Body, _ValidatorState) -> - ok. +-include_lib("kernel/include/logger.hrl"). -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ validate(schema, ReturnBaseType, Item, ValidatorState) || Item <- Body]; @@ -141,7 +190,6 @@ 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); @@ -151,28 +199,28 @@ 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 @@ -183,12 +231,12 @@ 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) @@ -241,7 +289,7 @@ validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> _ -> 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 @@ -266,12 +314,10 @@ validate(Rule, Name, _Value, _ValidatorState) -> 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}). @@ -288,26 +334,24 @@ get_value(body, _Name, Req0) -> 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 - json: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) -> @@ -335,5 +379,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()) -> number(). +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: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. + +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 e4c812a76dd2..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, - jesse, - ranch, - 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 94b9da0aed17..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,50 +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 = <<>>, {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 ce638658e42c..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). - --include_lib("kernel/include/logger.hrl"). - --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 :: json:encode_value()}. - -handle_request(OperationID, Req, Context) -> - ?LOG_ERROR(#{what => "Got not implemented request to process", - operation_id => OperationID, request => Req, context => 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 0b1d5e6525b1..3415cdd8db3c 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -1,93 +1,61 @@ %% 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]). +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-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()}). --type processed_response() :: {stop, cowboy_req:req(), state()}. +-type state() :: #state{}. --record(state, { - operation_id :: {{packageName}}_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - context = #{} :: #{} -}). - --type state() :: state(). - --spec init(Req :: cowboy_req:req(), Opts :: {{packageName}}_router:init_opts()) -> - {cowboy_rest, Req :: cowboy_req:req(), 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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.size}} -is_authorized(Req0, #state{operation_id = '{{operationId}}' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = {{packageName}}_auth:authorize_api_key( - LogicHandler, - OperationID, -{{#isApiKey.isKeyInQuery}} - qs_val, -{{/isApiKey.isKeyInQuery}} -{{^isApiKey.isKeyInQuery}} - header, -{{/isApiKey.isKeyInQuery}} -{{#isApiKey}} - "{{keyParamName}}", -{{/isApiKey}} -{{^isApiKey}} - "authorization", -{{/isApiKey}} - Req0), - case Result of +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} -> @@ -99,108 +67,63 @@ is_authorized(Req0, #state{operation_id = '{{operationId}}' = OperationID, is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-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{operation_id = '{{operationId}}'} = State) -> - 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}. + {{#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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) + 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) -> - json: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(). +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 09439b6c9e30..31c58c741cb7 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,46 +1,55 @@ -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()). -type context() :: #{binary() => any()}. --type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. - --export_type([handler_response/0]). - -{{#authMethods}} - {{#isApiKey}} --callback authorize_api_key({{packageName}}_api:operation_id(), binary()) -> - boolean() | {boolean(), context()}. - {{/isApiKey}} -{{/authMethods}} - --callback handle_request({{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). - --spec handle_request(module(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). - -{{#authMethods}} - {{#isApiKey}} --spec authorize_api_key(module(), {{packageName}}_api:operation_id(), binary()) -> - Result :: false | {true, context()}. -authorize_api_key(Handler, OperationID, ApiKey) -> - Handler:authorize_api_key(OperationID, ApiKey). - {{/isApiKey}} -{{/authMethods}} -{{^authMethods}} --spec authorize_api_key(module(), {{packageName}}_api:operation_id(), binary()) -> 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(). + +-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(). +provide_callback(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => 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 c9a7f6675052..deca85d55f32 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 @@ -5,3 +5,8 @@ {ranch, "2.1.0"}, {jesse, "1.8.0"} ]}. + +{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. + +{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 4f548859b752..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,52 +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()) -> cowboy_router:routes(). +-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}} @@ -56,17 +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() -> - {ok, FileContents} = file:read_file(get_openapi_path()), - R = json:decode(FileContents), - 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 9e69f86dedc9..0f7671443ea8 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/server.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/server.mustache @@ -1,19 +1,19 @@ -module({{packageName}}_server). --define(DEFAULT_LOGIC_HANDLER, {{packageName}}_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, {{packageName}}_logic_handler). -export([start/2]). - --spec start(ID :: term(), #{ - transport := tcp | ssl, - transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), - logic_handler => module(), - net_opts => [] -}) -> {ok, pid()} | {error, any()}. - -start(ID, #{transport := Transport, - transport_opts := TransportOpts, - protocol_opts := ProtocolOpts} = Params) -> +-ignore_xref([start/2]). + +-spec start(term(), #{transport => tcp | ssl, + transport_opts => ranch:transport_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 @@ -27,7 +27,7 @@ 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) -> +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), 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 97fc713e0fad..000000000000 --- a/modules/openapi-generator/src/main/resources/erlang-server/utils.mustache +++ /dev/null @@ -1,158 +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/FILES b/samples/server/echo_api/erlang-server/.openapi-generator/FILES index 8c0dcafb5a67..3aa7956b7dab 100644 --- a/samples/server/echo_api/erlang-server/.openapi-generator/FILES +++ b/samples/server/echo_api/erlang-server/.openapi-generator/FILES @@ -6,7 +6,6 @@ src/openapi_api.erl src/openapi_auth.erl src/openapi_auth_handler.erl src/openapi_body_handler.erl -src/openapi_default_logic_handler.erl src/openapi_form_handler.erl src/openapi_header_handler.erl src/openapi_logic_handler.erl @@ -14,4 +13,3 @@ src/openapi_path_handler.erl src/openapi_query_handler.erl src/openapi_router.erl src/openapi_server.erl -src/openapi_utils.erl diff --git a/samples/server/echo_api/erlang-server/README.md b/samples/server/echo_api/erlang-server/README.md index e1e260bc1628..887f730062fe 100644 --- a/samples/server/echo_api/erlang-server/README.md +++ b/samples/server/echo_api/erlang-server/README.md @@ -24,10 +24,13 @@ Use erlang-server with rebar3 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 + 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/rebar.config b/samples/server/echo_api/erlang-server/rebar.config index c9a7f6675052..deca85d55f32 100644 --- a/samples/server/echo_api/erlang-server/rebar.config +++ b/samples/server/echo_api/erlang-server/rebar.config @@ -5,3 +5,8 @@ {ranch, "2.1.0"}, {jesse, "1.8.0"} ]}. + +{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. + +{xref_checks, + [undefined_function_calls, deprecated_function_calls, deprecated_functions]}. diff --git a/samples/server/echo_api/erlang-server/rebar.lock b/samples/server/echo_api/erlang-server/rebar.lock deleted file mode 100644 index 189addacd65e..000000000000 --- a/samples/server/echo_api/erlang-server/rebar.lock +++ /dev/null @@ -1,23 +0,0 @@ -{"1.2.0", -[{<<"cowboy">>,{pkg,<<"cowboy">>,<<"2.12.0">>},0}, - {<<"cowlib">>,{pkg,<<"cowlib">>,<<"2.13.0">>},1}, - {<<"jesse">>,{pkg,<<"jesse">>,<<"1.8.0">>},0}, - {<<"jsx">>,{pkg,<<"jsx">>,<<"3.1.0">>},1}, - {<<"ranch">>,{pkg,<<"ranch">>,<<"2.1.0">>},0}, - {<<"rfc3339">>,{pkg,<<"rfc3339">>,<<"0.9.0">>},1}]}. -[ -{pkg_hash,[ - {<<"cowboy">>, <<"F276D521A1FF88B2B9B4C54D0E753DA6C66DD7BE6C9FCA3D9418B561828A3731">>}, - {<<"cowlib">>, <<"DB8F7505D8332D98EF50A3EF34B34C1AFDDEC7506E4EE4DD4A3A266285D282CA">>}, - {<<"jesse">>, <<"CF7615C3F2BE892F77BCCF736F23B4BD54A0FC686C7040431AEBA5EF7932CC4D">>}, - {<<"jsx">>, <<"D12516BAA0BB23A59BB35DCCAF02A1BD08243FCBB9EFE24F2D9D056CCFF71268">>}, - {<<"ranch">>, <<"2261F9ED9574DCFCC444106B9F6DA155E6E540B2F82BA3D42B339B93673B72A3">>}, - {<<"rfc3339">>, <<"2075653DC9407541C84B1E15F8BDA2ABE95FB17C9694025E079583F2D19C1060">>}]}, -{pkg_hash_ext,[ - {<<"cowboy">>, <<"8A7ABE6D183372CEB21CAA2709BEC928AB2B72E18A3911AA1771639BEF82651E">>}, - {<<"cowlib">>, <<"E1E1284DC3FC030A64B1AD0D8382AE7E99DA46C3246B815318A4B848873800A4">>}, - {<<"jesse">>, <<"860EF4621DDBFB72792668929BE127E45E8B07CF19EEA264B0A9D48D36CCA41B">>}, - {<<"jsx">>, <<"0C5CC8FDC11B53CC25CF65AC6705AD39E54ECC56D1C22E4ADB8F5A53FB9427F3">>}, - {<<"ranch">>, <<"244EE3FA2A6175270D8E1FC59024FD9DBC76294A321057DE8F803B1479E76916">>}, - {<<"rfc3339">>, <<"182314DE35C9F4180B22EB5F22916D8D7A799C1109A060C752970273A9332AD6">>}]} -]. diff --git a/samples/server/echo_api/erlang-server/src/openapi.app.src b/samples/server/echo_api/erlang-server/src/openapi.app.src index 028beb782cc8..95507ce40e9c 100644 --- a/samples/server/echo_api/erlang-server/src/openapi.app.src +++ b/samples/server/echo_api/erlang-server/src/openapi.app.src @@ -1,19 +1,11 @@ -{application, openapi, [ - {description, "Echo Server API"}, - {vsn, "1.0.0"}, - {registered, []}, - {applications, [ - kernel, - stdlib, - ssl, - inets, - jesse, - ranch, - cowboy - ]}, - {env, [ - ]}, - {modules, []}, - {licenses, ["Apache 2.0"]}, - {links, []} -]}. +{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 index 0e532d72fdcf..fd60a684c82a 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_api.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_api.erl @@ -1,93 +1,228 @@ -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. +``` +""". --include_lib("kernel/include/logger.hrl"). +-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 | 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', @@ -97,8 +232,6 @@ request_params('TestFormOneof') -> 'id', 'name' ]; - - request_params('TestHeaderIntegerBooleanStringEnums') -> [ 'integer_header', @@ -107,8 +240,6 @@ request_params('TestHeaderIntegerBooleanStringEnums') -> 'enum_nonref_string_header', 'enum_ref_string_header' ]; - - request_params('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}') -> [ 'path_string', @@ -116,490 +247,395 @@ request_params('TestsPathString{pathString}Integer{pathInteger}{enumNonrefString '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). --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('TestBodyApplicationOctetstreamBinary', 'file') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, schema, not_required ] }; - request_param_info('TestBodyMultipartFormdataArrayOfBinary', 'files') -> #{ - source => body, + source => body, rules => [ required ] }; - request_param_info('TestBodyMultipartFormdataSingleBinary', 'my-file') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestEchoBodyAllOfPet', 'Pet') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - request_param_info('TestEchoBodyFreeFormObjectResponseString', 'object') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - request_param_info('TestEchoBodyPet', 'Pet') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - request_param_info('TestEchoBodyPetResponseString', 'Pet') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - request_param_info('TestEchoBodyStringEnum', 'binary') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - request_param_info('TestEchoBodyTagResponseString', 'Tag') -> #{ - source => body, + source => body, rules => [ schema, not_required ] }; - - request_param_info('TestFormIntegerBooleanString', 'integer_form') -> #{ - source => body, + source => body, rules => [ - {type, 'integer'}, + {type, integer}, not_required ] }; - request_param_info('TestFormIntegerBooleanString', 'boolean_form') -> #{ - source => body, + source => body, rules => [ - {type, 'boolean'}, + {type, boolean}, not_required ] }; - request_param_info('TestFormIntegerBooleanString', 'string_form') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestFormObjectMultipart', 'marker') -> #{ - source => body, + source => body, rules => [ required ] }; - request_param_info('TestFormOneof', 'form1') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestFormOneof', 'form2') -> #{ - source => body, + source => body, rules => [ - {type, 'integer'}, + {type, integer}, not_required ] }; - request_param_info('TestFormOneof', 'form3') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestFormOneof', 'form4') -> #{ - source => body, + source => body, rules => [ - {type, 'boolean'}, + {type, boolean}, not_required ] }; - request_param_info('TestFormOneof', 'id') -> #{ - source => body, + source => body, rules => [ - {type, 'integer'}, + {type, integer}, not_required ] }; - request_param_info('TestFormOneof', 'name') -> #{ - source => body, + source => body, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - - request_param_info('TestHeaderIntegerBooleanStringEnums', 'integer_header') -> #{ - source => header, + source => header, rules => [ - {type, 'integer'}, + {type, integer}, not_required ] }; - request_param_info('TestHeaderIntegerBooleanStringEnums', 'boolean_header') -> #{ - source => header, + source => header, rules => [ - {type, 'boolean'}, + {type, boolean}, not_required ] }; - request_param_info('TestHeaderIntegerBooleanStringEnums', 'string_header') -> #{ - source => header, + source => header, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestHeaderIntegerBooleanStringEnums', 'enum_nonref_string_header') -> #{ - source => header, + source => header, rules => [ - {type, 'binary'}, + {type, binary}, {enum, ['success', 'failure', 'unclassified'] }, not_required ] }; - request_param_info('TestHeaderIntegerBooleanStringEnums', 'enum_ref_string_header') -> #{ - source => header, + source => header, rules => [ not_required ] }; - - request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'path_string') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, required ] }; - request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'path_integer') -> #{ - source => binding , + source => binding, rules => [ - {type, 'integer'}, + {type, integer}, required ] }; - request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'enum_nonref_string_path') -> #{ - source => binding , + source => binding, rules => [ - {type, 'binary'}, + {type, binary}, {enum, ['success', 'failure', 'unclassified'] }, required ] }; - request_param_info('TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}', 'enum_ref_string_path') -> #{ - source => binding , + source => binding, rules => [ required ] }; - - request_param_info('TestEnumRefString', 'enum_nonref_string_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'binary'}, + {type, binary}, {enum, ['success', 'failure', 'unclassified'] }, not_required ] }; - request_param_info('TestEnumRefString', 'enum_ref_string_query') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryDatetimeDateString', 'datetime_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'datetime'}, + {type, datetime}, not_required ] }; - request_param_info('TestQueryDatetimeDateString', 'date_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'date'}, + {type, date}, not_required ] }; - request_param_info('TestQueryDatetimeDateString', 'string_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestQueryIntegerBooleanString', 'integer_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'integer'}, + {type, integer}, not_required ] }; - request_param_info('TestQueryIntegerBooleanString', 'boolean_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'boolean'}, + {type, boolean}, not_required ] }; - request_param_info('TestQueryIntegerBooleanString', 'string_query') -> #{ - source => qs_val , + source => qs_val, rules => [ - {type, 'binary'}, + {type, binary}, not_required ] }; - request_param_info('TestQueryStyleDeepObjectExplodeTrueObject', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleDeepObjectExplodeTrueObjectAllOf', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleFormExplodeFalseArrayInteger', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleFormExplodeFalseArrayString', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleFormExplodeTrueArrayString', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleFormExplodeTrueObject', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_required ] }; - request_param_info('TestQueryStyleFormExplodeTrueObjectAllOf', 'query_object') -> #{ - source => qs_val , + source => qs_val, rules => [ not_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) -> @@ -623,104 +659,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('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. +-include_lib("kernel/include/logger.hrl"). -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ validate(schema, ReturnBaseType, Item, ValidatorState) || Item <- Body]; @@ -728,7 +669,6 @@ 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); @@ -738,28 +678,28 @@ 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 @@ -770,12 +710,12 @@ 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) @@ -828,7 +768,7 @@ validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> _ -> 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 @@ -853,12 +793,10 @@ validate(Rule, Name, _Value, _ValidatorState) -> 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}). @@ -875,26 +813,24 @@ get_value(body, _Name, Req0) -> 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 - json: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) -> @@ -922,5 +858,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()) -> number(). +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: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. + +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 index db4a96d85875..0e7beb1132c4 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_auth.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_auth.erl @@ -2,44 +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 = <<>>, {false, AuthHeader, Req}; _ -> - Result = openapi_logic_handler:authorize_api_key( - LogicHandler, - OperationID, - ApiKey - ), - case Result of - false -> - AuthHeader = <<>>, + 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( - 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/echo_api/erlang-server/src/openapi_auth_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl index 9a6acedeedfc..e2a7aacca1cc 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl @@ -1,97 +1,69 @@ %% basic handler -module(openapi_auth_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), state()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestAuthHttpBasic'}) -> +-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 = #state{operation_id = 'TestAuthHttpBearer'}) -> +allowed_methods(Req, #state{operation_id = 'TestAuthHttpBearer'} = 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{operation_id = 'TestAuthHttpBasic' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +-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, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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} -> @@ -100,113 +72,55 @@ is_authorized(Req0, #state{operation_id = 'TestAuthHttpBearer' = OperationID, is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: 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) -> - {[ - {<<"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{operation_id = 'TestAuthHttpBasic'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestAuthHttpBearer'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, 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(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 = 'TestAuthHttpBasic'} = 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(auth, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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 index 4d87ce93529b..7d1622c89fd3 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl @@ -1,248 +1,206 @@ %% basic handler -module(openapi_body_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), state()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestBinaryGif'}) -> +-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 = #state{operation_id = 'TestBodyApplicationOctetstreamBinary'}) -> +allowed_methods(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'}) -> +allowed_methods(Req, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'}) -> +allowed_methods(Req, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyAllOfPet'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyPet'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyPet'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyPetResponseString'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyStringEnum'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyStringEnum'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestEchoBodyTagResponseString'}) -> +allowed_methods(Req, #state{operation_id = 'TestEchoBodyTagResponseString'} = 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() - }. +-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(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()}. +content_types_accepted(Req, #state{operation_id = 'TestBinaryGif'} = State) -> + {[], Req, State}; +content_types_accepted(Req, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = 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{operation_id = 'TestBinaryGif'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestBodyApplicationOctetstreamBinary'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestBodyMultipartFormdataArrayOfBinary'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestBodyMultipartFormdataSingleBinary'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyAllOfPet'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyFreeFormObjectResponseString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyPet'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyPetResponseString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyStringEnum'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestEchoBodyTagResponseString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, 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(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 = 'TestBinaryGif'} = 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(body, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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_default_logic_handler.erl b/samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl deleted file mode 100644 index a2f82fce981b..000000000000 --- a/samples/server/echo_api/erlang-server/src/openapi_default_logic_handler.erl +++ /dev/null @@ -1,19 +0,0 @@ --module(openapi_default_logic_handler). - --behaviour(openapi_logic_handler). - --include_lib("kernel/include/logger.hrl"). - --export([handle_request/3]). - - --spec handle_request( - OperationID :: openapi_api:operation_id(), - Req :: cowboy_req:req(), - Context :: #{_ => _}) -> - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: json:encode_value()}. - -handle_request(OperationID, Req, Context) -> - ?LOG_ERROR(#{what => "Got not implemented request to process", - operation_id => OperationID, request => Req, context => Context}), - {501, #{}, #{}}. 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 index 6da4aed4f1b9..d2b49bcbab3c 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl @@ -1,192 +1,124 @@ %% basic handler -module(openapi_form_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), state()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestFormIntegerBooleanString'}) -> +-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 = #state{operation_id = 'TestFormObjectMultipart'}) -> +allowed_methods(Req, #state{operation_id = 'TestFormObjectMultipart'} = State) -> {[<<"POST">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestFormOneof'}) -> +allowed_methods(Req, #state{operation_id = 'TestFormOneof'} = 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() - }. +-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(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()}. +content_types_accepted(Req, #state{operation_id = 'TestFormIntegerBooleanString'} = 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{operation_id = 'TestFormIntegerBooleanString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestFormObjectMultipart'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestFormOneof'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, 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(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 = 'TestFormIntegerBooleanString'} = 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(form, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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 index 049e266a13b1..fb12e229bc3b 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl @@ -1,176 +1,98 @@ %% basic handler -module(openapi_header_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), 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()}). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestHeaderIntegerBooleanStringEnums'}) -> +-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(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()}. is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: 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) -> - {[ - {<<"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{operation_id = 'TestHeaderIntegerBooleanStringEnums'} = State) -> - Headers = ["integer_header","boolean_header","string_header","enum_nonref_string_header","enum_ref_string_header"], - {Result, Req} = validate_headers(Headers, Req0), - {Result, 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(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 = 'TestHeaderIntegerBooleanStringEnums'} = 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}. + {<<"text/plain">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, State) -> + {[], Req, State}. --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(header, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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 index 2ebe9f76c4f7..4ff7ea6e10ed 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl @@ -1,17 +1,55 @@ -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()). -type context() :: #{binary() => any()}. --type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. --export_type([handler_response/0]). +-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(). +-export([api_key_callback/2, accept_callback/4, provide_callback/4]). +-ignore_xref([api_key_callback/2, accept_callback/4, provide_callback/4]). --callback handle_request(openapi_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). +-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 handle_request(module(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). +-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(). +provide_callback(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => 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 index 15d28351d34a..19cd834ddf1a 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl @@ -1,176 +1,98 @@ %% basic handler -module(openapi_path_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), 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()}). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'}) -> +-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(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()}. is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: 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) -> - {[ - {<<"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{operation_id = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, 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(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 = 'TestsPathString{pathString}Integer{pathInteger}{enumNonrefStringPath}{enumRefStringPath}'} = 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}. + {<<"text/plain">>, handle_type_provided} + ], Req, State}; +content_types_provided(Req, State) -> + {[], Req, State}. --spec delete_resource(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(path, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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 index 41d2e9dc6d67..ad5f7f2666e1 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl @@ -1,248 +1,188 @@ %% basic handler -module(openapi_query_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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), 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()}). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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 = 'TestEnumRefString'}) -> +-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 = #state{operation_id = 'TestQueryDatetimeDateString'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryDatetimeDateString'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryIntegerBooleanString'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) -> {[<<"GET">>], Req, State}; - -allowed_methods(Req, State = #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'}) -> +allowed_methods(Req, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) -> {[<<"GET">>], 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() - }. +-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(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: 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) -> - {[ - {<<"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{operation_id = 'TestEnumRefString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryDatetimeDateString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryIntegerBooleanString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObject'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleDeepObjectExplodeTrueObjectAllOf'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayInteger'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeFalseArrayString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueArrayString'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueObject'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'TestQueryStyleFormExplodeTrueObjectAllOf'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, 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(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 = 'TestEnumRefString'} = 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(query, OperationID, Req, State#state.context). + +-spec handle_type_provided(cowboy_req:req(), state()) -> + cowboy_req:resp_body(). +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 index 355df8d46f4f..84cee5a6256d 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_router.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_router.erl @@ -1,52 +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()) -> cowboy_router:routes(). +-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() -> #{ @@ -186,17 +170,3 @@ get_operations() -> handler => 'openapi_query_handler' } }. - -get_validator_state() -> - persistent_term:get({?MODULE, validator_state}). - -prepare_validator() -> - {ok, FileContents} = file:read_file(get_openapi_path()), - R = json:decode(FileContents), - 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/echo_api/erlang-server/src/openapi_server.erl b/samples/server/echo_api/erlang-server/src/openapi_server.erl index d0299408d519..e7e4e601e4ae 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_server.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_server.erl @@ -1,19 +1,19 @@ -module(openapi_server). --define(DEFAULT_LOGIC_HANDLER, openapi_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, openapi_logic_handler). -export([start/2]). - --spec start(ID :: term(), #{ - transport := tcp | ssl, - transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), - logic_handler => module(), - net_opts => [] -}) -> {ok, pid()} | {error, any()}. - -start(ID, #{transport := Transport, - transport_opts := TransportOpts, - protocol_opts := ProtocolOpts} = Params) -> +-ignore_xref([start/2]). + +-spec start(term(), #{transport => tcp | ssl, + transport_opts => ranch:transport_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 @@ -27,7 +27,7 @@ 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) -> +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), diff --git a/samples/server/echo_api/erlang-server/src/openapi_utils.erl b/samples/server/echo_api/erlang-server/src/openapi_utils.erl deleted file mode 100644 index 22c7867236be..000000000000 --- a/samples/server/echo_api/erlang-server/src/openapi_utils.erl +++ /dev/null @@ -1,158 +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. 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 e1e260bc1628..887f730062fe 100644 --- a/samples/server/petstore/erlang-server/README.md +++ b/samples/server/petstore/erlang-server/README.md @@ -24,10 +24,13 @@ Use erlang-server with rebar3 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 + 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/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index c9a7f6675052..deca85d55f32 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -5,3 +5,8 @@ {ranch, "2.1.0"}, {jesse, "1.8.0"} ]}. + +{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. + +{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 043e811f22eb..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, - jesse, - ranch, - 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 f1e06fee2ffb..f74f29beeff6 100644 --- a/samples/server/petstore/erlang-server/src/openapi_api.erl +++ b/samples/server/petstore/erlang-server/src/openapi_api.erl @@ -1,401 +1,488 @@ -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. +``` +""". --include_lib("kernel/include/logger.hrl"). +-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 | 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) -> @@ -419,114 +506,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); - -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. +-include_lib("kernel/include/logger.hrl"). -validate_response_body('list', ReturnBaseType, Body, ValidatorState) -> +validate_response_body(list, ReturnBaseType, Body, ValidatorState) -> [ validate(schema, ReturnBaseType, Item, ValidatorState) || Item <- Body]; @@ -534,7 +516,6 @@ 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); @@ -544,28 +525,28 @@ 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 @@ -576,12 +557,12 @@ 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) @@ -634,7 +615,7 @@ validate(Rule = {pattern, Pattern}, Name, Value, _ValidatorState) -> _ -> 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 @@ -659,12 +640,10 @@ validate(Rule, Name, _Value, _ValidatorState) -> 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}). @@ -681,26 +660,24 @@ get_value(body, _Name, Req0) -> 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 - json: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) -> @@ -728,5 +705,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()) -> number(). +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: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. + +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 6d6f85069a04..0e7beb1132c4 100644 --- a/samples/server/petstore/erlang-server/src/openapi_auth.erl +++ b/samples/server/petstore/erlang-server/src/openapi_auth.erl @@ -2,46 +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 = <<>>, {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 45c191f005aa..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). - --include_lib("kernel/include/logger.hrl"). - --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 :: json:encode_value()}. - -handle_request(OperationID, Req, Context) -> - ?LOG_ERROR(#{what => "Got not implemented request to process", - operation_id => OperationID, request => Req, context => 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 f28c93ca326c..4ff7ea6e10ed 100644 --- a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl @@ -1,23 +1,55 @@ -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()). -type context() :: #{binary() => any()}. --type handler_response() ::{cowboy:http_status(), cowboy:http_headers(), json:encode_value()}. --export_type([handler_response/0]). +-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(). --callback authorize_api_key(openapi_api:operation_id(), binary()) -> - boolean() | {boolean(), context()}. +-export([api_key_callback/2, accept_callback/4, provide_callback/4]). +-ignore_xref([api_key_callback/2, accept_callback/4, provide_callback/4]). --callback handle_request(openapi_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). +-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 handle_request(module(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - handler_response(). -handle_request(Handler, OperationID, Req, Context) -> - Handler:handle_request(OperationID, Req, Context). +-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 authorize_api_key(module(), openapi_api:operation_id(), binary()) -> - Result :: false | {true, context()}. -authorize_api_key(Handler, OperationID, ApiKey) -> - Handler:authorize_api_key(OperationID, ApiKey). +-spec provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) -> + cowboy_req:resp_body(). +provide_callback(Class, OperationID, Req, Context) -> + ?LOG_ERROR(#{what => "Got not implemented request to process", + class => Class, + operation_id => OperationID, + request => Req, + context => 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 6caa86ba5293..9a86375c0717 100644 --- a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl @@ -1,199 +1,135 @@ %% 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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), 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()}). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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{operation_id = 'AddPet' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +-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{operation_id = 'DeletePet' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'FindPetsByStatus' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'FindPetsByTags' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'GetPetById' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'UpdatePet' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'UpdatePetWithForm' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'UploadFile' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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} -> @@ -202,143 +138,114 @@ is_authorized(Req0, #state{operation_id = 'UploadFile' = OperationID, is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-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{operation_id = 'AddPet'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'DeletePet'} = State) -> - Headers = ["api_key"], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'FindPetsByStatus'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'FindPetsByTags'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'GetPetById'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'UpdatePet'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'UpdatePetWithForm'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'UploadFile'} = State) -> - 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(). +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 fa3360a5b1ec..618d2024794b 100644 --- a/samples/server/petstore/erlang-server/src/openapi_router.erl +++ b/samples/server/petstore/erlang-server/src/openapi_router.erl @@ -1,52 +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()) -> cowboy_router:routes(). +-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() -> #{ @@ -151,17 +135,3 @@ get_operations() -> handler => 'openapi_user_handler' } }. - -get_validator_state() -> - persistent_term:get({?MODULE, validator_state}). - -prepare_validator() -> - {ok, FileContents} = file:read_file(get_openapi_path()), - R = json:decode(FileContents), - 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 d0299408d519..e7e4e601e4ae 100644 --- a/samples/server/petstore/erlang-server/src/openapi_server.erl +++ b/samples/server/petstore/erlang-server/src/openapi_server.erl @@ -1,19 +1,19 @@ -module(openapi_server). --define(DEFAULT_LOGIC_HANDLER, openapi_default_logic_handler). +-define(DEFAULT_LOGIC_HANDLER, openapi_logic_handler). -export([start/2]). - --spec start(ID :: term(), #{ - transport := tcp | ssl, - transport_opts => ranch_tcp:opts() | ranch_ssl:opts(), - logic_handler => module(), - net_opts => [] -}) -> {ok, pid()} | {error, any()}. - -start(ID, #{transport := Transport, - transport_opts := TransportOpts, - protocol_opts := ProtocolOpts} = Params) -> +-ignore_xref([start/2]). + +-spec start(term(), #{transport => tcp | ssl, + transport_opts => ranch:transport_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 @@ -27,7 +27,7 @@ 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) -> +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), 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 a0d74f76b6b6..1bab70f882a5 100644 --- a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl @@ -1,89 +1,64 @@ %% 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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. - --type result_error() :: {error, Reason :: any()}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), state()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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{operation_id = 'GetInventory' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +-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} -> @@ -92,123 +67,73 @@ is_authorized(Req0, #state{operation_id = 'GetInventory' = OperationID, is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-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{operation_id = 'DeleteOrder'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'GetInventory'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'GetOrderById'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'PlaceOrder'} = State) -> - 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(). +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 60364311126a..c4b42b6e89d6 100644 --- a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl @@ -1,171 +1,117 @@ %% 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]). - --type result_ok() :: { - ok, - {Status :: cowboy:http_status(), Headers :: cowboy:http_headers(), Body :: iodata()} -}. +-export([handle_type_accepted/2, handle_type_provided/2]). --type result_error() :: {error, Reason :: any()}. +-ignore_xref([handle_type_accepted/2, handle_type_provided/2]). --type processed_response() :: {stop, cowboy_req:req(), 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()}). --record(state, { - operation_id :: openapi_api:operation_id(), - logic_handler :: module(), - validator_state :: jesse_state:state(), - 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(), - - ?LOG_INFO(#{what => "Attempt to process operation", operation_id => 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{operation_id = 'CreateUser' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +-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{operation_id = 'CreateUsersWithArrayInput' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'CreateUsersWithListInput' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'DeleteUser' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'LogoutUser' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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{operation_id = 'UpdateUser' = OperationID, - logic_handler = LogicHandler} = State) -> - Result = openapi_auth:authorize_api_key( - LogicHandler, - OperationID, - header, - "authorization", - Req0), - case Result of +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} -> @@ -174,143 +120,101 @@ is_authorized(Req0, #state{operation_id = 'UpdateUser' = OperationID, is_authorized(Req, State) -> {true, Req, State}. --spec content_types_accepted(Req :: cowboy_req:req(), State :: state()) -> - { - Value :: [{binary(), AcceptResource :: atom()}], - Req :: cowboy_req:req(), - State :: state() - }. - -content_types_accepted(Req, State) -> +-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{operation_id = 'CreateUser'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'CreateUsersWithArrayInput'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'CreateUsersWithListInput'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'DeleteUser'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'GetUserByName'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'LoginUser'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'LogoutUser'} = State) -> - Headers = [], - {Result, Req} = validate_headers(Headers, Req0), - {Result, Req, State}; - -valid_content_headers(Req0, #state{operation_id = 'UpdateUser'} = State) -> - 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}. + {<<"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(Req :: cowboy_req:req(), State :: state()) -> - processed_response(). +-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}. - -%%%% --spec process_response(result_ok() | result_error(), cowboy_req:req(), state()) -> - processed_response(). -process_response({ok, {Code, Headers, Body}}, Req0, State) -> - Req = cowboy_req:reply(Code, Headers, Body, Req0), - {stop, Req, State}; -process_response({error, Reason}, Req0, #state{operation_id = OperationID} = State) -> - ?LOG_ERROR(#{what => "Unable to process request", operation_id => OperationID, Reason => Reason}), - Req = cowboy_req:reply(400, Req0), - {stop, Req, State}. - --spec handle_request_json(cowboy_req:req(), state()) -> processed_response(). -handle_request_json(Req0, #state{operation_id = OperationID, - logic_handler = LogicHandler, - validator_state = ValidatorState} = State) -> - 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) -> - json: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(). +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 22c7867236be..000000000000 --- a/samples/server/petstore/erlang-server/src/openapi_utils.erl +++ /dev/null @@ -1,158 +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. From a6346929b04c8221d6db1d99939242b674add2bb Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Thu, 29 Aug 2024 22:36:52 +0200 Subject: [PATCH 09/12] Fix return types for provide_callbacks --- .../src/main/resources/erlang-server/handler.mustache | 2 +- .../main/resources/erlang-server/logic_handler.mustache | 9 +++++---- .../echo_api/erlang-server/src/openapi_auth_handler.erl | 2 +- .../echo_api/erlang-server/src/openapi_body_handler.erl | 2 +- .../echo_api/erlang-server/src/openapi_form_handler.erl | 2 +- .../erlang-server/src/openapi_header_handler.erl | 2 +- .../echo_api/erlang-server/src/openapi_logic_handler.erl | 9 +++++---- .../echo_api/erlang-server/src/openapi_path_handler.erl | 2 +- .../echo_api/erlang-server/src/openapi_query_handler.erl | 2 +- .../petstore/erlang-server/src/openapi_logic_handler.erl | 9 +++++---- .../petstore/erlang-server/src/openapi_pet_handler.erl | 2 +- .../petstore/erlang-server/src/openapi_store_handler.erl | 2 +- .../petstore/erlang-server/src/openapi_user_handler.erl | 2 +- 13 files changed, 25 insertions(+), 22 deletions(-) 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 3415cdd8db3c..833c364c9377 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/handler.mustache @@ -123,7 +123,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler({{operations.pathPrefix}}, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 31c58c741cb7..b3f43eda864b 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 @@ -8,7 +8,8 @@ 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()). + fun((atom(), {{packageName}}_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]). @@ -22,7 +23,7 @@ boolean() | {created, iodata()} | {see_other, iodata()}. -callback provide_callback(atom(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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]). @@ -45,11 +46,11 @@ accept_callback(Class, OperationID, Req, Context) -> {501, #{}, #{}}. -spec provide_callback(atom(), {{packageName}}_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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}), - <<>>. + {501, #{}, #{}}. 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 index e2a7aacca1cc..9b8e9b18352f 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_auth_handler.erl @@ -120,7 +120,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(auth, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 index 7d1622c89fd3..de4fd624e10f 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_body_handler.erl @@ -200,7 +200,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(body, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 index d2b49bcbab3c..2d6fff400073 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_form_handler.erl @@ -118,7 +118,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(form, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 index fb12e229bc3b..8077a9ca8826 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_header_handler.erl @@ -92,7 +92,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(header, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 index 4ff7ea6e10ed..4057d569e8cb 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl @@ -8,7 +8,8 @@ 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()). + 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]). @@ -22,7 +23,7 @@ boolean() | {created, iodata()} | {see_other, iodata()}. -callback provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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]). @@ -45,11 +46,11 @@ accept_callback(Class, OperationID, Req, Context) -> {501, #{}, #{}}. -spec provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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}), - <<>>. + {501, #{}, #{}}. 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 index 19cd834ddf1a..0d08034f3932 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_path_handler.erl @@ -92,7 +92,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(path, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 index ad5f7f2666e1..5abfddcf7826 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_query_handler.erl @@ -182,7 +182,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(query, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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/petstore/erlang-server/src/openapi_logic_handler.erl b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl index 4ff7ea6e10ed..4057d569e8cb 100644 --- a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl @@ -8,7 +8,8 @@ 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()). + 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]). @@ -22,7 +23,7 @@ boolean() | {created, iodata()} | {see_other, iodata()}. -callback provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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]). @@ -45,11 +46,11 @@ accept_callback(Class, OperationID, Req, Context) -> {501, #{}, #{}}. -spec provide_callback(atom(), openapi_api:operation_id(), cowboy_req:req(), context()) -> - cowboy_req:resp_body(). + {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}), - <<>>. + {501, #{}, #{}}. 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 9a86375c0717..70edd6fb0010 100644 --- a/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_pet_handler.erl @@ -245,7 +245,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(pet, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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_store_handler.erl b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl index 1bab70f882a5..89952c4cc04e 100644 --- a/samples/server/petstore/erlang-server/src/openapi_store_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_store_handler.erl @@ -133,7 +133,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(store, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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 c4b42b6e89d6..3fedf1c9fd56 100644 --- a/samples/server/petstore/erlang-server/src/openapi_user_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_user_handler.erl @@ -214,7 +214,7 @@ handle_type_accepted(Req, #state{operation_id = OperationID, Handler(user, OperationID, Req, State#state.context). -spec handle_type_provided(cowboy_req:req(), state()) -> - cowboy_req:resp_body(). + {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). From 94290050b36cd68b4c7b822d85ad8a135ef6d694 Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 3 Sep 2024 21:02:13 +0200 Subject: [PATCH 10/12] Upgrade jesse to incur no dependencies The less dependencies the built code requires the better. --- .../src/main/resources/erlang-server/rebar.config.mustache | 2 +- samples/server/echo_api/erlang-server/rebar.config | 2 +- samples/server/petstore/erlang-server/rebar.config | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) 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 deca85d55f32..c2ecd8c9b78f 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 @@ -3,7 +3,7 @@ {deps, [ {cowboy, "2.12.0"}, {ranch, "2.1.0"}, - {jesse, "1.8.0"} + {jesse, "1.8.1"} ]}. {dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. diff --git a/samples/server/echo_api/erlang-server/rebar.config b/samples/server/echo_api/erlang-server/rebar.config index deca85d55f32..c2ecd8c9b78f 100644 --- a/samples/server/echo_api/erlang-server/rebar.config +++ b/samples/server/echo_api/erlang-server/rebar.config @@ -3,7 +3,7 @@ {deps, [ {cowboy, "2.12.0"}, {ranch, "2.1.0"}, - {jesse, "1.8.0"} + {jesse, "1.8.1"} ]}. {dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index deca85d55f32..c2ecd8c9b78f 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -3,7 +3,7 @@ {deps, [ {cowboy, "2.12.0"}, {ranch, "2.1.0"}, - {jesse, "1.8.0"} + {jesse, "1.8.1"} ]}. {dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. From 53c734826e980d602dc36fcc1e0d780eb3d5bb3b Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Tue, 3 Sep 2024 21:06:59 +0200 Subject: [PATCH 11/12] Fix dialyzer errors in the generated code --- .../src/main/resources/erlang-server/logic_handler.mustache | 2 +- .../src/main/resources/erlang-server/server.mustache | 2 +- .../server/echo_api/erlang-server/src/openapi_logic_handler.erl | 2 +- samples/server/echo_api/erlang-server/src/openapi_server.erl | 2 +- .../server/petstore/erlang-server/src/openapi_logic_handler.erl | 2 +- samples/server/petstore/erlang-server/src/openapi_server.erl | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) 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 b3f43eda864b..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 @@ -53,4 +53,4 @@ provide_callback(Class, OperationID, Req, Context) -> operation_id => OperationID, request => Req, context => Context}), - {501, #{}, #{}}. + {<<>>, Req, Context}. 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 0f7671443ea8..43ed54e5d230 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/server.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/server.mustache @@ -6,7 +6,7 @@ -ignore_xref([start/2]). -spec start(term(), #{transport => tcp | ssl, - transport_opts => ranch:transport_opts(), + transport_opts => ranch:opts(), protocol_opts => cowboy:opts(), logic_handler => module()}) -> {ok, pid()} | {error, any()}. 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 index 4057d569e8cb..28d45ab65113 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_logic_handler.erl @@ -53,4 +53,4 @@ provide_callback(Class, OperationID, Req, Context) -> operation_id => OperationID, request => Req, context => Context}), - {501, #{}, #{}}. + {<<>>, Req, Context}. diff --git a/samples/server/echo_api/erlang-server/src/openapi_server.erl b/samples/server/echo_api/erlang-server/src/openapi_server.erl index e7e4e601e4ae..0cd992fe69d2 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_server.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_server.erl @@ -6,7 +6,7 @@ -ignore_xref([start/2]). -spec start(term(), #{transport => tcp | ssl, - transport_opts => ranch:transport_opts(), + transport_opts => ranch:opts(), protocol_opts => cowboy:opts(), logic_handler => module()}) -> {ok, pid()} | {error, any()}. 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 4057d569e8cb..28d45ab65113 100644 --- a/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl +++ b/samples/server/petstore/erlang-server/src/openapi_logic_handler.erl @@ -53,4 +53,4 @@ provide_callback(Class, OperationID, Req, Context) -> operation_id => OperationID, request => Req, context => Context}), - {501, #{}, #{}}. + {<<>>, Req, Context}. diff --git a/samples/server/petstore/erlang-server/src/openapi_server.erl b/samples/server/petstore/erlang-server/src/openapi_server.erl index e7e4e601e4ae..0cd992fe69d2 100644 --- a/samples/server/petstore/erlang-server/src/openapi_server.erl +++ b/samples/server/petstore/erlang-server/src/openapi_server.erl @@ -6,7 +6,7 @@ -ignore_xref([start/2]). -spec start(term(), #{transport => tcp | ssl, - transport_opts => ranch:transport_opts(), + transport_opts => ranch:opts(), protocol_opts => cowboy:opts(), logic_handler => module()}) -> {ok, pid()} | {error, any()}. From 36e89c5cb09b2cc7afba24f1b9e522258712cc4f Mon Sep 17 00:00:00 2001 From: Nelson Vides Date: Thu, 5 Sep 2024 18:03:42 +0200 Subject: [PATCH 12/12] Apply stronger dialyzer checks --- .../src/main/resources/erlang-server/api.mustache | 7 ++++--- .../src/main/resources/erlang-server/rebar.config.mustache | 5 ++++- samples/server/echo_api/erlang-server/rebar.config | 5 ++++- samples/server/echo_api/erlang-server/src/openapi_api.erl | 7 ++++--- samples/server/petstore/erlang-server/rebar.config | 5 ++++- samples/server/petstore/erlang-server/src/openapi_api.erl | 7 ++++--- 6 files changed, 24 insertions(+), 12 deletions(-) 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 f876b3a5361f..ba8f13854f61 100644 --- a/modules/openapi-generator/src/main/resources/erlang-server/api.mustache +++ b/modules/openapi-generator/src/main/resources/erlang-server/api.mustache @@ -108,7 +108,8 @@ for the `OperationID` operation. OperationID :: operation_id(), Code :: 200..599, Body :: jesse:json_term(), - ValidatorState :: jesse_state:state()) -> ok | no_return(). + 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}} @@ -393,7 +394,7 @@ 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()) -> number(). +-spec to_float(iodata()) -> float(). to_float(V) -> binary_to_float(iolist_to_binary([V])). @@ -438,7 +439,7 @@ get_openapi_path() -> -include_lib("kernel/include/file.hrl"). --spec priv_dir(Application :: atom()) -> file:filename(). +-spec priv_dir(Application :: atom()) -> file:name_all(). priv_dir(AppName) -> case code:priv_dir(AppName) of Value when is_list(Value) -> 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 c2ecd8c9b78f..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 @@ -6,7 +6,10 @@ {jesse, "1.8.1"} ]}. -{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. +{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/rebar.config b/samples/server/echo_api/erlang-server/rebar.config index c2ecd8c9b78f..50cd482ca39b 100644 --- a/samples/server/echo_api/erlang-server/rebar.config +++ b/samples/server/echo_api/erlang-server/rebar.config @@ -6,7 +6,10 @@ {jesse, "1.8.1"} ]}. -{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. +{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_api.erl b/samples/server/echo_api/erlang-server/src/openapi_api.erl index fd60a684c82a..b796f2df4a36 100644 --- a/samples/server/echo_api/erlang-server/src/openapi_api.erl +++ b/samples/server/echo_api/erlang-server/src/openapi_api.erl @@ -108,7 +108,8 @@ for the `OperationID` operation. OperationID :: operation_id(), Code :: 200..599, Body :: jesse:json_term(), - ValidatorState :: jesse_state:state()) -> ok | no_return(). + 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) -> @@ -872,7 +873,7 @@ 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()) -> number(). +-spec to_float(iodata()) -> float(). to_float(V) -> binary_to_float(iolist_to_binary([V])). @@ -917,7 +918,7 @@ get_openapi_path() -> -include_lib("kernel/include/file.hrl"). --spec priv_dir(Application :: atom()) -> file:filename(). +-spec priv_dir(Application :: atom()) -> file:name_all(). priv_dir(AppName) -> case code:priv_dir(AppName) of Value when is_list(Value) -> diff --git a/samples/server/petstore/erlang-server/rebar.config b/samples/server/petstore/erlang-server/rebar.config index c2ecd8c9b78f..50cd482ca39b 100644 --- a/samples/server/petstore/erlang-server/rebar.config +++ b/samples/server/petstore/erlang-server/rebar.config @@ -6,7 +6,10 @@ {jesse, "1.8.1"} ]}. -{dialyzer, [{plt_extra_apps, [cowboy, cowlib, ranch, jesse]}]}. +{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_api.erl b/samples/server/petstore/erlang-server/src/openapi_api.erl index f74f29beeff6..16ad18e97823 100644 --- a/samples/server/petstore/erlang-server/src/openapi_api.erl +++ b/samples/server/petstore/erlang-server/src/openapi_api.erl @@ -108,7 +108,8 @@ for the `OperationID` operation. OperationID :: operation_id(), Code :: 200..599, Body :: jesse:json_term(), - ValidatorState :: jesse_state:state()) -> ok | no_return(). + 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) -> @@ -719,7 +720,7 @@ 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()) -> number(). +-spec to_float(iodata()) -> float(). to_float(V) -> binary_to_float(iolist_to_binary([V])). @@ -764,7 +765,7 @@ get_openapi_path() -> -include_lib("kernel/include/file.hrl"). --spec priv_dir(Application :: atom()) -> file:filename(). +-spec priv_dir(Application :: atom()) -> file:name_all(). priv_dir(AppName) -> case code:priv_dir(AppName) of Value when is_list(Value) ->