Skip to content

Instantly share code, notes, and snippets.

@jmhodges
Created June 26, 2009 00:07
Show Gist options
  • Save jmhodges/136243 to your computer and use it in GitHub Desktop.
Save jmhodges/136243 to your computer and use it in GitHub Desktop.
-module(recess_controller).
-import(error_logger).
-export([
render/2, render/3,
render_error/2,
not_found/1, not_found/2,
method_not_allowed/1, method_not_allowed/2,
internal_server_error/1, internal_server_error/2,
ambiguous_routing/1
]).
render(_Request, Result) ->
{200, [{"Content-Type", "text/plain"}], Result}.
render(_Request, Result, Status) ->
{Status, [{"Content-Type", "text/plain"}], Result}.
render_error(_Request, Error) ->
{500, [{"Content-Type", "text/plain"}], Error}.
not_found(_Req, Body) -> {404, [{"Content-Type", "text/plain"}], Body}.
not_found(Req) -> not_found(Req, "Resource Not Found").
method_not_allowed(Req) -> method_not_allowed(Req, "Method Not Allowed").
method_not_allowed(_Req, Body) -> {405, [{"Content-Type", "text/plain"}], Body}.
ambiguous_routing(Req) -> internal_server_error(Req, "Multiple Resources Satisfied Given Method and Path").
internal_server_error(Req) -> internal_server_error(Req, "Internal Server Error").
internal_server_error(_Req, Body) -> {500, [{"Content-Type", "text/plain"}], Body}.
-module(recess_httpd).
-import(mochiweb_http).
-import(error_logger).
-import(io).
-export ([start_link/3, stop/0]).
start_link(BindAddress, PortNum, DocumentRoot) ->
Loop = fun(Req) -> handle_request(Req, DocumentRoot) end,
mochiweb_http:start([
{loop, Loop},
{name, ?MODULE},
{port, PortNum},
{ip, BindAddress}
]).
stop() -> mochiweb_http:stop(?MODULE).
% Private
handle_request(Req, DocumentRoot) ->
% alias HEAD to GET because mochiweb takes care of stripping the body
Method = case Req:get(method) of
'HEAD' -> 'GET';
Other -> Other
end,
% file:join/1 assumes / is root in all of the paths given to it and will
% not override it. So, we break it off for file serving and for pattern-
% matching routes
[ "/" | Path ] = filename:split(Req:get(path)),
Params = Req:parse_qs(),
case catch(match(Req, DocumentRoot, Method, Path, Params)) of
{ok, Response} -> Req:respond(Response);
{file, Path, DocumentRoot} -> Req:serve_file(Path, DocumentRoot);
_ -> recess_controller:internal_server_error(Req)
end.
match(Req, DocumentRoot, Method, Path, Params) ->
%% mochiweb tosses out ".." on its own, so no safety checks for it here.
FilePath = filename:join( [DocumentRoot | Path] ),
case filelib:is_regular(FilePath) of
false ->
controller_response(Req, Method, Path, Params);
true ->
{file, Path, DocumentRoot}
end.
controller_response(Req, Method, Path, Params) ->
case recess_routing:find(Method, Path) of
{error, not_found_by_path} ->
{ok, recess_controller:not_found(Req)};
{error, not_found_by_method} ->
{ok, recess_controller:method_not_allowed(Req)};
{error, ambiguous_routing} ->
{ok, recess_controller:ambiguous_routing(Req)};
{ok, Route} ->
{ok, Route:run(Req, Path, Params)};
_ -> {ok, recess_controller:internal_server_error(Req)}
end.
-module(recess_route, [Method, RoutePath, Controller, Function]).
-import(recess_utils, [keycompact/1, keymergereplace/2]).
-import(lists, [zip/2, keysort/2, filter/2]).
-export([
http_method/0,
path/0,
controller/0,
function/0,
run/3
]).
http_method() -> Method.
path() -> RoutePath.
controller() -> Controller.
function() -> Function.
run(Request, Path, CGIParams) ->
Params = params_from_path_and_cgi(Path, CGIParams),
Con = Controller:new(Request, Path, Params),
case Con:Function() of
{ok, Result} -> recess_controller:render(Request, Result);
{ok, Result, Status} -> recess_controller:render(Request, Result, Status);
{error, Error} -> recess_controller:render_error(Request, Error);
_ -> recess_controller:internal_server_error(Request)
end.
is_path_variable(RouteItem) ->
is_atom(RouteItem).
params_from_path_and_cgi(Path, CGIParams) ->
% Create a tuple list with the parts of the Path as the keys and the
% equivalent parts of the RoutePath as the values. Sort that list and
% remove the duplicates. Only the first seen values of duplicates are used.
% One might be concerned that a sort would shuffle the duplicates around,
% but I suggest you think a bit harder and maybe use some paper to check it
% out.
IsPathVar = fun( {RouteItem, _PathItem} ) -> is_path_variable(RouteItem) end,
PathParams = keysort(1, filter( IsPathVar, zip(RoutePath, Path) )),
CompactPathParams = keycompact(PathParams),
CompactCGIParams = keycompact(keysort(1, CGIParams)),
% Params from a path take precedence over ones in the CGI query string
keymergereplace(CompactPathParams, CompactCGIParams).
-module(recess_routing).
-behaviour(gen_server).
% gen_server boilerplate exports
-export([
init/1,
handle_call/3,
handle_cast/2,
handle_info/2,
terminate/2,
code_change/3
]).
% API exports
-export([
start_link/0,
stop/0,
add/1,
all/0,
find/2,
clear/0
]).
% gen_server behavior boilerplate
init([]) ->
{ok, []}.
handle_call(stop, _From, Routes) ->
{stop, normal, Routes};
handle_call({add, NewRoute}, _From, Routes) ->
case is_duplicate(NewRoute, Routes) of
true ->
{reply, {error, duplicate_route}, Routes};
false ->
{reply, {ok, NewRoute}, Routes ++ [NewRoute]}
end;
handle_call(all, _From, Routes) ->
{reply, Routes, Routes};
handle_call({find, Method, Path}, _From, Routes) ->
Response = find_by_path_and_http_method(Method, Path, Routes),
{reply, Response, Routes};
handle_call(clear, _From, _Routes) ->
{reply, ok, []}.
% handle_call({remove, Route}, _From, Routes) ->
% NewRoutes = Routes -- [Route],
%
% RLen = length(Routes),
% NRLen = length(NewRoutes),
%
% Response = case RLen =:= NRLen of
% true -> {error, route_not_found};
% false -> {ok, Route}
% end,
%
% {reply, Response, NewRoutes};
handle_cast(_Request, Routes) ->
{noreply, Routes}.
handle_info(_Info, Routes) ->
{noreply, Routes}.
terminate(_Reason, _Routes) ->
ok.
code_change(_OldVersion, Routes, _Extra) ->
{ok, Routes}.
% API
start_link() ->
gen_server:start_link({local, ?MODULE}, ?MODULE, [], []).
stop() ->
gen_server:call(?MODULE, stop).
all() ->
gen_server:call(?MODULE, all).
add(NewRoute) ->
gen_server:call(?MODULE, {add, NewRoute}).
clear() -> gen_server:call(?MODULE, clear).
find(Method, Path) ->
gen_server:call(?MODULE, {find, Method, Path}).
% Private
is_duplicate(NewRoute, Routes) ->
Response = find_by_path_and_http_method(
NewRoute:http_method(), NewRoute:path(), Routes
),
case Response of
{error, not_found_by_path} -> false;
{error, not_found_by_method} -> false;
{error, ambiguous_routing} -> true;
{ok, _} -> true
end.
find_by_path_and_http_method(Method, Path, Routes) ->
case MatchingPathRoutes = filter_by_path(Path, Routes) of
[] ->
{error, not_found_by_path};
_ ->
find_by_http_method(Method, MatchingPathRoutes)
end.
find_by_http_method(Method, Routes) ->
case filter_by_http_method(Method, Routes) of
[] -> {error, not_found_by_method};
[Route] -> {ok, Route};
_ -> {error, ambiguous_routing}
end.
filter_by_path(Path, Routes) ->
lists:filter(fun(Route) -> paths_match(Path, Route:path()) end, Routes).
filter_by_http_method(Method, Routes) ->
lists:filter(fun(Route) -> Method =:= Route:http_method() end, Routes).
paths_match([], []) -> true; % if we get here, we know we matched properly
paths_match([PathPrefix | PathSuffix], [RoutePrefix | RouteSuffix]) ->
case path_items_match(PathPrefix, RoutePrefix) of
true -> paths_match(PathSuffix, RouteSuffix);
false -> false
end;
paths_match(_Path, _RoutePath) ->
% if we get here, it means one of the args is blank and the other isn't
% which, of course, is not a match
false.
path_items_match(PathItem, RouteItem) ->
% if RouteItem is an atom than PathItem is the part of a path that fills a
% variable in the controller's method, and it is not allowed to be empty.
% Otherwise, check to make sure that PathItem and RouteItem are the same.
( is_atom(RouteItem) andalso PathItem =/= [] ) orelse PathItem =:= RouteItem.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment