From 0ca8f1364b857632030dbc3735ce789686361073 Mon Sep 17 00:00:00 2001
From: Anthony Ramine <nox@dev-extend.eu>
Date: Mon, 9 May 2011 14:31:06 +0200
Subject: Implement path_info feature

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">>].
---
 src/cowboy_dispatcher.erl    | 84 ++++++++++++++++++++++++++++++++------------
 src/cowboy_http_protocol.erl |  6 ++--
 src/cowboy_http_req.erl      | 16 +++++++--
 3 files changed, 79 insertions(+), 27 deletions(-)

(limited to 'src')

diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl
index c006185..67a7bf6 100644
--- a/src/cowboy_dispatcher.erl
+++ b/src/cowboy_dispatcher.erl
@@ -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);
@@ -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.
 
@@ -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].
diff --git a/src/cowboy_http_protocol.erl b/src/cowboy_http_protocol.erl
index 302df26..ed4d963 100644
--- a/src/cowboy_http_protocol.erl
+++ b/src/cowboy_http_protocol.erl
@@ -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
@@ -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);
diff --git a/src/cowboy_http_req.erl b/src/cowboy_http_req.erl
index 60f0b55..fad5aaf 100644
--- a/src/cowboy_http_req.erl
+++ b/src/cowboy_http_req.erl
@@ -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
@@ -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}.
@@ -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}.
-- 
cgit v1.2.3