Created
July 14, 2011 05:20
-
-
Save teburd/1081983 to your computer and use it in GitHub Desktop.
Cowboy HTTP REST
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(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