diff --git a/rebar.config b/rebar.config index 47dc0fd..91752dd 100644 --- a/rebar.config +++ b/rebar.config @@ -92,7 +92,10 @@ erf, {erf_router, handle, 2}, {erf_router, handle_event, 3}, - {erf_static, mime_type, 1} + {erf_static, mime_type, 1}, + {erf_problem_details, new, 4}, + {erf_problem_details, validation_detail, 3}, + {erf_util, binarize_atoms, 1} ]}. {gradualizer_opts, [ diff --git a/src/erf.erl b/src/erf.erl index 88a8a37..eb94e69 100644 --- a/src/erf.erl +++ b/src/erf.erl @@ -336,6 +336,7 @@ build_router(SpecPath, SpecParser, Callback, RawStaticRoutes, SwaggerUI) -> callback => Callback, static_routes => StaticRoutes }), + file:write_file("router.erl", erl_prettypr:format(Router)), case erf_router:load(Router) of ok -> {ok, RouterMod, Router}; diff --git a/src/erf_problem_details.erl b/src/erf_problem_details.erl new file mode 100644 index 0000000..74b1e93 --- /dev/null +++ b/src/erf_problem_details.erl @@ -0,0 +1,74 @@ +%%% Copyright 2024 Nomasystems, S.L. http://www.nomasystems.com +%% +%% Licensed under the Apache License, Version 2.0 (the "License"); +%% you may not use this file except in compliance with the License. +%% You may obtain a copy of the License at +%% +%% http://www.apache.org/licenses/LICENSE-2.0 +%% +%% Unless required by applicable law or agreed to in writing, software +%% distributed under the License is distributed on an "AS IS" BASIS, +%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +%% See the License for the specific language governing permissions and +%% limitations under the License + +%% @doc Module that generates Problem Details representations, based on RFC 9457. +-module(erf_problem_details). + +%%% EXTERNAL EXPORTS +-export([ + new/4, + validation_detail/3 +]). + +%%% TYPES +-type detail() :: + validation_detail(). + +-type validation_detail() :: #{ + schema_path := binary(), + type := path | query | body, + reason := binary() +}. + +-type t() :: #{ + title := undefined | binary(), + status := undefined | erf_parser:status_code(), + detail := undefined | binary(), + details := undefined | list(detail()) +}. + +%%% TYPE EXPORTS +-export_type([ + t/0, + validation_detail/0 +]). + +%%%----------------------------------------------------------------------------- +%%% EXTERNAL EXPORTS +%%%----------------------------------------------------------------------------- +-spec new(Title, Status, Detail, Errors) -> Resp when + Title :: undefined | binary(), + Status :: undefined | erf_parser:status_code(), + Detail :: undefined | binary(), + Errors :: undefined | list(detail()), + Resp :: t(). +new(Title, Status, Detail, Details) -> + #{ + title => Title, + status => Status, + detail => Detail, + details => Details + }. + +-spec validation_detail(Parameter, Type, Reason) -> Resp when + Parameter :: binary(), + Type :: path | query | body, + Reason :: binary(), + Resp :: validation_detail(). +validation_detail(Parameter, Type, Reason) -> + #{ + schema_path => Parameter, + type => Type, + reason => Reason + }. diff --git a/src/erf_router.erl b/src/erf_router.erl index df8fc58..b423d47 100644 --- a/src/erf_router.erl +++ b/src/erf_router.erl @@ -351,44 +351,84 @@ handle_ast(API, #{callback := Callback} = Opts) -> ] ), erl_syntax:clause( - [erl_syntax:tuple([ - erl_syntax:atom('false'), + [ erl_syntax:tuple([ + erl_syntax:atom('false'), erl_syntax:tuple([ - erl_syntax:variable('SchemaPath'), - erl_syntax:variable('Description') - ]), - erl_syntax:variable('_ConditionIndex') + erl_syntax:tuple([ + erl_syntax:variable('SchemaPath'), + erl_syntax:variable('Description') + ]), + erl_syntax:variable('_ConditionIndex') + ]) ]) - ])], + ], none, [ erl_syntax:tuple( [ erl_syntax:integer(400), erl_syntax:list([]), - erl_syntax:map_expr([ - erl_syntax:map_field_assoc( - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string("schema_path") - ) - ]), + erl_syntax:application( + erl_syntax:atom(erf_util), + erl_syntax:atom(binarize_atoms), + [ erl_syntax:application( - erl_syntax:atom(erlang), - erl_syntax:atom(atom_to_binary), - [erl_syntax:variable('SchemaPath')] + erl_syntax:atom( + erf_problem_details + ), + erl_syntax:atom(new), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + "Validation error" + ) + ) + ]), + erl_syntax:integer(400), + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + "One or more parameters failed validation." + ) + ) + ]), + erl_syntax:list([ + erl_syntax:application( + erl_syntax:atom( + erf_problem_details + ), + erl_syntax:atom( + validation_detail + ), + [ + erl_syntax:application( + erl_syntax:atom( + erlang + ), + erl_syntax:atom( + atom_to_binary + ), + [ + erl_syntax:variable( + 'SchemaPath' + ) + ] + ), + erl_syntax:atom( + 'body' + ), + erl_syntax:variable( + 'Description' + ) + ] + ) + ]) + ] ) - ), - erl_syntax:map_field_assoc( - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string("description") - ) - ]), - erl_syntax:variable('Description') - ) - ]) + ] + ) ] ) ] @@ -733,17 +773,20 @@ is_valid_request(RawParameters, Request) -> erl_syntax:atom('andalso'), [ erl_syntax:list( - [ erl_syntax:tuple([ - erl_syntax:fun_expr([ - erl_syntax:clause( - none, - [ - Condition - ] - ) - ]), - erl_syntax:list([]) - ]) || Condition <- [RequestBody | Parameters]] + [ + erl_syntax:tuple([ + erl_syntax:fun_expr([ + erl_syntax:clause( + none, + [ + Condition + ] + ) + ]), + erl_syntax:list([]) + ]) + || Condition <- [RequestBody | Parameters] + ] ) ] ). diff --git a/src/erf_util.erl b/src/erf_util.erl index 76c381b..0f6b5eb 100644 --- a/src/erf_util.erl +++ b/src/erf_util.erl @@ -16,7 +16,8 @@ %%% EXTERNAL EXPORTS -export([ to_pascal_case/1, - to_snake_case/1 + to_snake_case/1, + binarize_atoms/1 ]). %%%----------------------------------------------------------------------------- @@ -48,6 +49,30 @@ to_snake_case([C | Rest]) when C >= $A andalso C =< $Z -> to_snake_case([_C | Rest]) -> to_snake_case(Rest). +-spec binarize_atoms(Map) -> Resp when + Map :: map(), + Resp :: map(). +binarize_atoms(Map) when is_map(Map) -> + RecursiveFun = fun + R(V) when is_map(V) -> binarize_atoms(V); + R(V) when is_list(V) -> + lists:map(R, V); + R(V) when is_atom(V) -> + erlang:atom_to_binary(V); + R(V) -> + V + end, + maps:fold( + fun + (K, V, Acc) when is_atom(K) -> + maps:put(atom_to_binary(K), RecursiveFun(V), Acc); + (K, V, Acc) -> + maps:put(K, V, Acc) + end, + #{}, + Map + ). + %%%----------------------------------------------------------------------------- %%% INTERNAL FUNCTIONS %%%-----------------------------------------------------------------------------