zf

zenflows testing
git clone https://s.sonu.ch/~srfsh/zf.git
Log | Files | Refs | Submodules | README | LICENSE

cowboy_router.erl (23154B)


      1 %% Copyright (c) 2011-2017, Loïc Hoguin <essen@ninenines.eu>
      2 %%
      3 %% Permission to use, copy, modify, and/or distribute this software for any
      4 %% purpose with or without fee is hereby granted, provided that the above
      5 %% copyright notice and this permission notice appear in all copies.
      6 %%
      7 %% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
      8 %% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
      9 %% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
     10 %% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
     11 %% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
     12 %% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
     13 %% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
     14 
     15 %% Routing middleware.
     16 %%
     17 %% Resolve the handler to be used for the request based on the
     18 %% routing information found in the <em>dispatch</em> environment value.
     19 %% When found, the handler module and associated data are added to
     20 %% the environment as the <em>handler</em> and <em>handler_opts</em> values
     21 %% respectively.
     22 %%
     23 %% If the route cannot be found, processing stops with either
     24 %% a 400 or a 404 reply.
     25 -module(cowboy_router).
     26 -behaviour(cowboy_middleware).
     27 
     28 -export([compile/1]).
     29 -export([execute/2]).
     30 
     31 -type bindings() :: #{atom() => any()}.
     32 -type tokens() :: [binary()].
     33 -export_type([bindings/0]).
     34 -export_type([tokens/0]).
     35 
     36 -type route_match() :: '_' | iodata().
     37 -type route_path() :: {Path::route_match(), Handler::module(), Opts::any()}
     38 	| {Path::route_match(), cowboy:fields(), Handler::module(), Opts::any()}.
     39 -type route_rule() :: {Host::route_match(), Paths::[route_path()]}
     40 	| {Host::route_match(), cowboy:fields(), Paths::[route_path()]}.
     41 -type routes() :: [route_rule()].
     42 -export_type([routes/0]).
     43 
     44 -type dispatch_match() :: '_' | <<_:8>> | [binary() | '_' | '...' | atom()].
     45 -type dispatch_path() :: {dispatch_match(), cowboy:fields(), module(), any()}.
     46 -type dispatch_rule() :: {Host::dispatch_match(), cowboy:fields(), Paths::[dispatch_path()]}.
     47 -opaque dispatch_rules() :: [dispatch_rule()].
     48 -export_type([dispatch_rules/0]).
     49 
     50 -spec compile(routes()) -> dispatch_rules().
     51 compile(Routes) ->
     52 	compile(Routes, []).
     53 
     54 compile([], Acc) ->
     55 	lists:reverse(Acc);
     56 compile([{Host, Paths}|Tail], Acc) ->
     57 	compile([{Host, [], Paths}|Tail], Acc);
     58 compile([{HostMatch, Fields, Paths}|Tail], Acc) ->
     59 	HostRules = case HostMatch of
     60 		'_' -> '_';
     61 		_ -> compile_host(HostMatch)
     62 	end,
     63 	PathRules = compile_paths(Paths, []),
     64 	Hosts = case HostRules of
     65 		'_' -> [{'_', Fields, PathRules}];
     66 		_ -> [{R, Fields, PathRules} || R <- HostRules]
     67 	end,
     68 	compile(Tail, Hosts ++ Acc).
     69 
     70 compile_host(HostMatch) when is_list(HostMatch) ->
     71 	compile_host(list_to_binary(HostMatch));
     72 compile_host(HostMatch) when is_binary(HostMatch) ->
     73 	compile_rules(HostMatch, $., [], [], <<>>).
     74 
     75 compile_paths([], Acc) ->
     76 	lists:reverse(Acc);
     77 compile_paths([{PathMatch, Handler, Opts}|Tail], Acc) ->
     78 	compile_paths([{PathMatch, [], Handler, Opts}|Tail], Acc);
     79 compile_paths([{PathMatch, Fields, Handler, Opts}|Tail], Acc)
     80 		when is_list(PathMatch) ->
     81 	compile_paths([{iolist_to_binary(PathMatch),
     82 		Fields, Handler, Opts}|Tail], Acc);
     83 compile_paths([{'_', Fields, Handler, Opts}|Tail], Acc) ->
     84 	compile_paths(Tail, [{'_', Fields, Handler, Opts}] ++ Acc);
     85 compile_paths([{<<"*">>, Fields, Handler, Opts}|Tail], Acc) ->
     86 	compile_paths(Tail, [{<<"*">>, Fields, Handler, Opts}|Acc]);
     87 compile_paths([{<< $/, PathMatch/bits >>, Fields, Handler, Opts}|Tail],
     88 		Acc) ->
     89 	PathRules = compile_rules(PathMatch, $/, [], [], <<>>),
     90 	Paths = [{lists:reverse(R), Fields, Handler, Opts} || R <- PathRules],
     91 	compile_paths(Tail, Paths ++ Acc);
     92 compile_paths([{PathMatch, _, _, _}|_], _) ->
     93 	error({badarg, "The following route MUST begin with a slash: "
     94 		++ binary_to_list(PathMatch)}).
     95 
     96 compile_rules(<<>>, _, Segments, Rules, <<>>) ->
     97 	[Segments|Rules];
     98 compile_rules(<<>>, _, Segments, Rules, Acc) ->
     99 	[[Acc|Segments]|Rules];
    100 compile_rules(<< S, Rest/bits >>, S, Segments, Rules, <<>>) ->
    101 	compile_rules(Rest, S, Segments, Rules, <<>>);
    102 compile_rules(<< S, Rest/bits >>, S, Segments, Rules, Acc) ->
    103 	compile_rules(Rest, S, [Acc|Segments], Rules, <<>>);
    104 %% Colon on path segment start is special, otherwise allow.
    105 compile_rules(<< $:, Rest/bits >>, S, Segments, Rules, <<>>) ->
    106 	{NameBin, Rest2} = compile_binding(Rest, S, <<>>),
    107 	Name = binary_to_atom(NameBin, utf8),
    108 	compile_rules(Rest2, S, Segments, Rules, Name);
    109 compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc)
    110 		when Acc =:= <<>> ->
    111 	compile_rules(Rest, S, ['...'|Segments], Rules, Acc);
    112 compile_rules(<< $[, $., $., $., $], Rest/bits >>, S, Segments, Rules, Acc) ->
    113 	compile_rules(Rest, S, ['...', Acc|Segments], Rules, Acc);
    114 compile_rules(<< $[, S, Rest/bits >>, S, Segments, Rules, Acc) ->
    115 	compile_brackets(Rest, S, [Acc|Segments], Rules);
    116 compile_rules(<< $[, Rest/bits >>, S, Segments, Rules, <<>>) ->
    117 	compile_brackets(Rest, S, Segments, Rules);
    118 %% Open bracket in the middle of a segment.
    119 compile_rules(<< $[, _/bits >>, _, _, _, _) ->
    120 	error(badarg);
    121 %% Missing an open bracket.
    122 compile_rules(<< $], _/bits >>, _, _, _, _) ->
    123 	error(badarg);
    124 compile_rules(<< C, Rest/bits >>, S, Segments, Rules, Acc) ->
    125 	compile_rules(Rest, S, Segments, Rules, << Acc/binary, C >>).
    126 
    127 %% Everything past $: until the segment separator ($. for hosts,
    128 %% $/ for paths) or $[ or $] or end of binary is the binding name.
    129 compile_binding(<<>>, _, <<>>) ->
    130 	error(badarg);
    131 compile_binding(Rest = <<>>, _, Acc) ->
    132 	{Acc, Rest};
    133 compile_binding(Rest = << C, _/bits >>, S, Acc)
    134 		when C =:= S; C =:= $[; C =:= $] ->
    135 	{Acc, Rest};
    136 compile_binding(<< C, Rest/bits >>, S, Acc) ->
    137 	compile_binding(Rest, S, << Acc/binary, C >>).
    138 
    139 compile_brackets(Rest, S, Segments, Rules) ->
    140 	{Bracket, Rest2} = compile_brackets_split(Rest, <<>>, 0),
    141 	Rules1 = compile_rules(Rest2, S, Segments, [], <<>>),
    142 	Rules2 = compile_rules(<< Bracket/binary, Rest2/binary >>,
    143 		S, Segments, [], <<>>),
    144 	Rules ++ Rules2 ++ Rules1.
    145 
    146 %% Missing a close bracket.
    147 compile_brackets_split(<<>>, _, _) ->
    148 	error(badarg);
    149 %% Make sure we don't confuse the closing bracket we're looking for.
    150 compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $[ ->
    151 	compile_brackets_split(Rest, << Acc/binary, C >>, N + 1);
    152 compile_brackets_split(<< C, Rest/bits >>, Acc, N) when C =:= $], N > 0 ->
    153 	compile_brackets_split(Rest, << Acc/binary, C >>, N - 1);
    154 %% That's the right one.
    155 compile_brackets_split(<< $], Rest/bits >>, Acc, 0) ->
    156 	{Acc, Rest};
    157 compile_brackets_split(<< C, Rest/bits >>, Acc, N) ->
    158 	compile_brackets_split(Rest, << Acc/binary, C >>, N).
    159 
    160 -spec execute(Req, Env)
    161 	-> {ok, Req, Env} | {stop, Req}
    162 	when Req::cowboy_req:req(), Env::cowboy_middleware:env().
    163 execute(Req=#{host := Host, path := Path}, Env=#{dispatch := Dispatch0}) ->
    164 	Dispatch = case Dispatch0 of
    165 		{persistent_term, Key} -> persistent_term:get(Key);
    166 		_ -> Dispatch0
    167 	end,
    168 	case match(Dispatch, Host, Path) of
    169 		{ok, Handler, HandlerOpts, Bindings, HostInfo, PathInfo} ->
    170 			{ok, Req#{
    171 				host_info => HostInfo,
    172 				path_info => PathInfo,
    173 				bindings => Bindings
    174 			}, Env#{
    175 				handler => Handler,
    176 				handler_opts => HandlerOpts
    177 			}};
    178 		{error, notfound, host} ->
    179 			{stop, cowboy_req:reply(400, Req)};
    180 		{error, badrequest, path} ->
    181 			{stop, cowboy_req:reply(400, Req)};
    182 		{error, notfound, path} ->
    183 			{stop, cowboy_req:reply(404, Req)}
    184 	end.
    185 
    186 %% Internal.
    187 
    188 %% Match hostname tokens and path tokens against dispatch rules.
    189 %%
    190 %% It is typically used for matching tokens for the hostname and path of
    191 %% the request against a global dispatch rule for your listener.
    192 %%
    193 %% Dispatch rules are a list of <em>{Hostname, PathRules}</em> tuples, with
    194 %% <em>PathRules</em> being a list of <em>{Path, HandlerMod, HandlerOpts}</em>.
    195 %%
    196 %% <em>Hostname</em> and <em>Path</em> are match rules and can be either the
    197 %% atom <em>'_'</em>, which matches everything, `<<"*">>', which match the
    198 %% wildcard path, or a list of tokens.
    199 %%
    200 %% Each token can be either a binary, the atom <em>'_'</em>,
    201 %% the atom '...' or a named atom. A binary token must match exactly,
    202 %% <em>'_'</em> matches everything for a single token, <em>'...'</em> matches
    203 %% everything for the rest of the tokens and a named atom will bind the
    204 %% corresponding token value and return it.
    205 %%
    206 %% The list of hostname tokens is reversed before matching. For example, if
    207 %% we were to match "www.ninenines.eu", we would first match "eu", then
    208 %% "ninenines", then "www". This means that in the context of hostnames,
    209 %% the <em>'...'</em> atom matches properly the lower levels of the domain
    210 %% as would be expected.
    211 %%
    212 %% When a result is found, this function will return the handler module and
    213 %% options found in the dispatch list, a key-value list of bindings and
    214 %% the tokens that were matched by the <em>'...'</em> atom for both the
    215 %% hostname and path.
    216 -spec match(dispatch_rules(), Host::binary() | tokens(), Path::binary())
    217 	-> {ok, module(), any(), bindings(),
    218 		HostInfo::undefined | tokens(),
    219 		PathInfo::undefined | tokens()}
    220 	| {error, notfound, host} | {error, notfound, path}
    221 	| {error, badrequest, path}.
    222 match([], _, _) ->
    223 	{error, notfound, host};
    224 %% If the host is '_' then there can be no constraints.
    225 match([{'_', [], PathMatchs}|_Tail], _, Path) ->
    226 	match_path(PathMatchs, undefined, Path, #{});
    227 match([{HostMatch, Fields, PathMatchs}|Tail], Tokens, Path)
    228 		when is_list(Tokens) ->
    229 	case list_match(Tokens, HostMatch, #{}) of
    230 		false ->
    231 			match(Tail, Tokens, Path);
    232 		{true, Bindings, HostInfo} ->
    233 			HostInfo2 = case HostInfo of
    234 				undefined -> undefined;
    235 				_ -> lists:reverse(HostInfo)
    236 			end,
    237 			case check_constraints(Fields, Bindings) of
    238 				{ok, Bindings2} ->
    239 					match_path(PathMatchs, HostInfo2, Path, Bindings2);
    240 				nomatch ->
    241 					match(Tail, Tokens, Path)
    242 			end
    243 	end;
    244 match(Dispatch, Host, Path) ->
    245 	match(Dispatch, split_host(Host), Path).
    246 
    247 -spec match_path([dispatch_path()],
    248 	HostInfo::undefined | tokens(), binary() | tokens(), bindings())
    249 	-> {ok, module(), any(), bindings(),
    250 		HostInfo::undefined | tokens(),
    251 		PathInfo::undefined | tokens()}
    252 	| {error, notfound, path} | {error, badrequest, path}.
    253 match_path([], _, _, _) ->
    254 	{error, notfound, path};
    255 %% If the path is '_' then there can be no constraints.
    256 match_path([{'_', [], Handler, Opts}|_Tail], HostInfo, _, Bindings) ->
    257 	{ok, Handler, Opts, Bindings, HostInfo, undefined};
    258 match_path([{<<"*">>, _, Handler, Opts}|_Tail], HostInfo, <<"*">>, Bindings) ->
    259 	{ok, Handler, Opts, Bindings, HostInfo, undefined};
    260 match_path([_|Tail], HostInfo, <<"*">>, Bindings) ->
    261 	match_path(Tail, HostInfo, <<"*">>, Bindings);
    262 match_path([{PathMatch, Fields, Handler, Opts}|Tail], HostInfo, Tokens,
    263 		Bindings) when is_list(Tokens) ->
    264 	case list_match(Tokens, PathMatch, Bindings) of
    265 		false ->
    266 			match_path(Tail, HostInfo, Tokens, Bindings);
    267 		{true, PathBinds, PathInfo} ->
    268 			case check_constraints(Fields, PathBinds) of
    269 				{ok, PathBinds2} ->
    270 					{ok, Handler, Opts, PathBinds2, HostInfo, PathInfo};
    271 				nomatch ->
    272 					match_path(Tail, HostInfo, Tokens, Bindings)
    273 			end
    274 	end;
    275 match_path(_Dispatch, _HostInfo, badrequest, _Bindings) ->
    276 	{error, badrequest, path};
    277 match_path(Dispatch, HostInfo, Path, Bindings) ->
    278 	match_path(Dispatch, HostInfo, split_path(Path), Bindings).
    279 
    280 check_constraints([], Bindings) ->
    281 	{ok, Bindings};
    282 check_constraints([Field|Tail], Bindings) when is_atom(Field) ->
    283 	check_constraints(Tail, Bindings);
    284 check_constraints([Field|Tail], Bindings) ->
    285 	Name = element(1, Field),
    286 	case Bindings of
    287 		#{Name := Value0} ->
    288 			Constraints = element(2, Field),
    289 			case cowboy_constraints:validate(Value0, Constraints) of
    290 				{ok, Value} ->
    291 					check_constraints(Tail, Bindings#{Name => Value});
    292 				{error, _} ->
    293 					nomatch
    294 			end;
    295 		_ ->
    296 			check_constraints(Tail, Bindings)
    297 	end.
    298 
    299 -spec split_host(binary()) -> tokens().
    300 split_host(Host) ->
    301 	split_host(Host, []).
    302 
    303 split_host(Host, Acc) ->
    304 	case binary:match(Host, <<".">>) of
    305 		nomatch when Host =:= <<>> ->
    306 			Acc;
    307 		nomatch ->
    308 			[Host|Acc];
    309 		{Pos, _} ->
    310 			<< Segment:Pos/binary, _:8, Rest/bits >> = Host,
    311 			false = byte_size(Segment) == 0,
    312 			split_host(Rest, [Segment|Acc])
    313 	end.
    314 
    315 %% Following RFC2396, this function may return path segments containing any
    316 %% character, including <em>/</em> if, and only if, a <em>/</em> was escaped
    317 %% and part of a path segment.
    318 -spec split_path(binary()) -> tokens() | badrequest.
    319 split_path(<< $/, Path/bits >>) ->
    320 	split_path(Path, []);
    321 split_path(_) ->
    322 	badrequest.
    323 
    324 split_path(Path, Acc) ->
    325 	try
    326 		case binary:match(Path, <<"/">>) of
    327 			nomatch when Path =:= <<>> ->
    328 				remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- Acc]), []);
    329 			nomatch ->
    330 				remove_dot_segments(lists:reverse([cow_uri:urldecode(S) || S <- [Path|Acc]]), []);
    331 			{Pos, _} ->
    332 				<< Segment:Pos/binary, _:8, Rest/bits >> = Path,
    333 				split_path(Rest, [Segment|Acc])
    334 		end
    335 	catch error:_ ->
    336 		badrequest
    337 	end.
    338 
    339 remove_dot_segments([], Acc) ->
    340 	lists:reverse(Acc);
    341 remove_dot_segments([<<".">>|Segments], Acc) ->
    342 	remove_dot_segments(Segments, Acc);
    343 remove_dot_segments([<<"..">>|Segments], Acc=[]) ->
    344 	remove_dot_segments(Segments, Acc);
    345 remove_dot_segments([<<"..">>|Segments], [_|Acc]) ->
    346 	remove_dot_segments(Segments, Acc);
    347 remove_dot_segments([S|Segments], Acc) ->
    348 	remove_dot_segments(Segments, [S|Acc]).
    349 
    350 -ifdef(TEST).
    351 remove_dot_segments_test_() ->
    352 	Tests = [
    353 		{[<<"a">>, <<"b">>, <<"c">>, <<".">>, <<"..">>, <<"..">>, <<"g">>], [<<"a">>, <<"g">>]},
    354 		{[<<"mid">>, <<"content=5">>, <<"..">>, <<"6">>], [<<"mid">>, <<"6">>]},
    355 		{[<<"..">>, <<"a">>], [<<"a">>]}
    356 	],
    357 	[fun() -> R = remove_dot_segments(S, []) end || {S, R} <- Tests].
    358 -endif.
    359 
    360 -spec list_match(tokens(), dispatch_match(), bindings())
    361 	-> {true, bindings(), undefined | tokens()} | false.
    362 %% Atom '...' matches any trailing path, stop right now.
    363 list_match(List, ['...'], Binds) ->
    364 	{true, Binds, List};
    365 %% Atom '_' matches anything, continue.
    366 list_match([_E|Tail], ['_'|TailMatch], Binds) ->
    367 	list_match(Tail, TailMatch, Binds);
    368 %% Both values match, continue.
    369 list_match([E|Tail], [E|TailMatch], Binds) ->
    370 	list_match(Tail, TailMatch, Binds);
    371 %% Bind E to the variable name V and continue,
    372 %% unless V was already defined and E isn't identical to the previous value.
    373 list_match([E|Tail], [V|TailMatch], Binds) when is_atom(V) ->
    374 	case Binds of
    375 		%% @todo This isn't right, the constraint must be applied FIRST
    376 		%% otherwise we can't check for example ints in both host/path.
    377 		#{V := E} ->
    378 			list_match(Tail, TailMatch, Binds);
    379 		#{V := _} ->
    380 			false;
    381 		_ ->
    382 			list_match(Tail, TailMatch, Binds#{V => E})
    383 	end;
    384 %% Match complete.
    385 list_match([], [], Binds) ->
    386 	{true, Binds, undefined};
    387 %% Values don't match, stop.
    388 list_match(_List, _Match, _Binds) ->
    389 	false.
    390 
    391 %% Tests.
    392 
    393 -ifdef(TEST).
    394 compile_test_() ->
    395 	Tests = [
    396 		%% Match any host and path.
    397 		{[{'_', [{'_', h, o}]}],
    398 			[{'_', [], [{'_', [], h, o}]}]},
    399 		{[{"cowboy.example.org",
    400 				[{"/", ha, oa}, {"/path/to/resource", hb, ob}]}],
    401 			[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [
    402 				{[], [], ha, oa},
    403 				{[<<"path">>, <<"to">>, <<"resource">>], [], hb, ob}]}]},
    404 		{[{'_', [{"/path/to/resource/", h, o}]}],
    405 			[{'_', [], [{[<<"path">>, <<"to">>, <<"resource">>], [], h, o}]}]},
    406 		% Cyrillic from a latin1 encoded file.
    407 		{[{'_', [{[47,208,191,209,131,209,130,209,140,47,208,186,47,209,128,
    408 				208,181,209,129,209,131,209,128,209,129,209,131,47], h, o}]}],
    409 			[{'_', [], [{[<<208,191,209,131,209,130,209,140>>, <<208,186>>,
    410 				<<209,128,208,181,209,129,209,131,209,128,209,129,209,131>>],
    411 				[], h, o}]}]},
    412 		{[{"cowboy.example.org.", [{'_', h, o}]}],
    413 			[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
    414 		{[{".cowboy.example.org", [{'_', h, o}]}],
    415 			[{[<<"org">>, <<"example">>, <<"cowboy">>], [], [{'_', [], h, o}]}]},
    416 		% Cyrillic from a latin1 encoded file.
    417 		{[{[208,189,208,181,208,186,208,184,208,185,46,209,129,208,176,
    418 				208,185,209,130,46,209,128,209,132,46], [{'_', h, o}]}],
    419 			[{[<<209,128,209,132>>, <<209,129,208,176,208,185,209,130>>,
    420 				<<208,189,208,181,208,186,208,184,208,185>>],
    421 				[], [{'_', [], h, o}]}]},
    422 		{[{":subdomain.example.org", [{"/hats/:name/prices", h, o}]}],
    423 			[{[<<"org">>, <<"example">>, subdomain], [], [
    424 				{[<<"hats">>, name, <<"prices">>], [], h, o}]}]},
    425 		{[{"ninenines.:_", [{"/hats/:_", h, o}]}],
    426 			[{['_', <<"ninenines">>], [], [{[<<"hats">>, '_'], [], h, o}]}]},
    427 		{[{"[www.]ninenines.eu",
    428 			[{"/horses", h, o}, {"/hats/[page/:number]", h, o}]}], [
    429 				{[<<"eu">>, <<"ninenines">>], [], [
    430 					{[<<"horses">>], [], h, o},
    431 					{[<<"hats">>], [], h, o},
    432 					{[<<"hats">>, <<"page">>, number], [], h, o}]},
    433 				{[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
    434 					{[<<"horses">>], [], h, o},
    435 					{[<<"hats">>], [], h, o},
    436 					{[<<"hats">>, <<"page">>, number], [], h, o}]}]},
    437 		{[{'_', [{"/hats/:page/:number", h, o}]}], [{'_', [], [
    438 			{[<<"hats">>, page, number], [], h, o}]}]},
    439 		{[{'_', [{"/hats/[page/[:number]]", h, o}]}], [{'_', [], [
    440 			{[<<"hats">>], [], h, o},
    441 			{[<<"hats">>, <<"page">>], [], h, o},
    442 			{[<<"hats">>, <<"page">>, number], [], h, o}]}]},
    443 		{[{"[...]ninenines.eu", [{"/hats/[...]", h, o}]}],
    444 			[{[<<"eu">>, <<"ninenines">>, '...'], [], [
    445 				{[<<"hats">>, '...'], [], h, o}]}]},
    446 		%% Path segment containing a colon.
    447 		{[{'_', [{"/foo/bar:blah", h, o}]}], [{'_', [], [
    448 			{[<<"foo">>, <<"bar:blah">>], [], h, o}]}]}
    449 	],
    450 	[{lists:flatten(io_lib:format("~p", [Rt])),
    451 		fun() -> Rs = compile(Rt) end} || {Rt, Rs} <- Tests].
    452 
    453 split_host_test_() ->
    454 	Tests = [
    455 		{<<"">>, []},
    456 		{<<"*">>, [<<"*">>]},
    457 		{<<"cowboy.ninenines.eu">>,
    458 			[<<"eu">>, <<"ninenines">>, <<"cowboy">>]},
    459 		{<<"ninenines.eu">>,
    460 			[<<"eu">>, <<"ninenines">>]},
    461 		{<<"ninenines.eu.">>,
    462 			[<<"eu">>, <<"ninenines">>]},
    463 		{<<"a.b.c.d.e.f.g.h.i.j.k.l.m.n.o.p.q.r.s.t.u.v.w.x.y.z">>,
    464 			[<<"z">>, <<"y">>, <<"x">>, <<"w">>, <<"v">>, <<"u">>, <<"t">>,
    465 			<<"s">>, <<"r">>, <<"q">>, <<"p">>, <<"o">>, <<"n">>, <<"m">>,
    466 			<<"l">>, <<"k">>, <<"j">>, <<"i">>, <<"h">>, <<"g">>, <<"f">>,
    467 			<<"e">>, <<"d">>, <<"c">>, <<"b">>, <<"a">>]}
    468 	],
    469 	[{H, fun() -> R = split_host(H) end} || {H, R} <- Tests].
    470 
    471 split_path_test_() ->
    472 	Tests = [
    473 		{<<"/">>, []},
    474 		{<<"/extend//cowboy">>, [<<"extend">>, <<>>, <<"cowboy">>]},
    475 		{<<"/users">>, [<<"users">>]},
    476 		{<<"/users/42/friends">>, [<<"users">>, <<"42">>, <<"friends">>]},
    477 		{<<"/users/a%20b/c%21d">>, [<<"users">>, <<"a b">>, <<"c!d">>]}
    478 	],
    479 	[{P, fun() -> R = split_path(P) end} || {P, R} <- Tests].
    480 
    481 match_test_() ->
    482 	Dispatch = [
    483 		{[<<"eu">>, <<"ninenines">>, '_', <<"www">>], [], [
    484 			{[<<"users">>, '_', <<"mails">>], [], match_any_subdomain_users, []}
    485 		]},
    486 		{[<<"eu">>, <<"ninenines">>], [], [
    487 			{[<<"users">>, id, <<"friends">>], [], match_extend_users_friends, []},
    488 			{'_', [], match_extend, []}
    489 		]},
    490 		{[var, <<"ninenines">>], [], [
    491 			{[<<"threads">>, var], [], match_duplicate_vars,
    492 				[we, {expect, two}, var, here]}
    493 		]},
    494 		{[ext, <<"erlang">>], [], [
    495 			{'_', [], match_erlang_ext, []}
    496 		]},
    497 		{'_', [], [
    498 			{[<<"users">>, id, <<"friends">>], [], match_users_friends, []},
    499 			{'_', [], match_any, []}
    500 		]}
    501 	],
    502 	Tests = [
    503 		{<<"any">>, <<"/">>, {ok, match_any, [], #{}}},
    504 		{<<"www.any.ninenines.eu">>, <<"/users/42/mails">>,
    505 			{ok, match_any_subdomain_users, [], #{}}},
    506 		{<<"www.ninenines.eu">>, <<"/users/42/mails">>,
    507 			{ok, match_any, [], #{}}},
    508 		{<<"www.ninenines.eu">>, <<"/">>,
    509 			{ok, match_any, [], #{}}},
    510 		{<<"www.any.ninenines.eu">>, <<"/not_users/42/mails">>,
    511 			{error, notfound, path}},
    512 		{<<"ninenines.eu">>, <<"/">>,
    513 			{ok, match_extend, [], #{}}},
    514 		{<<"ninenines.eu">>, <<"/users/42/friends">>,
    515 			{ok, match_extend_users_friends, [], #{id => <<"42">>}}},
    516 		{<<"erlang.fr">>, '_',
    517 			{ok, match_erlang_ext, [], #{ext => <<"fr">>}}},
    518 		{<<"any">>, <<"/users/444/friends">>,
    519 			{ok, match_users_friends, [], #{id => <<"444">>}}},
    520 		{<<"any">>, <<"/users//friends">>,
    521 			{ok, match_users_friends, [], #{id => <<>>}}}
    522 	],
    523 	[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
    524 		{ok, Handler, Opts, Binds, undefined, undefined}
    525 			= match(Dispatch, H, P)
    526 	end} || {H, P, {ok, Handler, Opts, Binds}} <- Tests].
    527 
    528 match_info_test_() ->
    529 	Dispatch = [
    530 		{[<<"eu">>, <<"ninenines">>, <<"www">>], [], [
    531 			{[<<"pathinfo">>, <<"is">>, <<"next">>, '...'], [], match_path, []}
    532 		]},
    533 		{[<<"eu">>, <<"ninenines">>, '...'], [], [
    534 			{'_', [], match_any, []}
    535 		]}
    536 	],
    537 	Tests = [
    538 		{<<"ninenines.eu">>, <<"/">>,
    539 			{ok, match_any, [], #{}, [], undefined}},
    540 		{<<"bugs.ninenines.eu">>, <<"/">>,
    541 			{ok, match_any, [], #{}, [<<"bugs">>], undefined}},
    542 		{<<"cowboy.bugs.ninenines.eu">>, <<"/">>,
    543 			{ok, match_any, [], #{}, [<<"cowboy">>, <<"bugs">>], undefined}},
    544 		{<<"www.ninenines.eu">>, <<"/pathinfo/is/next">>,
    545 			{ok, match_path, [], #{}, undefined, []}},
    546 		{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/path_info">>,
    547 			{ok, match_path, [], #{}, undefined, [<<"path_info">>]}},
    548 		{<<"www.ninenines.eu">>, <<"/pathinfo/is/next/foo/bar">>,
    549 			{ok, match_path, [], #{}, undefined, [<<"foo">>, <<"bar">>]}}
    550 	],
    551 	[{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() ->
    552 		R = match(Dispatch, H, P)
    553 	end} || {H, P, R} <- Tests].
    554 
    555 match_constraints_test() ->
    556 	Dispatch0 = [{'_', [],
    557 		[{[<<"path">>, value], [{value, int}], match, []}]}],
    558 	{ok, _, [], #{value := 123}, _, _} = match(Dispatch0,
    559 		<<"ninenines.eu">>, <<"/path/123">>),
    560 	{ok, _, [], #{value := 123}, _, _} = match(Dispatch0,
    561 		<<"ninenines.eu">>, <<"/path/123/">>),
    562 	{error, notfound, path} = match(Dispatch0,
    563 		<<"ninenines.eu">>, <<"/path/NaN/">>),
    564 	Dispatch1 = [{'_', [],
    565 		[{[<<"path">>, value, <<"more">>], [{value, nonempty}], match, []}]}],
    566 	{ok, _, [], #{value := <<"something">>}, _, _} = match(Dispatch1,
    567 		<<"ninenines.eu">>, <<"/path/something/more">>),
    568 	{error, notfound, path} = match(Dispatch1,
    569 		<<"ninenines.eu">>, <<"/path//more">>),
    570 	Dispatch2 = [{'_', [], [{[<<"path">>, username],
    571 		[{username, fun(_, Value) ->
    572 			case cowboy_bstr:to_lower(Value) of
    573 				Value -> {ok, Value};
    574 				_ -> {error, not_lowercase}
    575 			end end}],
    576 		match, []}]}],
    577 	{ok, _, [], #{username := <<"essen">>}, _, _} = match(Dispatch2,
    578 		<<"ninenines.eu">>, <<"/path/essen">>),
    579 	{error, notfound, path} = match(Dispatch2,
    580 		<<"ninenines.eu">>, <<"/path/ESSEN">>),
    581 	ok.
    582 
    583 match_same_bindings_test() ->
    584 	Dispatch = [{[same, same], [], [{'_', [], match, []}]}],
    585 	{ok, _, [], #{same := <<"eu">>}, _, _} = match(Dispatch,
    586 		<<"eu.eu">>, <<"/">>),
    587 	{error, notfound, host} = match(Dispatch,
    588 		<<"ninenines.eu">>, <<"/">>),
    589 	Dispatch2 = [{[<<"eu">>, <<"ninenines">>, user], [],
    590 		[{[<<"path">>, user], [], match, []}]}],
    591 	{ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2,
    592 		<<"essen.ninenines.eu">>, <<"/path/essen">>),
    593 	{ok, _, [], #{user := <<"essen">>}, _, _} = match(Dispatch2,
    594 		<<"essen.ninenines.eu">>, <<"/path/essen/">>),
    595 	{error, notfound, path} = match(Dispatch2,
    596 		<<"essen.ninenines.eu">>, <<"/path/notessen">>),
    597 	Dispatch3 = [{'_', [], [{[same, same], [], match, []}]}],
    598 	{ok, _, [], #{same := <<"path">>}, _, _} = match(Dispatch3,
    599 		<<"ninenines.eu">>, <<"/path/path">>),
    600 	{error, notfound, path} = match(Dispatch3,
    601 		<<"ninenines.eu">>, <<"/path/to">>),
    602 	ok.
    603 -endif.