jmhodges (owner)

Forks

Revisions

gist: 136243 Download_button fork
public
Public Clone URL: git://gist.github.com/136243.git
Embed All Files: show embed
recess_controller.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
-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}.
recess_httpd.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
-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.
recess_route.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
-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).
recess_routing.erl #
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
-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.