diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index ec467bd..1c852e0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,4 +1,4 @@ -name: erf ci +name: CI on: push: diff --git a/src/erf_oas_3_0.erl b/src/erf_oas_3_0.erl index b80cd77..9c5453d 100644 --- a/src/erf_oas_3_0.erl +++ b/src/erf_oas_3_0.erl @@ -259,12 +259,18 @@ parse_operation( maps:get(<<"parameters">>, RawOperation, []) ), - RequestBodyRef = erf_util:to_snake_case(<<(NewCTX#ctx.namespace)/binary, "_request_body">>), RawRequestBody = maps:get(<<"requestBody">>, RawOperation, undefined), - {RequestBodySchema, RequestBodyExtraSchemas, RequestBodyCTX} = parse_request_body( + {ParsedRequestBody, RawRequestBodySchemas, RequestBodyCTX} = parse_request_body( RawRequestBody, ParametersCTX ), - RequestBodySchemas = [{RequestBodyRef, RequestBodySchema} | RequestBodyExtraSchemas], + RequestBodyRef = erf_util:to_snake_case(<<(NewCTX#ctx.namespace)/binary, "_request_body">>), + RequestBodyRequired = maps:get(required, ParsedRequestBody), + RequestBodySchema = maps:get(schema, ParsedRequestBody), + RequestBodySchemas = [{RequestBodyRef, RequestBodySchema} | RawRequestBodySchemas], + RequestBody = #{ + ref => RequestBodyRef, + required => RequestBodyRequired + }, {Responses, ResponsesSchemas, ResponsesCTX} = lists:foldl( @@ -279,22 +285,22 @@ parse_operation( _RawStatusCode -> erlang:binary_to_integer(RawStatusCode) end, - {ResponseBody, ResponseExtraSchemas, ResponseCTX} = parse_response_body( + {ParsedResponse, RawResponseExtraSchemas, ResponseCTX} = parse_response( RawResponse, ResponsesCTXAcc ), - RawRef = - case StatusCode of - '*' -> - <<"default">>; - _StatusCode -> - erlang:integer_to_binary(StatusCode) - end, - Ref = erf_util:to_snake_case(<< - (NewCTX#ctx.namespace)/binary, "_response_body_", RawRef/binary + ResponseRef = erf_util:to_snake_case(<< + (NewCTX#ctx.namespace)/binary, "_response_body_", RawStatusCode/binary >>), + ResponseRequired = maps:get(required, ParsedResponse), + ResponseSchema = maps:get(schema, ParsedResponse), + ResponseExtraSchemas = [{ResponseRef, ResponseSchema} | RawResponseExtraSchemas], + Response = #{ + ref => ResponseRef, + required => ResponseRequired + }, { - ResponsesAcc#{StatusCode => Ref}, - [{Ref, ResponseBody} | ResponseExtraSchemas] ++ ResponsesExtraSchemasAcc, + ResponsesAcc#{StatusCode => Response}, + ResponseExtraSchemas ++ ResponsesExtraSchemasAcc, ResponseCTX } end, @@ -306,7 +312,7 @@ parse_operation( id => OperationId, method => Method, parameters => Parameters, - request_body => RequestBodyRef, + request_body => RequestBody, responses => Responses }, Schemas = ParametersSchemas ++ RequestBodySchemas ++ ResponsesSchemas, @@ -347,17 +353,14 @@ parse_parameter(#{<<"content">> := Content} = RawParameter, #ctx{namespace = Nam Parameter = #{ ref => ParameterRef, name => ParameterName, - type => ParameterType + type => ParameterType, + required => Required }, {AnyOf, ExtraSchemas, NewCTX} = lists:foldl( fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - { - [Schema#{<<"nullable">> => not Required} | AnyOfAcc], - ExtraSchemas ++ ExtraSchemasAcc, - SchemaCTX - } + {[Schema | AnyOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} end, {[], [], CTX}, maps:to_list(Content) @@ -389,54 +392,57 @@ parse_parameter(#{<<"schema">> := RawSchema} = RawParameter, #ctx{namespace = Na Parameter = #{ ref => ParameterRef, name => ParameterName, - type => ParameterType + type => ParameterType, + required => Required }, - {RawParameterSchema, NewExtraSchemas, NewCTX} = parse_schema(RawSchema, CTX), - ParameterSchema = maps:put(<<"nullable">>, not Required, RawParameterSchema), + {ParameterSchema, NewExtraSchemas, NewCTX} = parse_schema(RawSchema, CTX), {Parameter, [{ParameterRef, ParameterSchema} | NewExtraSchemas], NewCTX}. -spec parse_request_body(OAS, CTX) -> Result when OAS :: oas(), CTX :: ctx(), Result :: {RequestBody, ExtraSchemas, NewCTX}, - RequestBody :: erf_parser:schema(), + RequestBody :: #{schema := erf_parser:schema(), required := boolean()}, ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], NewCTX :: ctx(). parse_request_body(#{<<"$ref">> := Ref}, CTX) -> - {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - {NewSchema, NewExtraSchemas, NewCTX} = parse_request_body(RefOAS, RefCTX), - {#{<<"$ref">> => RefResolved}, [{RefResolved, NewSchema} | NewExtraSchemas], NewCTX}; + {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), + parse_request_body(RefOAS, RefCTX); parse_request_body(#{<<"content">> := Content} = ReqBody, CTX) -> Required = maps:get(<<"required">>, ReqBody, false), - {AnyOf, NewExtraSchemas, NewCTX} = lists:foldl( + {AnyOf, ExtraSchemas, NewCTX} = lists:foldl( fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> - {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), - { - [Schema#{<<"nullable">> => not Required} | AnyOfAcc], - ExtraSchemas ++ ExtraSchemasAcc, - SchemaCTX - } + {Schema, NewExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), + {[Schema | AnyOfAcc], NewExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} end, {[], [], CTX}, maps:to_list(Content) ), - {#{<<"anyOf">> => AnyOf}, NewExtraSchemas, NewCTX}; + RequestBodySchema = #{<<"anyOf">> => AnyOf}, + RequestBody = #{ + schema => RequestBodySchema, + required => Required + }, + {RequestBody, ExtraSchemas, NewCTX}; parse_request_body(_ReqBody, CTX) -> - {undefined, [], CTX}. + RequestBody = #{ + schema => true, + required => false + }, + {RequestBody, [], CTX}. --spec parse_response_body(OAS, CTX) -> Result when +-spec parse_response(OAS, CTX) -> Result when OAS :: oas(), CTX :: ctx(), - Result :: {ResponseBody, ExtraSchemas, NewCTX}, - ResponseBody :: erf_parser:schema(), + Result :: {Response, ExtraSchemas, NewCTX}, + Response :: #{schema := erf_parser:schema(), required := boolean()}, ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], NewCTX :: ctx(). -parse_response_body(#{<<"$ref">> := Ref}, CTX) -> - {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), - {NewSchema, NewExtraSchemas, NewCTX} = parse_response_body(RefOAS, RefCTX), - {#{<<"$ref">> => RefResolved}, [{RefResolved, NewSchema} | NewExtraSchemas], NewCTX}; -parse_response_body(#{<<"content">> := Content}, CTX) -> - {AnyOf, NewExtraSchemas, NewCTX} = lists:foldl( +parse_response(#{<<"$ref">> := Ref}, CTX) -> + {_RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), + parse_response(RefOAS, RefCTX); +parse_response(#{<<"content">> := Content}, CTX) -> + {AnyOf, ExtraSchemas, NewCTX} = lists:foldl( fun({_MediaType, #{<<"schema">> := RawSchema}}, {AnyOfAcc, ExtraSchemasAcc, CTXAcc}) -> {Schema, ExtraSchemas, SchemaCTX} = parse_schema(RawSchema, CTXAcc), {[Schema | AnyOfAcc], ExtraSchemas ++ ExtraSchemasAcc, SchemaCTX} @@ -444,9 +450,18 @@ parse_response_body(#{<<"content">> := Content}, CTX) -> {[], [], CTX}, maps:to_list(Content) ), - {#{<<"anyOf">> => AnyOf}, NewExtraSchemas, NewCTX}; -parse_response_body(_Response, CTX) -> - {undefined, [], CTX}. + ResponseSchema = #{<<"anyOf">> => AnyOf}, + Response = #{ + schema => ResponseSchema, + required => false + }, + {Response, ExtraSchemas, NewCTX}; +parse_response(_Response, CTX) -> + Response = #{ + schema => true, + required => false + }, + {Response, [], CTX}. -spec parse_schema(OAS, CTX) -> Result when OAS :: oas(), @@ -455,6 +470,7 @@ parse_response_body(_Response, CTX) -> Schema :: erf_parser:schema(), ExtraSchemas :: [{erf_parser:ref(), erf_parser:schema()}], NewCTX :: ctx(). +%% TODO: parse to new ndto:schema() type parse_schema(#{<<"$ref">> := Ref}, CTX) -> {RefResolved, RefOAS, RefCTX} = resolve_ref(Ref, CTX), {NewSchema, NewExtraSchemas, NewCTX} = parse_schema(RefOAS, RefCTX), diff --git a/src/erf_parser.erl b/src/erf_parser.erl index ac1d4dd..2012b63 100644 --- a/src/erf_parser.erl +++ b/src/erf_parser.erl @@ -37,20 +37,29 @@ id := binary(), method := method(), parameters := [parameter()], - request_body := ref(), + request_body := request_body(), responses := #{ - '*' | status_code() := ref() + '*' | status_code() := response() } }. -type parameter() :: #{ ref := ref(), name := parameter_name(), - type := parameter_type() + type := parameter_type(), + required := boolean() }. -type parameter_name() :: binary(). -type parameter_type() :: header | cookie | path | query. -type path() :: binary(). -type ref() :: binary(). +-type request_body() :: #{ + ref := ref(), + required := boolean() +}. +-type response() :: #{ + ref := ref(), + required := boolean() +}. -type schema() :: ndto:schema(). -type status_code() :: 100..599. @@ -61,8 +70,14 @@ method/0, operation/0, parameter/0, + parameter_name/0, + parameter_type/0, + path/0, ref/0, - schema/0 + request_body/0, + response/0, + schema/0, + status_code/0 ]). %%%----------------------------------------------------------------------------- diff --git a/src/erf_router.erl b/src/erf_router.erl index 4c450bf..389dbc6 100644 --- a/src/erf_router.erl +++ b/src/erf_router.erl @@ -583,98 +583,132 @@ handle_ast(API, #{callback := Callback} = Opts) -> -spec is_valid_request(Parameters, RequestBody) -> Result when Parameters :: [erf_parser:parameter()], - RequestBody :: erf_parser:ref(), + RequestBody :: erf_parser:request_body(), Result :: erl_syntax:syntaxTree(). -is_valid_request(RawParameters, RequestBody) -> - Body = - case RequestBody of - undefined -> - erl_syntax:atom(true); - _RequestBody -> - RequestBodyModule = erlang:binary_to_atom(erf_util:to_snake_case(RequestBody)), - erl_syntax:application( - erl_syntax:atom(RequestBodyModule), - erl_syntax:atom(is_valid), - [erl_syntax:variable('Body')] +is_valid_request(RawParameters, RawRequestBody) -> + RequestBodyRef = maps:get(ref, RawRequestBody), + RequestBodyModule = + erlang:binary_to_atom(erf_util:to_snake_case(RequestBodyRef)), + RequestBodyIsValid = + erl_syntax:application( + erl_syntax:atom(RequestBodyModule), + erl_syntax:atom(is_valid), + [erl_syntax:variable('Body')] + ), + RequestBody = + case maps:get(required, RawRequestBody) of + true -> + RequestBodyIsValid; + false -> + erl_syntax:infix_expr( + erl_syntax:infix_expr( + erl_syntax:variable('Body'), + erl_syntax:operator('=:='), + erl_syntax:atom(undefined) + ), + erl_syntax:operator('orelse'), + RequestBodyIsValid ) end, - Parameters = lists:filtermap( - fun(Parameter) -> - ParameterModule = erlang:binary_to_atom(maps:get(ref, Parameter)), - ParameterName = maps:get(name, Parameter), - ParameterType = maps:get(type, Parameter), - case ParameterType of - header -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [ - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(ParameterName) - ) + FilteredParameters = + lists:filtermap( + fun(Parameter) -> + ParameterModule = erlang:binary_to_atom(maps:get(ref, Parameter)), + ParameterName = maps:get(name, Parameter), + ParameterType = maps:get(type, Parameter), + case ParameterType of + header -> + GetParameter = + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(ParameterName) ) - ]), - erl_syntax:variable('Headers') - ] - ) - ] - ) - }; - cookie -> - %% TODO: implement - false; - path -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:variable( - erlang:binary_to_atom( - erf_util:to_pascal_case(ParameterName) - ) + ) + ]), + erl_syntax:variable('Headers') + ] + ), + ParameterRequired = maps:get(required, Parameter), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => ParameterRequired + }}; + cookie -> + %% TODO: implement + false; + path -> + GetParameter = + erl_syntax:variable( + erlang:binary_to_atom( + erf_util:to_pascal_case(ParameterName) ) - ] - ) - }; - query -> - { - true, - erl_syntax:application( - erl_syntax:atom(ParameterModule), - erl_syntax:atom(is_valid), - [ - erl_syntax:application( - erl_syntax:atom(proplists), - erl_syntax:atom(get_value), - [ - erl_syntax:binary([ - erl_syntax:binary_field( - erl_syntax:string( - erlang:binary_to_list(ParameterName) - ) + ), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => true + }}; + query -> + GetParameter = + erl_syntax:application( + erl_syntax:atom(proplists), + erl_syntax:atom(get_value), + [ + erl_syntax:binary([ + erl_syntax:binary_field( + erl_syntax:string( + erlang:binary_to_list(ParameterName) ) - ]), - erl_syntax:variable('QueryParameters') - ] - ) - ] + ) + ]), + erl_syntax:variable('QueryParameters') + ] + ), + ParameterRequired = maps:get(required, Parameter), + {true, #{ + module => ParameterModule, + get => GetParameter, + required => ParameterRequired + }} + end + end, + RawParameters + ), + Parameters = + lists:map( + fun(#{module := ParameterModule, get := GetParameter, required := ParameterRequired}) -> + IsValidParameter = + erl_syntax:application( + erl_syntax:atom(ParameterModule), + erl_syntax:atom(is_valid), + [GetParameter] + ), + OptionalParameter = + erl_syntax:infix_expr( + GetParameter, + erl_syntax:operator('=:='), + erl_syntax:atom(undefined) + ), + case ParameterRequired of + true -> + IsValidParameter; + false -> + erl_syntax:infix_expr( + OptionalParameter, + erl_syntax:operator('orelse'), + IsValidParameter ) - } - end - end, - RawParameters - ), - chain_conditions([Body | Parameters], 'andalso'). + end + end, + FilteredParameters + ), + chain_conditions([RequestBody | Parameters], 'andalso'). -spec load_binary(ModuleName, Bin) -> Result when ModuleName :: atom(), @@ -711,11 +745,12 @@ postprocess( ), {Status, Headers, {file, File, Range}}; postprocess(_Request, {Status, RawHeaders, RawBody}) -> + ContentTypeHeader = string:casefold(<<"content-type">>), {Headers, Body} = - case proplists:get_value(<<"content-type">>, RawHeaders, undefined) of + case proplists:get_value(ContentTypeHeader, RawHeaders, undefined) of undefined -> { - [{<<"content-type">>, <<"application/json">>} | RawHeaders], + [{ContentTypeHeader, <<"application/json">>} | RawHeaders], njson:encode(RawBody) }; _Otherwise -> diff --git a/test/erf_oas_3_0_SUITE.erl b/test/erf_oas_3_0_SUITE.erl index 5defcc8..4570178 100644 --- a/test/erf_oas_3_0_SUITE.erl +++ b/test/erf_oas_3_0_SUITE.erl @@ -82,23 +82,22 @@ petstore(_Conf) -> <<"type">> := <<"integer">>, <<"maximum">> := 100 }, - <<"petstore_list_pets_request_body">> := undefined, + <<"petstore_list_pets_request_body">> := true, <<"petstore_list_pets_response_body_200">> := #{ <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Pets">>}] }, <<"petstore_list_pets_response_body_default">> := #{ <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Error">>}] }, - <<"petstore_create_pets_request_body">> := undefined, - <<"petstore_create_pets_response_body_201">> := undefined, + <<"petstore_create_pets_request_body">> := true, + <<"petstore_create_pets_response_body_201">> := true, <<"petstore_create_pets_response_body_default">> := #{ <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Error">>}] }, <<"petstore_show_pet_by_id_pet_id">> := #{ - <<"type">> := <<"string">>, - <<"nullable">> := false + <<"type">> := <<"string">> }, - <<"petstore_show_pet_by_id_request_body">> := undefined, + <<"petstore_show_pet_by_id_request_body">> := true, <<"petstore_show_pet_by_id_response_body_200">> := #{ <<"anyOf">> := [#{<<"$ref">> := <<"petstore_Pet">>}] }, @@ -118,23 +117,42 @@ petstore(_Conf) -> #{ ref := <<"petstore_list_pets_limit">>, name := <<"limit">>, - type := query + type := query, + required := false } ], - request_body := <<"petstore_list_pets_request_body">>, + request_body := #{ + ref := <<"petstore_list_pets_request_body">>, + required := false + }, responses := #{ - 200 := <<"petstore_list_pets_response_body_200">>, - '*' := <<"petstore_list_pets_response_body_default">> + 200 := #{ + ref := <<"petstore_list_pets_response_body_200">>, + required := false + }, + '*' := #{ + ref := <<"petstore_list_pets_response_body_default">>, + required := false + } } }, #{ id := <<"create_pets">>, method := post, parameters := [], - request_body := <<"petstore_create_pets_request_body">>, + request_body := #{ + ref := <<"petstore_create_pets_request_body">>, + required := false + }, responses := #{ - 201 := <<"petstore_create_pets_response_body_201">>, - '*' := <<"petstore_create_pets_response_body_default">> + 201 := #{ + ref := <<"petstore_create_pets_response_body_201">>, + required := false + }, + '*' := #{ + ref := <<"petstore_create_pets_response_body_default">>, + required := false + } } } ] @@ -153,10 +171,19 @@ petstore(_Conf) -> type := path } ], - request_body := <<"petstore_show_pet_by_id_request_body">>, + request_body := #{ + ref := <<"petstore_show_pet_by_id_request_body">>, + required := false + }, responses := #{ - 200 := <<"petstore_show_pet_by_id_response_body_200">>, - '*' := <<"petstore_show_pet_by_id_response_body_default">> + 200 := #{ + ref := <<"petstore_show_pet_by_id_response_body_200">>, + required := false + }, + '*' := #{ + ref := <<"petstore_show_pet_by_id_response_body_default">>, + required := false + } } } ] @@ -184,15 +211,14 @@ with_refs(_Conf) -> }, <<"common_oas_3_0_spec_version">> := #{ <<"type">> := <<"string">>, - <<"pattern">> := <<"^[0-9]+$">>, - <<"nullable">> := false - }, - <<"with_refs_oas_3_0_spec_delete_foo_request_body">> := undefined, - <<"with_refs_oas_3_0_spec_delete_foo_response_body_204">> := #{ - <<"$ref">> := <<"common_oas_3_0_spec_NoContent">> + <<"pattern">> := <<"^[0-9]+$">> }, + <<"with_refs_oas_3_0_spec_delete_foo_request_body">> := true, + <<"with_refs_oas_3_0_spec_delete_foo_response_body_204">> := true, <<"with_refs_oas_3_0_spec_delete_foo_response_body_404">> := #{ - <<"$ref">> := <<"common_oas_3_0_spec_NotFound">> + <<"anyOf">> := [ + #{<<"$ref">> := <<"common_oas_3_0_spec_Error">>} + ] } } }, diff --git a/test/erf_router_SUITE.erl b/test/erf_router_SUITE.erl index cbcda05..83297e7 100644 --- a/test/erf_router_SUITE.erl +++ b/test/erf_router_SUITE.erl @@ -71,10 +71,9 @@ foo(_Conf) -> version => <<"1.0.0">>, schemas => #{ <<"version_foo_version">> => #{ - <<"type">> => <<"integer">>, - <<"nullable">> => false + <<"type">> => <<"integer">> }, - <<"get_foo_request_body">> => undefined, + <<"get_foo_request_body">> => true, <<"get_foo_response_body">> => #{ <<"anyOf">> => [ #{ @@ -110,7 +109,8 @@ foo(_Conf) -> #{ ref => <<"version_foo_version">>, name => <<"version">>, - type => path + type => path, + required => true } ], operations => [ @@ -118,8 +118,13 @@ foo(_Conf) -> id => <<"get_foo">>, method => get, parameters => [], - request_body => undefined, - response_body => <<"get_foo_response_body">> + request_body => #{ + ref => <<"get_foo_request_body">>, + required => false + }, + response_body => #{ + ref => <<"get_foo_response_body">> + } } ] }