%%% @doc Base GET|PUT|DELETE /[entity]s/:id implementation -module(sr_single_entity_handler). -include_lib("mixer/include/mixer.hrl"). -mixin([{ sr_entities_handler , [ init/3 , allowed_methods/2 , content_types_provided/2 , content_types_accepted/2 , announce_req/2 ] }]). -export([ rest_init/2 , resource_exists/2 , handle_get/2 , handle_put/2 , handle_patch/2 , delete_resource/2 , id_from_binding_internal/2 % exported only for test coverage ]). -type options() :: #{ path => string() , model => atom() , verbose => boolean() }. -type state() :: sr_state:state(). -export_type([state/0, options/0]). %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Cowboy Callbacks %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %% @doc Announces the Req and moves on. %% It extracts the :id binding from the Req and leaves it in %% the id key in the state. %% @see cowboy_rest:rest_init/2 -spec rest_init(cowboy_req:req(), options()) -> {ok, cowboy_req:req(), state()}. rest_init(Req, Opts) -> Req1 = announce_req(Req, Opts), #{model := Model} = Opts, Module = sumo_config:get_prop_value(Model, module), {Id, Req2} = cowboy_req:binding(id, Req1), ActualId = id_from_binding(Id, Model, Module), State = sr_state:new(Opts, Module), State1 = sr_state:id(State, ActualId), {ok, Req2, State1}. %% @doc Verifies if there is an entity with the given id. %% The provided id must be the value for the id field in %% SumoDb. If the entity is found, it's kept in the %% state. %% @see cowboy_rest:resource_exists/2 %% @see sumo:find/2 -spec resource_exists(cowboy_req:req(), state()) -> {boolean(), cowboy_req:req(), state()}. resource_exists(Req, State) -> Id = sr_state:id(State), #{model := Model} = sr_state:opts(State), case sumo:fetch(Model, Id) of notfound -> {false, Req, State}; Entity -> {true, Req, sr_state:entity(State, Entity)} end. %% @doc Renders the found entity. %% @see resource_exists/2 -spec handle_get(cowboy_req:req(), state()) -> {iodata(), cowboy_req:req(), state()}. handle_get(Req, State) -> Entity = sr_state:entity(State), Module = sr_state:module(State), ResBody = sr_json:encode(Module:to_json(Entity)), {ResBody, Req, State}. %% @doc Updates the found entity. %% To parse the body, it uses update/2 from the %% model provided in the options. %% @see resource_exists/2 -spec handle_patch(cowboy_req:req(), state()) -> {boolean() | halt, cowboy_req:req(), state()}. handle_patch(Req, State) -> Entity = sr_state:entity(State), Module = sr_state:module(State), try {ok, Body, Req1} = cowboy_req:body(Req), Json = sr_json:decode(Body), persist(Module:update(Entity, Json), Req1, State) catch _:badjson -> Req3 = cowboy_req:set_resp_body( sr_json:error(<<"Malformed JSON request">>), Req), {false, Req3, State} end. %% @doc Updates the entity if found, otherwise it creates a new one. %% To parse the body, it uses either update/2 or %% from_json/2 (if defined) or from_json/1 %% from the model provided in the options. %% @see resource_exists/2 -spec handle_put(cowboy_req:req(), state()) -> {boolean() | halt, cowboy_req:req(), state()}. handle_put(Req, State) -> try Module = sr_state:module(State), {SrRequest, Req1} = sr_request:from_cowboy(Req), Entity = case sr_state:entity(State) of undefined -> Context = #{req => SrRequest, state => State}, build_entity(Context); OldEntity -> Json = sr_request:body(SrRequest), Module:update(OldEntity, Json) end, persist(Entity, Req1, State) catch _:badjson -> Req2 = cowboy_req:set_resp_body( sr_json:error(<<"Malformed JSON request">>), Req), {false, Req2, State} end. %% @doc Deletes the found entity. %% @see resource_exists/2 -spec delete_resource(cowboy_req:req(), state()) -> {boolean() | halt, cowboy_req:req(), state()}. delete_resource(Req, State) -> Id = sr_state:id(State), #{model := Model} = sr_state:opts(State), Result = sumo:delete(Model, Id), {Result, Req, State}. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% %%% Auxiliary Functions %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%% build_entity(#{req := SrRequest, state := State} = Context) -> Module = sr_state:module(State), case erlang:function_exported(Module, from_ctx, 1) of false -> Id = sr_state:id(State), Json = sr_request:body(SrRequest), from_json(Module, Id, Json); true -> Module:from_ctx(Context) end. from_json(Module, Id, Json) -> try Module:from_json(Id, Json) catch _:undef -> Module:from_json(Json) end. persist({error, Reason}, Req, State) -> Req1 = cowboy_req:set_resp_body(sr_json:error(Reason), Req), {false, Req1, State}; persist({ok, Entity}, Req1, State) -> Module = sr_state:module(State), #{model := Model} = sr_state:opts(State), PersistedEntity = sumo:persist(Model, Entity), ResBody = sr_json:encode(Module:to_json(PersistedEntity)), Req2 = cowboy_req:set_resp_body(ResBody, Req1), {true, Req2, State}. -spec id_from_binding(binary(), atom(), atom()) -> term(). id_from_binding(Id, Model, Module) -> case erlang:function_exported(Module, id_from_binding, 1) of false -> id_from_binding_internal(Id, sumo_internal:id_field_type(Model)); true -> Module:id_from_binding(Id) end. -spec id_from_binding_internal(binary(), binary | string | integer) -> term(). id_from_binding_internal(Id, binary) -> Id; id_from_binding_internal(Id, string) -> binary_to_list(Id); id_from_binding_internal(BinaryId, integer) -> try binary_to_integer(BinaryId) of Id -> Id catch error:badarg -> -1 end.