Skip to content

Instantly share code, notes, and snippets.

@ferd
Last active August 29, 2015 14:23
Show Gist options
  • Save ferd/8fc1e9b6019aefcf2ac0 to your computer and use it in GitHub Desktop.
Save ferd/8fc1e9b6019aefcf2ac0 to your computer and use it in GitHub Desktop.
-module(app_rest_handler).
%% general callbacks
-export([init/3, rest_init/2, rest_terminate/2,
service_available/2, allowed_methods/2, is_authorized/2,
forbidden/2, content_types_provided/2, content_types_accepted/2,
resource_exists/2]).
%% conditional/cache-related
-export([generate_etag/2, last_modified/2, expires/2]).
%% GET/HEAD callbacks
-export([to_json/2, to_html/2]).
%% PUT/POST callbacks
-export([allow_missing_post/2, is_conflict/2, write_resource/2]).
%% DELETE callbacks
-export([delete_resource/2, delete_completed/2]).
-define(PRIV, code:priv_dir(app)).
-define(TEMPLATE(Name), filename:join([?PRIV, "templates", Name])).
%%%%%%%%%%%%%%%%%%%%%%%%%
%%% General Callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%
init(_Proto, _Req, _Opts) ->
{upgrade, protocol, cowboy_rest}.
rest_init(Req, Opts) ->
%% See:
%% - http://ninenines.eu/docs/en/cowboy/1.0/guide/rest_handlers/
%% - http://ninenines.eu/docs/en/cowboy/1.0/guide/rest_flowcharts/
%% - http://ninenines.eu/docs/en/cowboy/1.0/guide/resource_design/
{ok, Req, Opts}.
rest_terminate(_, _) ->
ok.
%% Determine if lockdown mode is set for the app (503 if so)
service_available(Req, Opts) ->
{not application:get_env(app, lockdown, false), Req, Opts}.
%% make this a standard CRUD resource
allowed_methods(Req, Opts) ->
{[<<"GET">>, <<"HEAD">>, <<"PUT">>, <<"POST">>, <<"DELETE">>, <<"OPTIONS">>],
Req, Opts}.
%% HTTP Auth
is_authorized(Req, Opts) ->
%% Assume all users are valid for this, as long as a user is provided
case cowboy_req:parse_header(<<"authorization">>, Req) of
{ok, {<<"basic">>, {User, _Pass}}, Req2} ->
{true, Req2, Opts#{user => User}};
_ ->
{{false, <<"Basic realm=\"app\"">>}, Req, Opts}
end.
%% Does the user have access to this resource?
forbidden(Req, Opts) ->
%% Assume the users have access to everything (default value)
{false, Req, Opts}.
%% The content types we're ready to serve
content_types_provided(Req, Opts) ->
%% Provided mimetype with the function to convert the resource to the
%% proper format; if */* is used, first in the list is prioritized
{[{{<<"text">>, <<"html">>, '*'}, to_html},
{{<<"application">>, <<"json">>, '*'}, to_json}],
Req, Opts}.
%% The content types we're ready to accept
content_types_accepted(Req, Opts) ->
%% We only accept JSON
{[{{<<"application">>, <<"json">>, '*'}, write_resource}],
Req, Opts}.
%% Identify the resource or if it doesn't exist.
resource_exists(Req, Opts) ->
case cowboy_req:binding(id, Req) of
{undefined, Req2} ->
%% we consider just an undefined id to be the index
{true, Req2, Opts#{resource => index}};
{Id, Req2} ->
%% An ID was provided; does it identify an existing resource?
case app_resource:exists(Id) of
true ->
{true, Req2, Opts#{resource => Id}};
false ->
{false, Req2, Opts#{resource => undefined,
submitted_id => Id}}
end
end.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% Conditional/Caching Callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Generate an etag for the given resource.
generate_etag(Req, Opts=#{resource := undefined}) ->
{undefined, Req, Opts};
generate_etag(Req, Opts=#{resource := index}) ->
%% don't care for the index itself
{undefined, Req, Opts};
generate_etag(Req, Opts=#{resource := Id}) ->
{ok, Resource, Meta} = app_resource:find(Id),
Etag = integer_to_binary(erlang:phash2({Resource, Meta})),
%% because the etag is the full hash of the resource + metadata
%% we can consider it strong.
{{strong, Etag}, Req, Opts}.
last_modified(Req, Opts=#{resource := undefined}) ->
{undefined, Req, Opts};
last_modified(Req, Opts=#{resource := index}) ->
{undefined, Req, Opts};
last_modified(Req, Opts=#{resource := Id}) ->
{ok, _Resource, Meta} = app_resource:find(Id),
Stamp = case proplists:get_value(timestamp, Meta) of
undefined -> undefined;
Tup -> calendar:now_to_datetime(Tup)
end,
{Stamp, Req, Opts}.
%% TTL; we specifically set none for the index, otherwise 1 min
expires(Req, Opts=#{resource := Res}) when is_atom(Res) ->
{undefined, Req, Opts};
expires(Req, Opts=#{resource := _}) ->
{MegaSecs, Secs, MicroSecs} = os:timestamp(),
FinalTime = {MegaSecs, Secs+60, MicroSecs},
Stamp = calendar:now_to_datetime(FinalTime),
{Stamp, Req, Opts}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% GET and HEAD callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Here we implement the callbacks to generate our representation of a resource
%% as defined in content_types_provided/2
to_json(Req, Opts=#{resource := index}) ->
List = [Data#{<<"id">> => Key} || {Key, {Data, _Meta}} <- app_resource:list()],
{jsx:encode(List), Req, Opts};
to_json(Req, Opts=#{resource := Id}) ->
{ok, Data, _Meta} = app_resource:find(Id),
{jsx:encode(Data), Req, Opts}.
to_html(Req, Opts=#{resource := index, user := User}) ->
List = [Data#{<<"id">> => Key} || {Key, {Data, _Meta}} <- app_resource:list()],
Data = [{<<"user">>, User}, {<<"list">>, List}],
{render("index.html", Data), Req, Opts};
to_html(Req, Opts=#{resource := Id, user := User}) ->
{ok, Resource, _Meta} = app_resource:find(Id),
Data = [{<<"user">>, User}, {<<"resource">>, Resource}],
{render("resource.html", Data), Req, Opts}.
%% None of these need to specifically be implemented, but can be if resources
%% are allowed to be moved:
%% previously_existed, moved_permanently, moved_temporarily
%% See the docs if this is the case.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% PUT/POST Callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Choose if the resource can be 'POST'ed to when it doesn't exist
allow_missing_post(Req, Opts=#{resource := undefined}) ->
%% We allow people to choose their IDs if they want
{true, Req, Opts}.
%% is_conflict allows to restrain access to a resource already being
%% updated, for example. We don't have this problem. Only useful for PUTs.
is_conflict(Req, Opts) ->
{false, Req, Opts}.
%% Callbacks from content_types_accepted/2
write_resource(Req, Opts = #{resource := index}) ->
%% Only allow POST to the index and give them an ID; we can't
%% allow people to modify the index, but we interpret that as
%% 'please add a resource to this collection'.
case cowboy_req:method(Req) of
{<<"POST">>, Req2} ->
Id = app_resource:create_id(),
{ok, Body, Req3} = body(Req2),
Entry = jsx:decode(Body, [return_maps]),
app_resource:create(Id, Entry, [{timestamp, os:timestamp()}]),
{{true, <<"/resources/", (iolist_to_binary(Id))/binary>>},
Req3, Opts};
_ ->
{false, Req, Opts}
end;
write_resource(Req, Opts = #{resource := undefined, submitted_id := Id}) ->
%% Get the full body (in practice, putting a size limit and validation
%% would be a good idea!) and create the resource with the requested Id
{ok, Body, Req2} = body(Req),
Entry = jsx:decode(Body, [return_maps]),
app_resource:create(Id, Entry, [{timestamp, os:timestamp()}]),
{true, Req2, Opts};
write_resource(Req, Opts = #{resource := Id}) ->
%% overwrite an existing resource
{ok, Body, Req2} = body(Req),
Entry = jsx:decode(Body, [return_maps]),
app_resource:create(Id, Entry, [{timestamp, os:timestamp()}]),
{true, Req2, Opts}.
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%%% DELETE Method callbacks %%%
%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
%% Delete the resource, flush caches, do all the things.
delete_resource(Req, Opts=#{resource := index}) ->
{false, Req, Opts};
delete_resource(Req, Opts=#{resource := Id}) ->
app_resource:delete(Id),
{true, Req, Opts}.
%% It's possible the delete operation is asynchronous and we don't know
%% when it is exactly finished; in this case, return 'true'
delete_completed(Req, Opts) ->
{true, Req, Opts}.
%%%%%%%%%%%%%%%
%%% Helpers %%%
%%%%%%%%%%%%%%%
render(Template, Data) ->
{ok, TplBin} = file:read_file(?TEMPLATE(Template)),
mustache:render(TplBin, Data, [{key_type, binary}]).
body(Req) -> body(Req, <<>>).
body(Req, Acc) ->
case cowboy_req:body(Req) of
{ok, Bin, Req2} -> {ok, <<Acc/binary, Bin/binary>>, Req2};
{more, Bin, Req2} -> body(Req2, <<Acc/binary, Bin/binary>>);
Other -> Other
end.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment