From da7225594010d0df20779a0915474ccedd25d7cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Lo=C3=AFc=20Hoguin?= Date: Mon, 7 Mar 2011 22:59:22 +0100 Subject: Initial commit. --- src/cowboy_dispatcher.erl | 153 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 153 insertions(+) create mode 100644 src/cowboy_dispatcher.erl (limited to 'src/cowboy_dispatcher.erl') diff --git a/src/cowboy_dispatcher.erl b/src/cowboy_dispatcher.erl new file mode 100644 index 0000000..3e9a7e0 --- /dev/null +++ b/src/cowboy_dispatcher.erl @@ -0,0 +1,153 @@ +%% Copyright (c) 2011, Loïc Hoguin +%% +%% 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. + +-module(cowboy_dispatcher). +-export([split_host/1, split_path/1, match/3]). %% API. + +-include("include/types.hrl"). +-include_lib("eunit/include/eunit.hrl"). + +%% API. + +-spec split_host(Host::string()) -> Tokens::path_tokens(). +split_host(Host) -> + string:tokens(Host, "."). + +-spec split_path(Path::string()) -> {Tokens::path_tokens(), Qs::string()}. +split_path(Path) -> + case string:chr(Path, $?) of + 0 -> + {string:tokens(Path, "/"), []}; + N -> + {Path2, [$?|Qs]} = lists:split(N - 1, Path), + {string:tokens(Path2, "/"), Qs} + end. + +-spec match(Host::path_tokens(), Path::path_tokens(), Dispatch::dispatch()) + -> {ok, Handler::module(), Opts::term(), Binds::bindings()} | {error, notfound}. +match(_Host, _Path, []) -> + {error, notfound}; +match(_Host, _Path, [{'_', '_', Handler, Opts}|_Tail]) -> + {ok, Handler, Opts, []}; +match(Host, Path, [{HostMatch, PathMatch, Handler, Opts}|Tail]) -> + case try_match(host, Host, HostMatch) of + false -> match(Host, Path, Tail); + {true, HostBinds} -> case try_match(path, Path, PathMatch) of + false -> match(Host, Path, Tail); + {true, PathBinds} -> {ok, Handler, Opts, HostBinds ++ PathBinds} + end + end. + +%% Internal. + +-spec try_match(Type::host | path, List::path_tokens(), Match::match()) + -> {true, Binds::bindings()} | false. +try_match(_Type, _List, '_') -> + {true, []}; +try_match(_Type, List, Match) when length(List) =/= length(Match) -> + 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(), Binds::bindings()) + -> {true, Binds::bindings()} | false. +%% Atom '_' matches anything, continue. +list_match([_E|Tail], ['_'|TailMatch], Binds) -> + list_match(Tail, TailMatch, Binds); +%% Both values match, continue. +list_match([E|Tail], [E|TailMatch], Binds) -> + list_match(Tail, 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}. + +%% Tests. + +-ifdef(TEST). + +split_host_test_() -> + %% {Host, Result} + Tests = [ + {"", []}, + {".........", []}, + {"*", ["*"]}, + {"cowboy.dev-extend.eu", ["cowboy", "dev-extend", "eu"]}, + {"dev-extend..eu", ["dev-extend", "eu"]}, + {"dev-extend.eu", ["dev-extend", "eu"]}, + {"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", + ["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"]} + ], + [{H, fun() -> R = split_host(H) end} || {H, R} <- Tests]. + +split_path_test_() -> + %% {Path, Result, QueryString} + Tests = [ + {"?", [], []}, + {"???", [], "??"}, + {"*", ["*"], []}, + {"/", [], []}, + {"/users", ["users"], []}, + {"/users?", ["users"], []}, + {"/users?a", ["users"], "a"}, + {"/users/42/friends?a=b&c=d&e=notsure?whatever", + ["users", "42", "friends"], "a=b&c=d&e=notsure?whatever"} + ], + [{P, fun() -> {R, Qs} = split_path(P) end} || {P, R, Qs} <- Tests]. + +match_test_() -> + Dispatch = [ + {["www", '_', "dev-extend", "eu"], ["users", '_', "mails"], + match_any_subdomain_users, []}, + {["dev-extend", "eu"], ["users", id, "friends"], + match_extend_users_friends, []}, + {["dev-extend", var], ["threads", var], + match_duplicate_vars, [we, {expect, two}, var, here]}, + {["dev-extend", "eu"], '_', match_extend, []}, + {["erlang", ext], '_', match_erlang_ext, []}, + {'_', ["users", id, "friends"], match_users_friends, []}, + {'_', '_', match_any, []} + ], + %% {Host, Path, Result} + Tests = [ + {["any"], [], {ok, match_any, [], []}}, + {["www", "any", "dev-extend", "eu"], ["users", "42", "mails"], + {ok, match_any_subdomain_users, [], []}}, + {["www", "dev-extend", "eu"], ["users", "42", "mails"], + {ok, match_any, [], []}}, + {["www", "any", "dev-extend", "eu"], ["not_users", "42", "mails"], + {ok, match_any, [], []}}, + {["dev-extend", "eu"], [], {ok, match_extend, [], []}}, + {["dev-extend", "eu"], ["users", "42", "friends"], + {ok, match_extend_users_friends, [], [{id, "42"}]}}, + {["erlang", "fr"], '_', {ok, match_erlang_ext, [], [{ext, "fr"}]}}, + {["any"], ["users", "444", "friends"], + {ok, match_users_friends, [], [{id, "444"}]}}, + {["dev-extend", "eu"], ["threads", "987"], + {ok, match_duplicate_vars, [we, {expect, two}, var, here], + [{var, "eu"}, {var, "987"}]}} + ], + [{lists:flatten(io_lib:format("~p, ~p", [H, P])), fun() -> + R = match(H, P, Dispatch) + end} || {H, P, R} <- Tests]. + +-endif. -- cgit v1.2.3