Skip to content

Instantly share code, notes, and snippets.

@teburd
Created July 14, 2011 05:20
Show Gist options
  • Save teburd/1081983 to your computer and use it in GitHub Desktop.
Save teburd/1081983 to your computer and use it in GitHub Desktop.
Cowboy HTTP REST
-module(cowboy_http_rest).
-export([]).
-record(state, {
res,
res_state,
content_types_provided,
content_type_accepted
}).
%% HTTP.
%% @todo Should probably check that init is exported before calling it.
%% @todo Some error handling here?
init(Transport, Req, {Res, Opts}) ->
{ok, ResState} = Res:init(Transport, Req, Opts),
{ok, #state{res=Res, res_state=ResState}}.
handle(Req, State) ->
service_available(Req, State).
%% @todo Some error handling here?
terminate(Req, #state{res=Res, res_state=ResState}) ->
ok = Res:terminate(Req, ResState).
%% Internal. REST primitives.
expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
case call(Req, State, Callback) of
no_call ->
next(Req, State, OnTrue);
{Expected, Req2, ResState2} ->
next(Req2, State#state{res_state=ResState2}, OnTrue);
{_Unexpected, Req2, ResState2} ->
next(Req2, State#state{res_state=ResState2}, OnFalse)
end.
member(Req, State, Callback, Element, OnTrue, OnFalse) ->
case call(Req, State, Callback) of
no_call ->
next(Req, State, OnTrue);
{List, Req2, ResState2} ->
State2 = State#state{res_state=ResState2},
case lists:member(Element, List) of
true -> next(Req2, State2, OnTrue);
false -> next(Req2, State2, OnFalse)
end
end.
call(Req, State=#state{res=Res, res_state=ResState}, Callback) ->
case erlang:function_exported(Res, Callback, 2) of
true -> Res:Fun(Req, ResState);
false -> no_call
end.
next(Req, State, Next) when is_function(Next) ->
Next(Req, State);
next(Req, State, StatusCode) when is_integer(StatusCode) ->
%% @todo This.
response(Req, State, StatusCode).
%% Internal. REST logic.
%% @todo Have a {halt, Code} like WebMachine?
service_available(Req, State) ->
expect(Req, State, service_available, true, fun known_methods/2, 503).
known_methods(Req=#http_req{method=Method}, State) ->
member(Req, State, known_methods, Method, fun uri_too_long/2, 501).
uri_too_long(Req, State) ->
expect(Req, State, uri_too_long, true, 414, fun allowed_methods/2).
allowed_methods(Req=#http_req{method=Method}, State) ->
member(Req, State, allowed_methods, Method,
fun malformed_request/2, fun method_not_allowed/2).
%% @todo Binary! Binary?
method_not_allowed(Req, State) ->
response(Req, State, 405, [{'Allow',
string:join([atom_to_list(M) || M <- Methods], ", ")}]).
malformed_request(Req, State) ->
expect(Req, State, malformed_request, true, 400, fun is_authorized/2).
is_authorized(Req, State) ->
case call(Req, State, is_authorized) of
no_call ->
forbidden(Req, State);
{true, Req2, ResState2} ->
forbidden(Req2, State#state{res_state=ResState2});
%% @todo This should probably be {false, AuthHead}.
{AuthHead, Req2, ResState2} ->
response(Req2, State#state{res_state=ResState2}, 401,
[{'WWW-Authenticate', AuthHead}])
end.
forbidden(Req, State) ->
expect(Req, State, forbidden, true, 403, fun valid_content_headers/2).
valid_content_headers(Req, State) ->
expect(Req, State, valid_content_headers, true,
fun known_content_type/2, 501).
known_content_type(Req, State) ->
expect(Req, State, known_content_type, true,
fun valid_entity_length/2, 413).
valid_entity_length(Req, State) ->
expect(Req, State, valid_entity_length, true, fun options/2, 413).
options(Req=#http_req{method='OPTIONS'}, State) ->
{Headers, Req2, ResState2} = call(Req, State, options),
response(Req2, State#state{res_state=ResState2}, 200, Headers);
options(Req, State) ->
content_types_provided(Req, State).
%% @todo Yeah I don't think that kind of defaults are doing any good.
%% It makes things a lot less clear as to_html isn't explicit.
content_types_provided(Req, State) ->
case call(Req, State, content_types_provided) of
no_call ->
choose_content_type(Req, State#state{
content_types_provided=[{<<"text/html">>, to_html}]});
{ContentTypes, Req2, ResState2} ->
choose_content_type(Req2, State#state{res_state=ResState2,
content_types_provided=ContentTypes})
end.
choose_content_type(Req, State=#state{content_types_provided=ContentTypes}) ->
{Accept, Req2} = cowboy_http_req:header('Accept', Req),
case Accept of
undefined ->
language_available(Req2, State#state{
content_type_accepted=hd(ContentTypes)});
_Any ->
case doit() of %% @todo webmachine_util:choose_media_type(Types, Accept)
undefined ->
response(Req2, State, 406);
ContentTypeAccepted ->
languages_provided(Req2, State#state{
content_type_accepted=ContentTypeAccepted})
end
end.
%% @todo That one should be chosen just like the Content-Type.
%% We don't care if the language is available, we want to select
%% the most appropriate language.
languages_provided(Req, State) ->
{AcceptLanguage, Req2} = cowboy_http_req:header('Accept-Language', Req),
case AcceptLanguage of
undefined ->
charsets_provided(Req2, State);
_Any ->
expect(Req2, State, language_available, true,
fun charsets_provided/2, 406)
end.
charsets_provided(Req, State) ->
etc.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment