Created
June 26, 2009 00:07
-
-
Save jmhodges/136243 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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}. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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). |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
-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