Last active
August 29, 2015 14:23
-
-
Save ferd/8fc1e9b6019aefcf2ac0 to your computer and use it in GitHub Desktop.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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