Skip to content

Commit

Permalink
Implement path_info feature
Browse files Browse the repository at this point in the history
The dispatcher now accepts '...' as the leading segment of Host and the
trailing segment of Path, this special atom matches any remaining path tail.

When given "cowboy.bugs.dev-extend.eu", host rule ['...', <<"dev-extend">>,
<<"eu">>] matches and fills host_info with [<<"cowboy">>, <<"bugs">>].

When given "/a/b/c/d", path rule [<<"a">>, <<"b">>, '...'] matches and fills
path_info with [<<"c">>, <<"d">>].
  • Loading branch information
nox committed May 9, 2011
1 parent 094f5c1 commit 0ca8f13
Show file tree
Hide file tree
Showing 4 changed files with 82 additions and 27 deletions.
3 changes: 3 additions & 0 deletions include/http.hrl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
Expand Down Expand Up @@ -49,9 +50,11 @@
version = {1, 1} :: http_version(),
peer = undefined :: undefined | {Address::ip_address(), Port::ip_port()},
host = undefined :: undefined | cowboy_dispatcher:path_tokens(),
host_info = undefined :: undefined | cowboy_dispatcher:path_tokens(),
raw_host = undefined :: undefined | binary(),
port = undefined :: undefined | ip_port(),
path = undefined :: undefined | '*' | cowboy_dispatcher:path_tokens(),
path_info = undefined :: undefined | cowboy_dispatcher:path_tokens(),
raw_path = undefined :: undefined | binary(),
qs_vals = undefined :: undefined
| list({Name::binary(), Value::binary() | true}),
Expand Down
84 changes: 61 additions & 23 deletions src/cowboy_dispatcher.erl
Original file line number Diff line number Diff line change
Expand Up @@ -62,51 +62,59 @@ do_split_path(RawPath, Separator) ->

-spec match(Host::path_tokens(), Path::path_tokens(),
Dispatch::dispatch_rules())
-> {ok, Handler::module(), Opts::term(), Binds::bindings()}
-> {ok, Handler::module(), Opts::term(), Binds::bindings(),
HostInfo::undefined | path_tokens(),
PathInfo::undefined | path_tokens()}
| {error, notfound, host} | {error, notfound, path}.
match(_Host, _Path, []) ->
{error, notfound, host};
match(_Host, Path, [{'_', PathMatchs}|_Tail]) ->
match_path(Path, PathMatchs, []);
match_path(Path, PathMatchs, [], undefined);
match(Host, Path, [{HostMatch, PathMatchs}|Tail]) ->
case try_match(host, Host, HostMatch) of
false ->
match(Host, Path, Tail);
{true, HostBinds} ->
match_path(Path, PathMatchs, HostBinds)
{true, HostBinds, undefined} ->
match_path(Path, PathMatchs, HostBinds, undefined);
{true, HostBinds, HostInfo} ->
match_path(Path, PathMatchs, HostBinds, lists:reverse(HostInfo))
end.

-spec match_path(Path::path_tokens(), list({Path::match_rule(),
Handler::module(), Opts::term()}), HostBinds::bindings())
-> {ok, Handler::module(), Opts::term(), Binds::bindings()}
Handler::module(), Opts::term()}), HostBinds::bindings(),
HostInfo::undefined | path_tokens())
-> {ok, Handler::module(), Opts::term(), Binds::bindings(),
HostInfo::undefined | path_tokens(),
PathInfo::undefined | path_tokens()}
| {error, notfound, path}.
match_path(_Path, [], _HostBinds) ->
match_path(_Path, [], _HostBinds, _HostInfo) ->
{error, notfound, path};
match_path(_Path, [{'_', Handler, Opts}|_Tail], HostBinds) ->
{ok, Handler, Opts, HostBinds};
match_path('*', [{'*', Handler, Opts}|_Tail], HostBinds) ->
{ok, Handler, Opts, HostBinds};
match_path(Path, [{PathMatch, Handler, Opts}|Tail], HostBinds) ->
match_path(_Path, [{'_', Handler, Opts}|_Tail], HostBinds, HostInfo) ->
{ok, Handler, Opts, HostBinds, HostInfo, undefined};
match_path('*', [{'*', Handler, Opts}|_Tail], HostBinds, HostInfo) ->
{ok, Handler, Opts, HostBinds, HostInfo, undefined};
match_path(Path, [{PathMatch, Handler, Opts}|Tail], HostBinds, HostInfo) ->
case try_match(path, Path, PathMatch) of
false ->
match_path(Path, Tail, HostBinds);
{true, PathBinds} ->
{ok, Handler, Opts, HostBinds ++ PathBinds}
match_path(Path, Tail, HostBinds, HostInfo);
{true, PathBinds, PathInfo} ->
{ok, Handler, Opts, HostBinds ++ PathBinds, HostInfo, PathInfo}
end.

%% Internal.

-spec try_match(Type::host | path, List::path_tokens(), Match::match_rule())
-> {true, Binds::bindings()} | false.
try_match(_Type, List, Match) when length(List) =/= length(Match) ->
false;
-> {true, Binds::bindings(), ListInfo::undefined | path_tokens()} | false.
try_match(host, List, Match) ->
list_match(lists:reverse(List), lists:reverse(Match), []);
try_match(path, List, Match) ->
list_match(List, Match, []).

-spec list_match(List::path_tokens(), Match::match_rule(), Binds::bindings())
-> {true, Binds::bindings()} | false.
-> {true, Binds::bindings(), ListInfo::undefined | path_tokens()} | false.
%% Atom '...' matches any trailing path, stop right now.
list_match(List, ['...'], Binds) ->
{true, Binds, List};
%% Atom '_' matches anything, continue.
list_match([_E|Tail], ['_'|TailMatch], Binds) ->
list_match(Tail, TailMatch, Binds);
Expand All @@ -116,12 +124,12 @@ list_match([E|Tail], [E|TailMatch], Binds) ->
%% Bind E to the variable name V and continue.
list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
list_match(Tail, TailMatch, [{V, E}|Binds]);
%% Values don't match, stop.
list_match([_E|_Tail], [_F|_TailMatch], _Binds) ->
false;
%% Match complete.
list_match([], [], Binds) ->
{true, Binds}.
{true, Binds, undefined};
%% Values don't match, stop.
list_match(_List, _Match, _Binds) ->
false.

%% Tests.

Expand Down Expand Up @@ -228,6 +236,36 @@ match_test_() ->
{ok, match_duplicate_vars, [we, {expect, two}, var, here],
[{var, <<"fr">>}, {var, <<"987">>}]}}
],
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
{ok, Handler, Opts, Binds, undefined, undefined} = match(H, P, Dispatch)
end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].

match_info_test_() ->
Dispatch = [
{[<<"www">>, <<"dev-extend">>, <<"eu">>], [
{[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], match_path, []}
]},
{['...', <<"dev-extend">>, <<"eu">>], [
{'_', match_any, []}
]}
],
Tests = [
{[<<"dev-extend">>, <<"eu">>], [],
{ok, match_any, [], [], [], undefined}},
{[<<"bugs">>, <<"dev-extend">>, <<"eu">>], [],
{ok, match_any, [], [], [<<"bugs">>], undefined}},
{[<<"cowboy">>, <<"bugs">>, <<"dev-extend">>, <<"eu">>], [],
{ok, match_any, [], [], [<<"cowboy">>, <<"bugs">>], undefined}},
{[<<"www">>, <<"dev-extend">>, <<"eu">>],
[<<"pathinfo">>, <<"is">>, <<"next">>],
{ok, match_path, [], [], undefined, []}},
{[<<"www">>, <<"dev-extend">>, <<"eu">>],
[<<"pathinfo">>, <<"is">>, <<"next">>, <<"path_info">>],
{ok, match_path, [], [], undefined, [<<"path_info">>]}},
{[<<"www">>, <<"dev-extend">>, <<"eu">>],
[<<"pathinfo">>, <<"is">>, <<"next">>, <<"foo">>, <<"bar">>],
{ok, match_path, [], [], undefined, [<<"foo">>, <<"bar">>]}}
],
[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
R = match(H, P, Dispatch)
end} || {H, P, R} <- Tests].
Expand Down
6 changes: 4 additions & 2 deletions src/cowboy_http_protocol.erl
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
%% Copyright (c) 2011, Loïc Hoguin <essen@dev-extend.eu>
%% Copyright (c) 2011, Anthony Ramine <nox@dev-extend.eu>
%%
%% Permission to use, copy, modify, and/or distribute this software for any
%% purpose with or without fee is hereby granted, provided that the above
Expand Down Expand Up @@ -158,8 +159,9 @@ dispatch(Req=#http_req{host=Host, path=Path},
%% @todo We probably want to filter the Host and Path here to allow
%% things like url rewriting.
case cowboy_dispatcher:match(Host, Path, Dispatch) of
{ok, Handler, Opts, Binds} ->
parse_header(Req#http_req{bindings=Binds},
{ok, Handler, Opts, Binds, HostInfo, PathInfo} ->
parse_header(Req#http_req{host_info=HostInfo, path_info=PathInfo,
bindings=Binds},
State#state{handler={Handler, Opts}});
{error, notfound, host} ->
error_terminate(400, State);
Expand Down
16 changes: 14 additions & 2 deletions src/cowboy_http_req.erl
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@

-export([
method/1, version/1, peer/1,
host/1, raw_host/1, port/1,
path/1, raw_path/1,
host/1, host_info/1, raw_host/1, port/1,
path/1, path_info/1, raw_path/1,
qs_val/2, qs_val/3, qs_vals/1, raw_qs/1,
binding/2, binding/3, bindings/1,
header/2, header/3, headers/1
Expand Down Expand Up @@ -59,6 +59,12 @@ peer(Req) ->
host(Req) ->
{Req#http_req.host, Req}.

-spec host_info(Req::#http_req{})
-> {HostInfo::cowboy_dispatcher:path_tokens() | undefined,
Req::#http_req{}}.
host_info(Req) ->
{Req#http_req.host_info, Req}.

-spec raw_host(Req::#http_req{}) -> {RawHost::binary(), Req::#http_req{}}.
raw_host(Req) ->
{Req#http_req.raw_host, Req}.
Expand All @@ -72,6 +78,12 @@ port(Req) ->
path(Req) ->
{Req#http_req.path, Req}.

-spec path_info(Req::#http_req{})
-> {PathInfo::cowboy_dispatcher:path_tokens() | undefined,
Req::#http_req{}}.
path_info(Req) ->
{Req#http_req.path_info, Req}.

-spec raw_path(Req::#http_req{}) -> {RawPath::binary(), Req::#http_req{}}.
raw_path(Req) ->
{Req#http_req.raw_path, Req}.
Expand Down

0 comments on commit 0ca8f13

Please sign in to comment.