diff --git a/src/sr_entities_handler.erl b/src/sr_entities_handler.erl index 46ce5d7..0f0d80b 100644 --- a/src/sr_entities_handler.erl +++ b/src/sr_entities_handler.erl @@ -131,8 +131,9 @@ announce_req(Req, _Opts) -> Req. %%% Auxiliary Functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% --spec atom_to_method(get|put|post|delete) -> binary(). +-spec atom_to_method(get|patch|put|post|delete) -> binary(). atom_to_method(get) -> <<"GET">>; +atom_to_method(patch) -> <<"PATCH">>; atom_to_method(put) -> <<"PUT">>; atom_to_method(post) -> <<"POST">>; atom_to_method(delete) -> <<"DELETE">>. diff --git a/src/sr_single_entity_handler.erl b/src/sr_single_entity_handler.erl index 380bc45..88d025f 100644 --- a/src/sr_single_entity_handler.erl +++ b/src/sr_single_entity_handler.erl @@ -15,6 +15,7 @@ , content_types_accepted/2 , handle_get/2 , handle_put/2 + , handle_patch/2 , delete_resource/2 ]). @@ -53,7 +54,9 @@ resource_exists(Req, State) -> -spec content_types_accepted(cowboy_req:req(), state()) -> {[{{binary(), binary(), '*'}, atom()}], cowboy_req:req(), state()}. content_types_accepted(Req, State) -> - {[{{<<"application">>, <<"json">>, '*'}, handle_put}], Req, State}. + {Method, Req1} = cowboy_req:method(Req), + Function = method_function(Method), + {[{{<<"application">>, <<"json">>, '*'}, Function}], Req1, State}. -spec handle_get(cowboy_req:req(), state()) -> {iodata(), cowboy_req:req(), state()}. @@ -62,6 +65,20 @@ handle_get(Req, State) -> ResBody = sr_json:encode(Model:to_json(Entity)), {ResBody, Req, State}. +-spec handle_patch(cowboy_req:req(), state()) -> + {{true, binary()} | false | halt, cowboy_req:req(), state()}. +handle_patch(Req, #{entity := Entity} = State) -> + #{opts := #{model := Model}} = State, + try + {ok, Body, Req1} = cowboy_req:body(Req), + Json = sr_json:decode(Body), + persist(Model:update(Entity, Json), Req1, State) + catch + _:badjson -> + Req3 = cowboy_req:set_resp_body(<<"Malformed JSON request">>, Req), + {false, Req3, State} + end. + -spec handle_put(cowboy_req:req(), state()) -> {{true, binary()} | false | halt, cowboy_req:req(), state()}. handle_put(Req, #{entity := Entity} = State) -> @@ -69,7 +86,7 @@ handle_put(Req, #{entity := Entity} = State) -> try {ok, Body, Req1} = cowboy_req:body(Req), Json = sr_json:decode(Body), - handle_put(Model:update(Entity, Json), Req1, State) + persist(Model:update(Entity, Json), Req1, State) catch _:badjson -> Req3 = cowboy_req:set_resp_body(<<"Malformed JSON request">>, Req), @@ -80,7 +97,7 @@ handle_put(Req, #{id := Id} = State) -> try {ok, Body, Req1} = cowboy_req:body(Req), Json = sr_json:decode(Body), - handle_put(from_json(Model, Id, Json), Req1, State) + persist(from_json(Model, Id, Json), Req1, State) catch _:badjson -> Req3 = cowboy_req:set_resp_body(<<"Malformed JSON request">>, Req), @@ -103,12 +120,16 @@ from_json(Model, Id, Json) -> _:undef -> Model:from_json(Json) end. -handle_put({error, Reason}, Req, State) -> +persist({error, Reason}, Req, State) -> Req1 = cowboy_req:set_resp_body(Reason, Req), {false, Req1, State}; -handle_put({ok, Entity}, Req1, State) -> +persist({ok, Entity}, Req1, State) -> #{opts := #{model := Model}} = State, PersistedEntity = sumo:persist(Model, Entity), ResBody = sr_json:encode(Model:to_json(PersistedEntity)), Req2 = cowboy_req:set_resp_body(ResBody, Req1), {true, Req2, State}. + +-spec method_function(binary()) -> atom(). +method_function(<<"PUT">>) -> handle_put; +method_function(<<"PATCH">>) -> handle_patch. diff --git a/test/sr_elements_SUITE.erl b/test/sr_elements_SUITE.erl index b9c50eb..51e444c 100644 --- a/test/sr_elements_SUITE.erl +++ b/test/sr_elements_SUITE.erl @@ -69,9 +69,10 @@ success_scenario(_Config) -> sr_test_utils:api_call( put, "/elements/element1", Headers, #{ key => <<"element1">> - , value => <<"newval1">> + , value => <<"newval3">> }), #{ <<"key">> := <<"element1">> + , <<"value">> := <<"newval3">> , <<"created_at">> := CreatedAt , <<"updated_at">> := UpdatedAt } = Element3 = sr_json:decode(Body3), @@ -82,40 +83,57 @@ success_scenario(_Config) -> sr_test_utils:api_call(get, "/elements"), [Element3] = sr_json:decode(Body4), + ct:comment("The element value can be changed by PATCH"), + #{status_code := 200, body := Body5} = + sr_test_utils:api_call( + patch, "/elements/element1", Headers, #{value => <<"newval5">>}), + #{ <<"key">> := <<"element1">> + , <<"value">> := <<"newval5">> + , <<"created_at">> := CreatedAt + , <<"updated_at">> := UpdatedAt5 + } = Element5 = sr_json:decode(Body5), + true = UpdatedAt5 >= CreatedAt, + + ct:comment("Still just one element"), + #{status_code := 200, body := Body6} = + sr_test_utils:api_call(get, "/elements"), + [Element5] = sr_json:decode(Body6), + ct:comment("Elements can be created by PUT"), - #{status_code := 201, body := Body5} = + #{status_code := 201, body := Body7} = sr_test_utils:api_call( put, "/elements/element2", Headers, #{ key => <<"element2">> , value => <<"val2">> }), #{ <<"key">> := <<"element2">> - , <<"created_at">> := CreatedAt5 - , <<"updated_at">> := CreatedAt5 - } = Element5 = sr_json:decode(Body5), - true = CreatedAt5 >= CreatedAt, + , <<"value">> := <<"val2">> + , <<"created_at">> := CreatedAt7 + , <<"updated_at">> := CreatedAt7 + } = Element7 = sr_json:decode(Body7), + true = CreatedAt7 >= CreatedAt, ct:comment("There are two elements now"), - #{status_code := 200, body := Body6} = + #{status_code := 200, body := Body8} = sr_test_utils:api_call(get, "/elements"), - [Element5] = sr_json:decode(Body6) -- [Element3], + [Element7] = sr_json:decode(Body8) -- [Element5], ct:comment("Element1 is deleted"), #{status_code := 204} = sr_test_utils:api_call(delete, "/elements/element1"), ct:comment("One element again"), - #{status_code := 200, body := Body7} = + #{status_code := 200, body := Body9} = sr_test_utils:api_call(get, "/elements"), - [Element5] = sr_json:decode(Body7), + [Element7] = sr_json:decode(Body9), ct:comment("DELETE is not idempotent"), #{status_code := 204} = sr_test_utils:api_call(delete, "/elements/element2"), #{status_code := 404} = sr_test_utils:api_call(delete, "/elements/element2"), ct:comment("There are no elements"), - #{status_code := 200, body := Body8} = + #{status_code := 200, body := Body10} = sr_test_utils:api_call(get, "/elements"), - [] = sr_json:decode(Body8), + [] = sr_json:decode(Body10), {comment, ""}. @@ -181,12 +199,16 @@ invalid_parameters(_Config) -> sr_test_utils:api_call(put, "/elements/nobody", Headers, <<>>), #{status_code := 400} = sr_test_utils:api_call(put, "/elements/key", Headers, <<>>), + #{status_code := 400} = + sr_test_utils:api_call(patch, "/elements/key", Headers, <<>>), #{status_code := 400} = sr_test_utils:api_call(post, "/elements", Headers, <<"{">>), #{status_code := 400} = sr_test_utils:api_call(put, "/elements/broken", Headers, <<"{">>), #{status_code := 400} = sr_test_utils:api_call(put, "/elements/key", Headers, <<"{">>), + #{status_code := 400} = + sr_test_utils:api_call(patch, "/elements/key", Headers, <<"{">>), ct:comment("Missing parameters are reported"), None = #{}, @@ -196,6 +218,8 @@ invalid_parameters(_Config) -> sr_test_utils:api_call(put, "/elements/none", Headers, None), #{status_code := 400} = sr_test_utils:api_call(put, "/elements/key", Headers, None), + #{status_code := 400} = + sr_test_utils:api_call(patch, "/elements/key", Headers, None), NoVal = #{key => <<"noval">>}, #{status_code := 400} = @@ -204,6 +228,8 @@ invalid_parameters(_Config) -> sr_test_utils:api_call(put, "/elements/noval", Headers, NoVal), #{status_code := 400} = sr_test_utils:api_call(put, "/elements/key", Headers, NoVal), + #{status_code := 400} = + sr_test_utils:api_call(patch, "/elements/key", Headers, NoVal), {comment, ""}. @@ -211,5 +237,6 @@ invalid_parameters(_Config) -> not_found(_Config) -> ct:comment("Not existing element is not found"), #{status_code := 404} = sr_test_utils:api_call(get, "/elements/notfound"), + #{status_code := 404} = sr_test_utils:api_call(patch, "/elements/notfound"), #{status_code := 404} = sr_test_utils:api_call(delete, "/elements/notfound"), {comment, ""}. diff --git a/test/sr_test/sr_elements.erl b/test/sr_test/sr_elements.erl index ecca69a..574b6e1 100644 --- a/test/sr_test/sr_elements.erl +++ b/test/sr_test/sr_elements.erl @@ -87,11 +87,14 @@ from_json(Json) -> -spec update(element(), sumo_rest_doc:json()) -> {ok, element()} | {error, iodata()}. update(Element, Json) -> - case from_json(Json) of - {error, Reason} -> {error, Reason}; - {ok, Updates} -> - UpdatedElement = maps:merge(Element, Updates), - {ok, UpdatedElement#{updated_at => calendar:universal_time()}} + try + NewValue = maps:get(<<"value">>, Json), + UpdatedElement = + Element#{value := NewValue, updated_at := calendar:universal_time()}, + {ok, UpdatedElement} + catch + _:{badkey, Key} -> + {error, <<"missing field: ", Key/binary>>} end. -spec uri_path(element()) -> binary(). diff --git a/test/sr_test/sr_single_element_handler.erl b/test/sr_test/sr_single_element_handler.erl index 900a27c..348730a 100644 --- a/test/sr_test/sr_single_element_handler.erl +++ b/test/sr_test/sr_single_element_handler.erl @@ -13,6 +13,7 @@ , content_types_provided/2 , handle_get/2 , handle_put/2 + , handle_patch/2 , delete_resource/2 ] }]). @@ -42,6 +43,13 @@ trails() -> , produces => ["application/json"] , parameters => [Id] } + , patch => + #{ tags => ["elements"] + , description => "Updates an element" + , consumes => ["application/json"] + , produces => ["application/json"] + , parameters => [RequestBody, Id] + } , put => #{ tags => ["elements"] , description => "Updates or creates a new element"