aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorLoïc Hoguin <[email protected]>2011-12-05 22:53:59 +0100
committerLoïc Hoguin <[email protected]>2011-12-05 23:05:32 +0100
commitaab1587a4b3d8f0c3d92a2083227527d51109980 (patch)
tree4ac3dc54b3736a4ea86aff716fc7ce6812bf9188
parent7acaa996ed8dea5723df6e83729236e57e6eb850 (diff)
downloadcowboy-aab1587a4b3d8f0c3d92a2083227527d51109980.tar.gz
cowboy-aab1587a4b3d8f0c3d92a2083227527d51109980.tar.bz2
cowboy-aab1587a4b3d8f0c3d92a2083227527d51109980.zip
Add experimental Webmachine based REST protocol support
As with everything experimental, it probably has a lot of bugs and may not even work. Like Websocket, REST must be upgraded from a standard resource through the init/3 function. A key difference between Webmachine and Cowboy's REST protocol handler is that in Cowboy the resource has direct access to the request object. This makes a small change in a few places where you were expected to return headers or body in Webmachine and are now expected to set them directly yourself if needed (options/2, for example). Another difference is that the functions rest_init/2 will always be called when starting to process a request. Similarly, rest_terminate/2 will be called when the process completes successfully. The Cowboy REST support also includes automatic language selection, thanks to the languages_provided/2 callback. Finally, Cowboy REST expects full URIs to be given at all times, and will not try to reconstruct URIs from fragments. Note that REST requests cannot be chained (keepalive) at this time. This is a design issue in cowboy_http_protocol that will be fixed soon. Check out the source for more details. It has been designed to be very easy to read and understand so if you don't understand something, it's probably a bug. Thanks in advance for all the great bug reports, pull requests and comments you'll forward my way!
-rw-r--r--src/cowboy_http_rest.erl758
-rw-r--r--test/http_SUITE.erl21
-rw-r--r--test/rest_simple_resource.erl12
3 files changed, 789 insertions, 2 deletions
diff --git a/src/cowboy_http_rest.erl b/src/cowboy_http_rest.erl
new file mode 100644
index 0000000..9bb66fa
--- /dev/null
+++ b/src/cowboy_http_rest.erl
@@ -0,0 +1,758 @@
+%% Copyright (c) 2011, Loïc Hoguin <[email protected]>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Experimental REST protocol implementation.
+%%
+%% Based on the Webmachine Diagram from Alan Dean and Justin Sheehy, which
+%% can be found in the Webmachine source tree, and on the Webmachine
+%% documentation available at http://wiki.basho.com/Webmachine.html
+%% at the time of writing.
+-module(cowboy_http_rest).
+-export([upgrade/4]).
+
+-record(state, {
+ %% Handler.
+ handler :: atom(),
+ handler_state :: any(),
+
+ %% Media type.
+ content_types_p = [] ::
+ [{{binary(), binary(), [{binary(), binary()}]}, atom()}],
+ content_type_a :: undefined
+ | {{binary(), binary(), [{binary(), binary()}]}, atom()},
+
+ %% Language.
+ languages_p = [] :: [binary()],
+ language_a :: undefined | binary(),
+
+ %% Charset.
+ charsets_p = [] :: [binary()],
+ charset_a :: undefined | binary(),
+
+ %% Cached resource calls.
+ etag :: undefined | no_call | binary(),
+ last_modified :: undefined | no_call | cowboy_clock:datetime(),
+ expires :: undefined | no_call | cowboy_clock:datetime()
+}).
+
+-include("include/http.hrl").
+
+%% @doc Upgrade a HTTP request to the REST protocol.
+%%
+%% You do not need to call this function manually. To upgrade to the REST
+%% protocol, you simply need to return <em>{upgrade, protocol, {@module}}</em>
+%% in your <em>cowboy_http_handler:init/3</em> handler function.
+-spec upgrade(pid(), module(), any(), #http_req{}) -> ok.
+upgrade(_ListenerPid, Handler, Opts, Req) ->
+ try
+ case erlang:function_exported(Handler, rest_init, 2) of
+ true ->
+ case Handler:rest_init(Req, Opts) of
+ {ok, Req2, HandlerState} ->
+ service_available(Req2, #state{handler=Handler,
+ handler_state=HandlerState})
+ end;
+ false ->
+ service_available(Req, #state{handler=Handler})
+ end
+ catch Class:Reason ->
+ error_logger:error_msg(
+ "** Handler ~p terminating in rest_init/3~n"
+ " for the reason ~p:~p~n** Options were ~p~n"
+ "** Request was ~p~n** Stacktrace: ~p~n~n",
+ [Handler, Class, Reason, Opts, Req, erlang:get_stacktrace()]),
+ {ok, _Req2} = cowboy_http_req:reply(500, Req),
+ ok
+ end.
+
+service_available(Req, State) ->
+ expect(Req, State, service_available, true, fun known_methods/2, 503).
+
+known_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, known_methods) of
+ no_call ->
+ next(Req, State, fun uri_too_long/2);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun uri_too_long/2);
+ false -> next(Req2, State2, 501)
+ end
+ end.
+
+uri_too_long(Req, State) ->
+ expect(Req, State, uri_too_long, false, fun allowed_methods/2, 414).
+
+allowed_methods(Req=#http_req{method=Method}, State) ->
+ case call(Req, State, allowed_methods) of
+ no_call ->
+ next(Req, State, fun malformed_request/2);
+ {List, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ case lists:member(Method, List) of
+ true -> next(Req2, State2, fun malformed_request/2);
+ false -> method_not_allowed(Req2, State2, List)
+ end
+ end.
+
+method_not_allowed(Req, State, Methods) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Allow">>, method_not_allowed_build(Methods, []), Req),
+ respond(Req2, State, 405).
+
+method_not_allowed_build([], []) ->
+ <<>>;
+method_not_allowed_build([], [_Ignore|Acc]) ->
+ lists:reverse(Acc);
+method_not_allowed_build([Method|Tail], Acc) ->
+ Method2 = list_to_binary(atom_to_list(Method)),
+ method_not_allowed_build(Tail, [<<", ">>, Method2|Acc]).
+
+malformed_request(Req, State) ->
+ expect(Req, State, malformed_request, false, fun is_authorized/2, 400).
+
+is_authorized(Req, State) ->
+ case call(Req, State, is_authorized) of
+ no_call ->
+ forbidden(Req, State);
+ {true, Req2, HandlerState2} ->
+ forbidden(Req2, State#state{handler_state=HandlerState2});
+ {{false, AuthHead}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Www-Authenticate">>, AuthHead, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 401)
+ end.
+
+forbidden(Req, State) ->
+ expect(Req, State, forbidden, false, fun valid_content_headers/2, 403).
+
+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) ->
+ {ok, Req2, HandlerState2} = call(Req, State, options),
+ respond(Req2, State#state{handler_state=HandlerState2}, 200);
+options(Req, State) ->
+ content_types_provided(Req, State).
+
+%% The content_types_provided function MUST be defined in the resource
+%% from this point onward.
+content_types_provided(Req, State) ->
+ case call(Req, State, content_types_provided) of
+ no_call ->
+ not_acceptable(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {CTP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, content_types_p=CTP},
+ {Accept, Req3} = cowboy_http_req:parse_header('Accept', Req2),
+ case Accept of
+ undefined ->
+ languages_provided(Req3,
+ State2#state{content_type_a=hd(CTP)});
+ Accept ->
+ Accept2 = prioritize_accept(Accept),
+ choose_media_type(Req3, State2, Accept2)
+ end
+ end.
+
+prioritize_accept(Accept) ->
+ lists:sort(
+ fun ({MediaTypeA, Quality, _AcceptParamsA},
+ {MediaTypeB, Quality, _AcceptParamsB}) ->
+ %% Same quality, check precedence in more details.
+ prioritize_mediatype(MediaTypeA, MediaTypeB);
+ ({_MediaTypeA, QualityA, _AcceptParamsA},
+ {_MediaTypeB, QualityB, _AcceptParamsB}) ->
+ %% Just compare the quality.
+ QualityA > QualityB
+ end, Accept).
+
+%% Media ranges can be overridden by more specific media ranges or
+%% specific media types. If more than one media range applies to a given
+%% type, the most specific reference has precedence.
+%%
+%% We always choose B over A when we can't decide between the two.
+prioritize_mediatype({TypeA, SubTypeA, ParamsA}, {TypeB, SubTypeB, ParamsB}) ->
+ case TypeB of
+ TypeA ->
+ case SubTypeB of
+ SubTypeA -> length(ParamsA) > length(ParamsB);
+ <<"*">> -> true;
+ _Any -> false
+ end;
+ <<"*">> -> true;
+ _Any -> false
+ end.
+
+%% Ignoring the rare AcceptParams. Not sure what should be done about them.
+choose_media_type(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_media_type(Req, State=#state{content_types_p=CTP},
+ [MediaType|Tail]) ->
+ match_media_type(Req, State, Tail, CTP, MediaType).
+
+match_media_type(Req, State, Accept, [], _MediaType) ->
+ choose_media_type(Req, State, Accept);
+match_media_type(Req, State, Accept,
+ [Provided = {{Type, SubType_P, Params_P}, _Fun}|Tail],
+ MediaType = {{Type, SubType_A, Params_A}, _Quality, _AcceptParams})
+ when SubType_P =:= SubType_A; SubType_A =:= <<"*">> ->
+ case lists:sort(Params_P) =:= lists:sort(Params_A) of
+ true ->
+ languages_provided(Req, State#state{content_type_a=Provided});
+ false ->
+ match_media_type(Req, State, Accept, Tail, MediaType)
+ end;
+match_media_type(Req, State, Accept, [_Any|Tail], MediaType) ->
+ match_media_type(Req, State, Accept, Tail, MediaType).
+
+%% @todo I suppose we should also ask the resource if it wants to
+%% set a language itself or if it wants it to be automatically chosen.
+languages_provided(Req, State) ->
+ case call(Req, State, languages_provided) of
+ no_call ->
+ charsets_provided(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {LP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, languages_p=LP},
+ {AcceptLanguage, Req3} =
+ cowboy_http_req:parse_header('Accept-Language', Req2),
+ case AcceptLanguage of
+ undefined ->
+ set_language(Req3, State2#state{language_a=hd(LP)});
+ AcceptLanguage ->
+ AcceptLanguage2 = prioritize_languages(AcceptLanguage),
+ choose_language(Req3, State2, AcceptLanguage2)
+ end
+ end.
+
+%% A language-range matches a language-tag if it exactly equals the tag,
+%% or if it exactly equals a prefix of the tag such that the first tag
+%% character following the prefix is "-". The special range "*", if
+%% present in the Accept-Language field, matches every tag not matched
+%% by any other range present in the Accept-Language field.
+%%
+%% @todo The last sentence probably means we should always put '*'
+%% at the end of the list.
+prioritize_languages(AcceptLanguages) ->
+ lists:sort(
+ fun ({_TagA, QualityA}, {_TagB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptLanguages).
+
+choose_language(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_language(Req, State=#state{languages_p=LP}, [Language|Tail]) ->
+ match_language(Req, State, Tail, LP, Language).
+
+match_language(Req, State, Accept, [], _Language) ->
+ choose_language(Req, State, Accept);
+match_language(Req, State, _Accept, [Provided|_Tail], {'*', _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, _Accept, [Provided|_Tail], {Provided, _Quality}) ->
+ set_language(Req, State#state{language_a=Provided});
+match_language(Req, State, Accept, [Provided|Tail],
+ Language = {Tag, _Quality}) ->
+ Length = byte_size(Tag),
+ case Provided of
+ << Tag:Length/binary, $-, _Any/bits >> ->
+ set_language(Req, State#state{language_a=Provided});
+ _Any ->
+ match_language(Req, State, Accept, Tail, Language)
+ end.
+
+set_language(Req, State=#state{language_a=Language}) ->
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Language">>, Language, Req),
+ charsets_provided(Req2, State).
+
+charsets_provided(Req, State) ->
+ case call(Req, State, charsets_provided) of
+ no_call ->
+ set_content_type(Req, State);
+ {[], Req2, HandlerState2} ->
+ not_acceptable(Req2, State#state{handler_state=HandlerState2});
+ {CP, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2, charsets_p=CP},
+ {AcceptCharset, Req3} =
+ cowboy_http_req:parse_header('Accept-Charset', Req2),
+ case AcceptCharset of
+ undefined ->
+ set_content_type(Req3, State2#state{charset_a=hd(CP)});
+ AcceptCharset ->
+ AcceptCharset2 = prioritize_charsets(AcceptCharset),
+ choose_charset(Req3, State2, AcceptCharset2)
+ end
+ end.
+
+%% The special value "*", if present in the Accept-Charset field,
+%% matches every character set (including ISO-8859-1) which is not
+%% mentioned elsewhere in the Accept-Charset field. If no "*" is present
+%% in an Accept-Charset field, then all character sets not explicitly
+%% mentioned get a quality value of 0, except for ISO-8859-1, which gets
+%% a quality value of 1 if not explicitly mentioned.
+prioritize_charsets(AcceptCharsets) ->
+ AcceptCharsets2 = lists:sort(
+ fun ({_CharsetA, QualityA}, {_CharsetB, QualityB}) ->
+ QualityA > QualityB
+ end, AcceptCharsets),
+ case lists:keymember(<<"*">>, 1, AcceptCharsets2) of
+ true -> AcceptCharsets2;
+ false -> [{<<"iso-8859-1">>, 1000}|AcceptCharsets2]
+ end.
+
+choose_charset(Req, State, []) ->
+ not_acceptable(Req, State);
+choose_charset(Req, State=#state{charsets_p=CP}, [Charset|Tail]) ->
+ match_charset(Req, State, Tail, CP, Charset).
+
+match_charset(Req, State, Accept, [], _Charset) ->
+ choose_charset(Req, State, Accept);
+match_charset(Req, State, _Accept, [Provided|_Tail],
+ {Provided, _Quality}) ->
+ set_content_type(Req, State#state{charset_a=Provided});
+match_charset(Req, State, Accept, [_Provided|Tail], Charset) ->
+ match_charset(Req, State, Accept, Tail, Charset).
+
+set_content_type(Req, State=#state{
+ content_type_a={{Type, SubType, Params}, _Fun},
+ charset_a=Charset}) ->
+ ParamsBin = set_content_type_build_params(Params, []),
+ ContentType = [Type, <<"/">>, SubType, ParamsBin],
+ ContentType2 = case Charset of
+ undefined -> ContentType;
+ Charset -> [ContentType, <<"; charset=">>, Charset]
+ end,
+ {ok, Req2} = cowboy_http_req:set_resp_header(
+ <<"Content-Type">>, ContentType2, Req),
+ encodings_provided(Req2, State).
+
+set_content_type_build_params([], []) ->
+ <<>>;
+set_content_type_build_params([], Acc) ->
+ lists:reverse(Acc);
+set_content_type_build_params([{Attr, Value}|Tail], Acc) ->
+ set_content_type_build_params(Tail, [[Attr, <<"=">>, Value], <<";">>|Acc]).
+
+%% @todo Match for identity as we provide nothing else for now.
+%% @todo Don't forget to set the Content-Encoding header when we reply a body
+%% and the found encoding is something other than identity.
+encodings_provided(Req, State) ->
+ variances(Req, State).
+
+not_acceptable(Req, State) ->
+ respond(Req, State, 406).
+
+%% @todo Do Accept-Encoding too when we handle it.
+%% @todo Does the order matter?
+variances(Req, State=#state{content_types_p=CTP,
+ languages_p=LP, charsets_p=CP}) ->
+ Variances = case length(CTP) of
+ 0 -> [];
+ 1 -> [];
+ _NCT -> [<<"Accept">>]
+ end,
+ Variances2 = case length(LP) of
+ 0 -> Variances;
+ 1 -> Variances;
+ _NL -> [<<"Accept-Language">>|Variances]
+ end,
+ Variances3 = case length(CP) of
+ 0 -> Variances2;
+ 1 -> Variances2;
+ _NC -> [<<"Accept-Charset">>|Variances2]
+ end,
+ {Variances4, Req3, State2} = case call(Req, State, variances) of
+ no_call ->
+ {Variances3, Req, State};
+ {HandlerVariances, Req2, HandlerState} ->
+ {Variances3 ++ HandlerVariances, Req2,
+ State#state{handler_state=HandlerState}}
+ end,
+ case lists:flatten([[<<", ">>, V] || V <- Variances4]) of
+ [] ->
+ resource_exists(Req3, State2);
+ [<<", ">>, Variances5] ->
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Variances">>, Variances5, Req3),
+ resource_exists(Req4, State2)
+ end.
+
+resource_exists(Req, State) ->
+ expect(Req, State, resource_exists, true,
+ fun if_match_exists/2, fun if_match_musnt_exist/2).
+
+if_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Match', Req) of
+ {undefined, Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {'*', Req2} ->
+ if_unmodified_since_exists(Req2, State);
+ {ETagsList, Req2} ->
+ if_match(Req2, State, ETagsList)
+ end.
+
+if_match(Req, State, EtagsList) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ no_call ->
+ precondition_failed(Req2, State2);
+ Etag ->
+ case lists:member(Etag, EtagsList) of
+ true -> if_unmodified_since_exists(Req2, State2);
+ false -> precondition_failed(Req2, State2)
+ end
+ end.
+
+if_match_musnt_exist(Req, State) ->
+ case cowboy_http_req:header('If-Match', Req) of
+ {undefined, Req2} -> is_put_to_missing_resource(Req2, State);
+ {_Any, Req2} -> precondition_failed(Req2, State)
+ end.
+
+if_unmodified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Unmodified-Since', Req) of
+ {undefined, Req2} ->
+ if_none_match_exists(Req2, State);
+ {{error, badarg}, Req2} ->
+ if_none_match_exists(Req2, State);
+ {IfUnmodifiedSince, Req2} ->
+ if_unmodified_since(Req2, State, IfUnmodifiedSince)
+ end.
+
+%% If LastModified is the atom 'no_call', we continue.
+if_unmodified_since(Req, State, IfUnmodifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified > IfUnmodifiedSince of
+ true -> precondition_failed(Req2, State2);
+ false -> if_none_match_exists(Req2, State2)
+ end.
+
+if_none_match_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-None-Match', Req) of
+ {undefined, Req2} ->
+ if_modified_since_exists(Req2, State);
+ {'*', Req2} ->
+ precondition_is_head_get(Req2, State);
+ {EtagsList, Req2} ->
+ if_none_match(Req2, State, EtagsList)
+ end.
+
+if_none_match(Req, State, EtagsList) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ no_call ->
+ precondition_failed(Req2, State2);
+ Etag ->
+ case lists:member(Etag, EtagsList) of
+ true -> precondition_is_head_get(Req2, State2);
+ false -> if_modified_since_exists(Req2, State2)
+ end
+ end.
+
+precondition_is_head_get(Req=#http_req{method=Method}, State)
+ when Method =:= 'HEAD'; Method =:= 'GET' ->
+ not_modified(Req, State);
+precondition_is_head_get(Req, State) ->
+ precondition_failed(Req, State).
+
+if_modified_since_exists(Req, State) ->
+ case cowboy_http_req:parse_header('If-Modified-Since', Req) of
+ {undefined, Req2} ->
+ method(Req2, State);
+ {{error, badarg}, Req2} ->
+ method(Req2, State);
+ {IfModifiedSince, Req2} ->
+ if_modified_since_now(Req2, State, IfModifiedSince)
+ end.
+
+if_modified_since_now(Req, State, IfModifiedSince) ->
+ case IfModifiedSince > erlang:universaltime() of
+ true -> method(Req, State);
+ false -> if_modified_since(Req, State, IfModifiedSince)
+ end.
+
+if_modified_since(Req, State, IfModifiedSince) ->
+ {LastModified, Req2, State2} = last_modified(Req, State),
+ case LastModified of
+ no_call ->
+ method(Req2, State2);
+ LastModified ->
+ case LastModified > IfModifiedSince of
+ true -> method(Req2, State2);
+ false -> not_modified(Req2, State2)
+ end
+ end.
+
+not_modified(Req=#http_req{resp_headers=RespHeaders}, State) ->
+ RespHeaders2 = lists:keydelete(<<"Content-Type">>, 1, RespHeaders),
+ Req2 = Req#http_req{resp_headers=RespHeaders2},
+ {Req3, State2} = set_resp_etag(Req2, State),
+ {Req4, State3} = set_resp_expires(Req3, State2),
+ respond(Req4, State3, 304).
+
+precondition_failed(Req, State) ->
+ respond(Req, State, 412).
+
+is_put_to_missing_resource(Req=#http_req{method='PUT'}, State) ->
+ moved_permanently(Req, State, fun is_conflict/2);
+is_put_to_missing_resource(Req, State) ->
+ previously_existed(Req, State).
+
+moved_permanently(Req, State, OnFalse) ->
+ case call(Req, State, moved_permanently) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 301);
+ {false, Req2, HandlerState2} ->
+ OnFalse(Req2, State#state{handler_state=HandlerState2});
+ no_call ->
+ OnFalse(Req, State)
+ end.
+
+previously_existed(Req, State) ->
+ expect(Req, State, previously_existed, false,
+ fun (R, S) -> is_post_to_missing_resource(R, S, 404) end,
+ fun (R, S) -> moved_permanently(R, S, fun moved_temporarily/2) end).
+
+moved_temporarily(Req, State) ->
+ case call(Req, State, moved_temporarily) of
+ {{true, Location}, Req2, HandlerState2} ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ respond(Req3, State#state{handler_state=HandlerState2}, 307);
+ {false, Req2, HandlerState2} ->
+ is_post_to_missing_resource(Req2, State#state{handler_state=HandlerState2}, 410);
+ no_call ->
+ is_post_to_missing_resource(Req, State, 410)
+ end.
+
+is_post_to_missing_resource(Req=#http_req{method='POST'}, State, OnFalse) ->
+ allow_missing_post(Req, State, OnFalse);
+is_post_to_missing_resource(Req, State, OnFalse) ->
+ respond(Req, State, OnFalse).
+
+allow_missing_post(Req, State, OnFalse) ->
+ expect(Req, State, allow_missing_post, true, fun post_is_create/2, OnFalse).
+
+method(Req=#http_req{method='DELETE'}, State) ->
+ delete_resource(Req, State);
+method(Req=#http_req{method='POST'}, State) ->
+ post_is_create(Req, State);
+method(Req=#http_req{method='PUT'}, State) ->
+ is_conflict(Req, State);
+method(Req, State) ->
+ set_resp_body(Req, State).
+
+delete_resource(Req, State) ->
+ expect(Req, State, delete_resource, true, fun delete_completed/2, 500).
+
+delete_completed(Req, State) ->
+ expect(Req, State, delete_completed, true, fun has_resp_body/2, 202).
+
+post_is_create(Req, State) ->
+ expect(Req, State, post_is_create, false, fun process_post/2, fun create_path/2).
+
+create_path(Req, State) ->
+ case call(Req, State, create_path) of
+ {Location, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Location">>, Location, Req2),
+ put_resource(Req3, State2, 303)
+ end.
+
+process_post(Req, State) ->
+ case call(Req, State, process_post) of
+ {ok, _Req2, HandlerState} ->
+ _ = _State2 = State#state{handler_state=HandlerState},
+ todo %% @todo ???
+ end.
+
+is_conflict(Req, State) ->
+ expect(Req, State, is_conflict, false, fun put_resource/2, 409).
+
+put_resource(Req, State) ->
+ put_resource(Req, State, fun is_new_resource/2).
+
+put_resource(Req, State, OnTrue) ->
+ case call(Req, State, content_types_accepted) of
+ no_call ->
+ respond(Req, State, 415);
+ {CTA, Req2, HandlerState2} ->
+ State2 = State#state{handler_state=HandlerState2},
+ {ContentType, Req3}
+ = cowboy_http_req:parse_header('Content-Type', Req2),
+ choose_content_type(Req3, State2, OnTrue, ContentType, CTA)
+ end.
+
+choose_content_type(Req, State, _OnTrue, _ContentType, []) ->
+ respond(Req, State, 415);
+choose_content_type(Req, State, OnTrue, ContentType,
+ [{Accepted, Fun}|_Tail]) when ContentType =:= Accepted ->
+ case call(Req, State, Fun) of
+ {ok, Req2, HandlerState} ->
+ State2 = State#state{handler_state=HandlerState},
+ next(Req2, State2, OnTrue)
+ end;
+choose_content_type(Req, State, OnTrue, ContentType, [_Any|Tail]) ->
+ choose_content_type(Req, State, OnTrue, ContentType, Tail).
+
+is_new_resource(Req, State) ->
+ case cowboy_http_req:has_resp_header(<<"Location">>, Req) of
+ true -> respond(Req, State, 201);
+ false -> has_resp_body(Req, State)
+ end.
+
+has_resp_body(Req, State) ->
+ case cowboy_http_req:has_resp_body(Req) of
+ true -> multiple_choices(Req, State);
+ false -> respond(Req, State, 204)
+ end.
+
+set_resp_body(Req=#http_req{method=Method},
+ State=#state{content_type_a={_Type, Fun}})
+ when Method =:= 'GET'; Method =:= 'HEAD' ->
+ {Req2, State2} = set_resp_etag(Req, State),
+ {LastModified, Req3, State3} = last_modified(Req2, State2),
+ case LastModified of
+ LastModified when is_atom(LastModified) ->
+ Req4 = Req3;
+ LastModified ->
+ LastModifiedStr = httpd_util:rfc1123_date(LastModified),
+ {ok, Req4} = cowboy_http_req:set_resp_header(
+ <<"Last-Modified">>, LastModifiedStr, Req3)
+ end,
+ {Req5, State4} = set_resp_expires(Req4, State3),
+ case call(Req5, State4, Fun) of
+ {Body, Req6, HandlerState} ->
+ State5 = State4#state{handler_state=HandlerState},
+ {ok, Req7} = cowboy_http_req:set_resp_body(Body, Req6),
+ multiple_choices(Req7, State5)
+ end;
+set_resp_body(Req, State) ->
+ multiple_choices(Req, State).
+
+multiple_choices(Req, State) ->
+ expect(Req, State, multiple_choices, false, 200, 300).
+
+%% Response utility functions.
+
+set_resp_etag(Req, State) ->
+ {Etag, Req2, State2} = generate_etag(Req, State),
+ case Etag of
+ undefined ->
+ {Req2, State2};
+ Etag ->
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Etag">>, Etag, Req2),
+ {Req3, State2}
+ end.
+
+set_resp_expires(Req, State) ->
+ {Expires, Req2, State2} = expires(Req, State),
+ case Expires of
+ Expires when is_atom(Expires) ->
+ {Req2, State2};
+ Expires ->
+ ExpiresStr = httpd_util:rfc1123_date(Expires),
+ {ok, Req3} = cowboy_http_req:set_resp_header(
+ <<"Expires">>, ExpiresStr, Req2),
+ {Req3, State2}
+ end.
+
+%% Info retrieval. No logic.
+
+generate_etag(Req, State=#state{etag=no_call}) ->
+ {undefined, Req, State};
+generate_etag(Req, State=#state{etag=undefined}) ->
+ case call(Req, State, generate_etag) of
+ no_call ->
+ {undefined, Req, State#state{etag=no_call}};
+ {Etag, Req2, HandlerState2} ->
+ {Etag, Req2, State#state{handler_state=HandlerState2, etag=Etag}}
+ end;
+generate_etag(Req, State=#state{etag=Etag}) ->
+ {Etag, Req, State}.
+
+last_modified(Req, State=#state{last_modified=no_call}) ->
+ {undefined, Req, State};
+last_modified(Req, State=#state{last_modified=undefined}) ->
+ case call(Req, State, last_modified) of
+ no_call ->
+ {undefined, Req, State#state{last_modified=no_call}};
+ {LastModified, Req2, HandlerState2} ->
+ {LastModified, Req2, State#state{handler_state=HandlerState2,
+ last_modified=LastModified}}
+ end;
+last_modified(Req, State=#state{last_modified=LastModified}) ->
+ {LastModified, Req, State}.
+
+expires(Req, State=#state{expires=no_call}) ->
+ {undefined, Req, State};
+expires(Req, State=#state{expires=undefined}) ->
+ case call(Req, State, expires) of
+ no_call ->
+ {undefined, Req, State#state{expires=no_call}};
+ {Expires, Req2, HandlerState2} ->
+ {Expires, Req2, State#state{handler_state=HandlerState2,
+ expires=Expires}}
+ end;
+expires(Req, State=#state{expires=Expires}) ->
+ {Expires, Req, State}.
+
+%% REST primitives.
+
+expect(Req, State, Callback, Expected, OnTrue, OnFalse) ->
+ case call(Req, State, Callback) of
+ no_call ->
+ next(Req, State, OnTrue);
+ {Expected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnTrue);
+ {_Unexpected, Req2, HandlerState2} ->
+ next(Req2, State#state{handler_state=HandlerState2}, OnFalse)
+ end.
+
+call(Req, #state{handler=Handler, handler_state=HandlerState}, Fun) ->
+ case erlang:function_exported(Handler, Fun, 2) of
+ true -> Handler:Fun(Req, HandlerState);
+ false -> no_call
+ end.
+
+next(Req, State, Next) when is_function(Next) ->
+ Next(Req, State);
+next(Req, State, StatusCode) when is_integer(StatusCode) ->
+ respond(Req, State, StatusCode).
+
+%% @todo Allow some sort of callback for custom error pages.
+respond(Req, State, StatusCode) ->
+ {ok, Req2} = cowboy_http_req:reply(StatusCode, Req),
+ terminate(Req2, State).
+
+terminate(Req, #state{handler=Handler, handler_state=HandlerState}) ->
+ case erlang:function_exported(Handler, rest_terminate, 2) of
+ true -> ok = Handler:rest_terminate(Req, HandlerState);
+ false -> ok
+ end.
diff --git a/test/http_SUITE.erl b/test/http_SUITE.erl
index 5f02a7c..eb0bf54 100644
--- a/test/http_SUITE.erl
+++ b/test/http_SUITE.erl
@@ -25,11 +25,12 @@
set_resp_overwrite/1, set_resp_body/1]). %% http.
-export([http_200/1, http_404/1]). %% http and https.
-export([http_10_hostless/1]). %% misc.
+-export([rest_simple/1]). %% rest.
%% ct.
all() ->
- [{group, http}, {group, https}, {group, misc}].
+ [{group, http}, {group, https}, {group, misc}, {group, rest}].
groups() ->
BaseTests = [http_200, http_404],
@@ -38,7 +39,9 @@ groups() ->
ws0, ws8, ws8_single_bytes, ws8_init_shutdown, ws13,
ws_timeout_hibernate, set_resp_header,
set_resp_overwrite, set_resp_body] ++ BaseTests},
- {https, [], BaseTests}, {misc, [], [http_10_hostless]}].
+ {https, [], BaseTests},
+ {misc, [], [http_10_hostless]},
+ {rest, [], [rest_simple]}].
init_per_suite(Config) ->
application:start(inets),
@@ -77,6 +80,14 @@ init_per_group(misc, Config) ->
cowboy_http_protocol, [{dispatch, [{'_', [
{[], http_handler, []}
]}]}]),
+ [{port, Port}|Config];
+init_per_group(rest, Config) ->
+ Port = 33083,
+ cowboy:start_listener(reset, 100,
+ cowboy_tcp_transport, [{port, Port}],
+ cowboy_http_protocol, [{dispatch, [{'_', [
+ {[<<"simple">>], rest_simple_resource, []}
+ ]}]}]),
[{port, Port}|Config].
end_per_group(https, _Config) ->
@@ -535,3 +546,9 @@ http_404(Config) ->
http_10_hostless(Config) ->
Packet = "GET / HTTP/1.0\r\n\r\n",
{Packet, 200} = raw_req(Packet, Config).
+
+%% rest.
+
+rest_simple(Config) ->
+ Packet = "GET /simple HTTP/1.1\r\nHost: localhost\r\n\r\n",
+ {Packet, 200} = raw_req(Packet, Config).
diff --git a/test/rest_simple_resource.erl b/test/rest_simple_resource.erl
new file mode 100644
index 0000000..e2c573c
--- /dev/null
+++ b/test/rest_simple_resource.erl
@@ -0,0 +1,12 @@
+-module(rest_simple_resource).
+-export([init/3, content_types_provided/2, get_text_plain/2]).
+
+init(_Transport, _Req, _Opts) ->
+ {upgrade, protocol, cowboy_http_rest}.
+
+content_types_provided(Req, State) ->
+ {[{{<<"text">>, <<"plain">>, []}, get_text_plain}], Req, State}.
+
+get_text_plain(Req, State) ->
+ {<<"This is REST!">>, Req, State}.
+