From 25259671f51c076720b64959a700263eaa0937b2 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= <essen@ninenines.eu>
Date: Tue, 23 Sep 2014 21:36:52 +0300
Subject: Make routing constraints use the fields format

This makes routing more in line with the rest of Cowboy and
allows us to use cowboy_constraints directly.
---
 doc/src/guide/routing.ezdoc        | 26 +++++-----------
 doc/src/manual/cowboy_router.ezdoc | 19 ++----------
 src/cowboy_router.erl              | 61 ++++++++++++++++----------------------
 3 files changed, 36 insertions(+), 70 deletions(-)

diff --git a/doc/src/guide/routing.ezdoc b/doc/src/guide/routing.ezdoc
index e7b43f2..2482c12 100644
--- a/doc/src/guide/routing.ezdoc
+++ b/doc/src/guide/routing.ezdoc
@@ -191,26 +191,16 @@ HostMatch = "*".
 After the matching has completed, the resulting bindings can be tested
 against a set of constraints. Constraints are only tested when the
 binding is defined. They run in the order you defined them. The match
-will succeed only if they all succeed.
+will succeed only if they all succeed. If the match fails, then Cowboy
+tries the next route in the list.
 
-They are always given as a two or three elements tuple, where the first
-element is the name of the binding, the second element is the constraint's
-name, and the optional third element is the constraint's arguments.
+The format used for constraints is the same as match functions in
+`cowboy_req`: they are provided as a list of fields which may have
+one or more constraints. While the router accepts the same format,
+it will skip fields with no constraints and will also ignore default
+values, if any.
 
-The following constraints are currently defined:
-
-* {Name, int}
-* {Name, function, fun ((Value) -> true | {true, NewValue} | false)}
-
-The `int` constraint will check if the binding is a binary string
-representing an integer, and if it is, will convert the value to integer.
-
-The `function` constraint will pass the binding value to a user specified
-function that receives the binary value as its only argument and must
-return whether it fulfills the constraint, optionally modifying the value.
-The value thus returned can be of any type.
-
-Note that constraint functions SHOULD be pure and MUST NOT crash.
+Read more about ^constraints^.
 
 :: Compilation
 
diff --git a/doc/src/manual/cowboy_router.ezdoc b/doc/src/manual/cowboy_router.ezdoc
index f76acf6..8d45e67 100644
--- a/doc/src/manual/cowboy_router.ezdoc
+++ b/doc/src/manual/cowboy_router.ezdoc
@@ -22,31 +22,16 @@ Environment output:
 
 List of bindings found during routing.
 
-: constraints() = [IntConstraint | FunConstraint]
-
-Types:
-
-* IntConstraint = {atom(), int}
-* FunConstraint = {atom(), function, Fun}
-* Fun = fun((binary()) -> true | {true, any()} | false)
-
-List of constraints to apply to the bindings.
-
-The int constraint will convert the binding to an integer.
-The fun constraint allows writing custom code for checking
-the bindings. Returning a new value from that fun allows
-replacing the current binding with a new value.
-
 : dispatch_rules() - opaque to the user
 
 Rules for dispatching request used by Cowboy.
 
-: routes() = [{Host, Paths} | {Host, constraints(), Paths}]
+: routes() = [{Host, Paths} | {Host, cowboy:fields(), Paths}]
 
 Types:
 
 * Host = Path = '_' | iodata()
-* Paths = [{Path, Handler, Opts} | {Path, constraints(), Handler, Opts}]
+* Paths = [{Path, Handler, Opts} | {Path, cowboy:fields(), Handler, Opts}]
 * Handler = module()
 * Opts = any()
 
diff --git a/src/cowboy_router.erl b/src/cowboy_router.erl
index f5b2f26..3b71205 100644
--- a/src/cowboy_router.erl
+++ b/src/cowboy_router.erl
@@ -33,21 +33,17 @@
 -export_type([bindings/0]).
 -export_type([tokens/0]).
 
--type constraints() :: [{atom(), int}
-	| {atom(), function, fun ((binary()) -> true | {true, any()} | false)}].
--export_type([constraints/0]).
-
 -type route_match() :: '_' | iodata().
 -type route_path() :: {Path::route_match(), Handler::module(), Opts::any()}
-	| {Path::route_match(), constraints(), Handler::module(), Opts::any()}.
+	| {Path::route_match(), cowboy:fields(), Handler::module(), Opts::any()}.
 -type route_rule() :: {Host::route_match(), Paths::[route_path()]}
-	| {Host::route_match(), constraints(), Paths::[route_path()]}.
+	| {Host::route_match(), cowboy:fields(), Paths::[route_path()]}.
 -type routes() :: [route_rule()].
 -export_type([routes/0]).
 
 -type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
--type dispatch_path() :: {dispatch_match(), module(), any()}.
--type dispatch_rule() :: {Host::dispatch_match(), Paths::[dispatch_path()]}.
+-type dispatch_path() :: {dispatch_match(), cowboy:fields(), module(), any()}.
+-type dispatch_rule() :: {Host::dispatch_match(), cowboy:fields(), Paths::[dispatch_path()]}.
 -opaque dispatch_rules() :: [dispatch_rule()].
 -export_type([dispatch_rules/0]).
 
@@ -59,15 +55,15 @@ compile([], Acc) ->
 	lists:reverse(Acc);
 compile([{Host, Paths}|Tail], Acc) ->
 	compile([{Host, [], Paths}|Tail], Acc);
-compile([{HostMatch, Constraints, Paths}|Tail], Acc) ->
+compile([{HostMatch, Fields, Paths}|Tail], Acc) ->
 	HostRules = case HostMatch of
 		'_' -> '_';
 		_ -> compile_host(HostMatch)
 	end,
 	PathRules = compile_paths(Paths, []),
 	Hosts = case HostRules of
-		'_' -> [{'_', Constraints, PathRules}];
-		_ -> [{R, Constraints, PathRules} || R <- HostRules]
+		'_' -> [{'_', Fields, PathRules}];
+		_ -> [{R, Fields, PathRules} || R <- HostRules]
 	end,
 	compile(Tail, Hosts ++ Acc).
 
@@ -80,16 +76,16 @@ compile_paths([], Acc) ->
 	lists:reverse(Acc);
 compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) ->
 	compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc);
-compile_paths([{PathMatch, Constraints, Handler, Opts}|Tail], Acc)
+compile_paths([{PathMatch, Fields, Handler, Opts}|Tail], Acc)
 		when is_list(PathMatch) ->
 	compile_paths([{iolist_to_binary(PathMatch),
-		Constraints, Handler, Opts}|Tail], Acc);
-compile_paths([{'_', Constraints, Handler, Opts}|Tail], Acc) ->
-	compile_paths(Tail, [{'_', Constraints, Handler, Opts}] ++ Acc);
-compile_paths([{<< $/, PathMatch/binary >>, Constraints, Handler, Opts}|Tail],
+		Fields, Handler, Opts}|Tail], Acc);
+compile_paths([{'_', Fields, Handler, Opts}|Tail], Acc) ->
+	compile_paths(Tail, [{'_', Fields, Handler, Opts}] ++ Acc);
+compile_paths([{<< $/, PathMatch/binary >>, Fields, Handler, Opts}|Tail],
 		Acc) ->
 	PathRules = compile_rules(PathMatch, $/, [], [], <<>>),
-	Paths = [{lists:reverse(R), Constraints, Handler, Opts} || R <- PathRules],
+	Paths = [{lists:reverse(R), Fields, Handler, Opts} || R <- PathRules],
 	compile_paths(Tail, Paths ++ Acc);
 compile_paths([{PathMatch, _, _, _}|_], _) ->
 	error({badarg, "The following route MUST begin with a slash: "
@@ -220,7 +216,7 @@ match([], _, _) ->
 %% If the host is '_' then there can be no constraints.
 match([{'_', [], PathMatchs}|_Tail], _, Path) ->
 	match_path(PathMatchs, undefined, Path, []);
-match([{HostMatch, Constraints, PathMatchs}|Tail], Tokens, Path)
+match([{HostMatch, Fields, PathMatchs}|Tail], Tokens, Path)
 		when is_list(Tokens) ->
 	case list_match(Tokens, HostMatch, []) of
 		false ->
@@ -230,7 +226,7 @@ match([{HostMatch, Constraints, PathMatchs}|Tail], Tokens, Path)
 				undefined -> undefined;
 				_ -> lists:reverse(HostInfo)
 			end,
-			case check_constraints(Constraints, Bindings) of
+			case check_constraints(Fields, Bindings) of
 				{ok, Bindings2} ->
 					match_path(PathMatchs, HostInfo2, Path, Bindings2);
 				nomatch ->
@@ -251,15 +247,15 @@ match_path([], _, _, _) ->
 %% If the path is '_' then there can be no constraints.
 match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
 	{ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{<<"*">>, _Constraints, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
+match_path([{<<"*">>, _, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
 	{ok, Handler, Opts, Bindings, HostInfo, undefined};
-match_path([{PathMatch, Constraints, Handler, Opts}|Tail], HostInfo, Tokens,
+match_path([{PathMatch, Fields, Handler, Opts}|Tail], HostInfo, Tokens,
 		Bindings) when is_list(Tokens) ->
 	case list_match(Tokens, PathMatch, Bindings) of
 		false ->
 			match_path(Tail, HostInfo, Tokens, Bindings);
 		{true, PathBinds, PathInfo} ->
-			case check_constraints(Constraints, PathBinds) of
+			case check_constraints(Fields, PathBinds) of
 				{ok, PathBinds2} ->
 					{ok, Handler, Opts, PathBinds2, HostInfo, PathInfo};
 				nomatch ->
@@ -273,13 +269,16 @@ match_path(Dispatch, HostInfo, Path, Bindings) ->
 
 check_constraints([], Bindings) ->
 	{ok, Bindings};
-check_constraints([Constraint|Tail], Bindings) ->
-	Name = element(1, Constraint),
+check_constraints([Field|Tail], Bindings) when is_atom(Field) ->
+	check_constraints(Tail, Bindings);
+check_constraints([Field|Tail], Bindings) ->
+	Name = element(1, Field),
 	case lists:keyfind(Name, 1, Bindings) of
 		false ->
 			check_constraints(Tail, Bindings);
 		{_, Value} ->
-			case check_constraint(Constraint, Value) of
+			Constraints = element(2, Field),
+			case cowboy_constraints:validate(Value, Constraints) of
 				true ->
 					check_constraints(Tail, Bindings);
 				{true, Value2} ->
@@ -291,13 +290,6 @@ check_constraints([Constraint|Tail], Bindings) ->
 			end
 	end.
 
-check_constraint({_, int}, Value) ->
-	try {true, list_to_integer(binary_to_list(Value))}
-	catch _:_ -> false
-	end;
-check_constraint({_, function, Fun}, Value) ->
-	Fun(Value).
-
 -spec split_host(binary()) -> tokens().
 split_host(Host) ->
 	split_host(Host, []).
@@ -540,9 +532,8 @@ match_constraints_test() ->
 		<<"ninenines.eu">>, <<"/path/123/">>),
 	{error, notfound, path} = match(Dispatch,
 		<<"ninenines.eu">>, <<"/path/NaN/">>),
-	Dispatch2 = [{'_', [],
-		[{[<<"path">>, username], [{username, function,
-		fun(Value) -> Value =:= cowboy_bstr:to_lower(Value) end}],
+	Dispatch2 = [{'_', [], [{[<<"path">>, username],
+		[{username, fun(Value) -> Value =:= cowboy_bstr:to_lower(Value) end}],
 		match, []}]}],
 	{ok, _, [], [{username, <<"essen">>}], _, _} = match(Dispatch2,
 		<<"ninenines.eu">>, <<"/path/essen">>),
-- 
cgit v1.2.3