Skip to content
Browse files

Initial release

  • Loading branch information...
0 parents commit 3e0bf61e441114289d76ad30578c1ea8ee6421cd @flashingpumpkin committed Jun 19, 2011
22 LICENSE
@@ -0,0 +1,22 @@
+Copyright (c) 2011, Alen Mujezinovic <flashingpupmkin@gmail.com>
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
11 Makefile
@@ -0,0 +1,11 @@
+all:
+ ./rebar compile
+
+_examples:
+ erlc -pa ebin/ -o ebin/ examples/*erl
+
+examples: all _examples
+
+test: _examples
+ ./rebar compile eunit
+
50 README.md
@@ -0,0 +1,50 @@
+# Spooky
+
+## Synposis
+
+Spooky is a lightweight and dead easy to use RESTful request handler for
+Erlang.
+
+It's using the [Misultin](https://github.com/ostinelli/misultin) http
+library and provides RESTful request handling similar to the
+[Sinatra](http://www.sinatrarb.com/) web framework.
+
+*hello_world.erl*
+
+ -module(hello_world).
+ -behaviour(spooky).
+ -export([init/1, get/2]).
+
+ init([])->
+ [{ port, 8000 }].
+
+ get(Req, [])->
+ Req.ok("Hello world.");
+ get(Req, ["smashingpumpkins"])->
+ throw({418, "I'm a teapot."});
+ get(Req, [Name])->
+ Req.ok("Hello world, " ++ Name ++ ".").
+
+*shell*
+
+ $ erl
+ [...]
+ 1> spooky:start_link(hello_world)
+ {ok, <0.40.0>}
+ 2> spooky:stop()
+ true
+
+*shell*
+
+
+## Why?
+
+Scratching an itch. Spooky is as simple as it gets - you're left to deal
+with everything else than request handling. If this is not what you are
+looking for and need things like ORM support, templating, etc, you should
+look at other frameworks:
+
+* [Nitrogen](http://nitrogenproject.com/)
+* [Erlang Web](http://www.erlang-web.org/)
+* [Chicago Boss](http://www.chicagoboss.org/)
+* [Zotonic](http://zotonic.com/)
32 examples/spooky_get_hello_world.erl
@@ -0,0 +1,32 @@
+%% Author: Alen Mujezinovic
+%% Created: 18 Jun 2011
+%% Description: This file shows how to split an application up into multiple
+%% files. See `example/multi_hello_world.erl`
+-module(spooky_get_hello_world).
+-behaviour(spooky).
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([init/1, get/2]).
+
+%%
+%% API Functions
+%%
+
+init([])->
+ [{port, 8000}, {handlers, [?MODULE]}].
+
+get(Req, [])->
+ Req:ok("Hello world.");
+get(Req, [Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+%%
+%% Local Functions
+%%
+
52 examples/spooky_hello_world.erl
@@ -0,0 +1,52 @@
+%% Author: Alen Mujezinovic
+%% Created: 18 Jun 2011
+%% Description: TODO: Add description to simple_hello_world
+-module(spooky_hello_world).
+-behaviour(spooky).
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([init/1, get/2,post/2,put/2,delete/2,head/2]).
+
+%%
+%% API Functions
+%%
+
+init([])->
+ [{port, 8000}].
+
+get(Req,[])->
+ Req:ok("Hello world");
+get(_Req, ["smashingpumpkins"])->
+ throw(418);
+get(Req,[Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+post(Req,[])->
+ Req:ok("Hello world");
+post(_Req, ["smashingpumpkins"])->
+ throw({418, "I'm a teapot."});
+post(Req,[Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+put(Req,[])->
+ Req:ok("Hello world");
+put(_Req, ["smashingpumpkins"])->
+ throw({418, [{"X-Server", "Teapot"}], "I'm a teapot."});
+put(Req,[Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+delete(Req,[])->
+ Req:ok("Hello world");
+delete(Req,[Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+head(Req,[])->
+ Req:ok("Hello world");
+head(Req,[Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
22 examples/spooky_multi_hello_world.erl
@@ -0,0 +1,22 @@
+%% Author: Alen Mujezinovic
+%% Created: 18 Jun 2011
+%% Description: This file shows how to split an application up into multiple
+%% files.
+-module(spooky_multi_hello_world).
+-behaviour(spooky).
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([init/1]).
+
+%%
+%% API Functions
+%%
+
+init([])->
+ [{port, 8000}, {handlers, [spooky_get_hello_world, spooky_post_hello_world]}].
32 examples/spooky_post_hello_world.erl
@@ -0,0 +1,32 @@
+%% Author: Alen Mujezinovic
+%% Created: 18 Jun 2011
+%% Description: This file shows how to split an application up into multiple
+%% files. See `example/multi_hello_world.erl`
+-module(spooky_post_hello_world).
+-behaviour(spooky).
+
+%%
+%% Include files
+%%
+
+%%
+%% Exported Functions
+%%
+-export([init/1, post/2]).
+
+%%
+%% API Functions
+%%
+
+init([])->
+ [{port, 8000}, {handlers, [?MODULE]}].
+
+post(Req, [])->
+ Req:ok("Hello world.");
+post(Req, [Name])->
+ Req:ok("Hello world, " ++ Name ++ ".").
+
+%%
+%% Local Functions
+%%
+
BIN rebar
Binary file not shown.
1 rebar.config
@@ -0,0 +1 @@
+{cover_enabled, true}.
1 src/misultin
@@ -0,0 +1 @@
+Subproject commit f55b61025e029ad5a8b7a7758a1fa2724cb24bb9
11 src/spooky.app.src
@@ -0,0 +1,11 @@
+{application, spooky,
+ [
+ {description, ""},
+ {vsn, "1"},
+ {registered, []},
+ {applications, [
+ kernel,
+ stdlib
+ ]},
+ {env, []}
+ ]}.
84 src/spooky.erl
@@ -0,0 +1,84 @@
+%% Author: Alen Mujezinovic
+%% Created: 18 Jun 2011
+%% Description: TODO: Add description to spooky
+-module(spooky).
+-behaviour(supervisor).
+
+%%
+%% Exported Functions
+%%
+-export([start_link/1, init/1, stop/0, behaviour_info/1]).
+
+%%
+%% API Functions
+%%
+start_link(Module) when is_atom(Module)->
+ start(Module);
+start_link(Modules) when is_list(Modules)->
+ start(Modules).
+
+stop()->
+ stop(?MODULE).
+stop(Name) when is_atom(Name) ->
+ stop(whereis(Name));
+stop(Pid) when is_pid(Pid)->
+ exit(Pid, normal).
+
+%%
+%% Setup functions
+%%
+start(Module) when is_atom(Module)->
+ start([Module], []);
+start(Modules) when is_list(Modules)->
+ start(Modules, []).
+
+start([Module|T], Handlers)->
+ % The first module's options are the ones for misultin
+ Opts = apply(Module, init, [[]]),
+
+ Loop = fun(Req)-> spooky_server:handle(Req) end,
+
+ Opts0 = Opts ++ [{loop, Loop}],
+ Opts1 = proplists:delete(handlers, Opts0),
+ start([Module|T], Handlers, Opts1).
+
+start([Module|T], Handlers, Opts)->
+ % Accumulate all the handlers
+ ModuleOpts = apply(Module, init, [[]]),
+
+ case proplists:lookup(handlers, ModuleOpts)of
+ none ->
+ ModuleHandlers = [Module];
+ {handlers, ModuleHandlers}->
+ ModuleHandlers = ModuleHandlers
+ end,
+ start(T, Handlers ++ ModuleHandlers, Opts);
+start([], Handlers, Opts)->
+ % Start the supervisor
+ supervisor:start_link({local, ?MODULE}, ?MODULE, [Handlers, Opts]).
+
+%%
+%% Supervisor callback
+%%
+init([Handlers, Opts])->
+ % misultin specs
+ MisultinSpecs = {misultin,
+ {misultin, start_link, [Opts]},
+ permanent, infinity, supervisor, [misultin]
+ },
+ % spooky specs
+ ServerSpecs = {spooky_server,
+ {spooky_server, start_link, [Handlers]},
+ permanent, 5000, worker, [spooky_server]
+ },
+
+ {ok, { {one_for_all, 5, 10}, [MisultinSpecs, ServerSpecs]} }.
+
+%%
+%% Behaviour information
+%%
+behaviour_info(callbacks)->
+ [{init, 1}];
+behaviour_info(_Other)->
+ undefined.
+
147 src/spooky_server.erl
@@ -0,0 +1,147 @@
+%%% -------------------------------------------------------------------
+%%% Author : Alen Mujezinovic
+%%% Description :
+%%%
+%%% Created : 18 Jun 2011
+%%% -------------------------------------------------------------------
+-module(spooky_server).
+-behaviour(gen_server).
+
+%% --------------------------------------------------------------------
+%% Include files
+%% --------------------------------------------------------------------
+
+%% --------------------------------------------------------------------
+%% External exports
+-export([start_link/1, handle/1, handlers/0]).
+
+%% gen_server callbacks
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-record(state, {handlers}).
+
+%% ====================================================================
+%% External functions
+%% ====================================================================
+
+start_link(Handlers)->
+ gen_server:start_link({local, ?MODULE}, ?MODULE, [Handlers], []).
+
+handle(Req)->
+ io:format("Handling request~n"),
+ Handlers = handlers(),
+ Method = Req:get(method),
+ handle(Req, Method, Handlers).
+
+handlers()->
+ gen_server:call(?MODULE, handlers).
+
+%% ====================================================================
+%% Server functions
+%% ====================================================================
+
+%% --------------------------------------------------------------------
+%% Function: init/1
+%% Description: Initiates the server
+%% Returns: {ok, State} |
+%% {ok, State, Timeout} |
+%% ignore |
+%% {stop, Reason}
+%% --------------------------------------------------------------------
+init([Handlers]) ->
+ io:format("Started process with handlers ~p ~n", [Handlers]),
+ {ok, #state{handlers=Handlers}}.
+
+%% --------------------------------------------------------------------
+%% Function: handle_call/3
+%% Description: Handling call messages
+%% Returns: {reply, Reply, State} |
+%% {reply, Reply, State, Timeout} |
+%% {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, Reply, State} | (terminate/2 is called)
+%% {stop, Reason, State} (terminate/2 is called)
+%% --------------------------------------------------------------------
+handle_call(handlers, _From, State=#state{handlers=Handlers})->
+ {reply, Handlers, State};
+handle_call(_Request, _From, _State) ->
+ _Reply = ok,
+ {reply, _Reply, _State}.
+
+%% --------------------------------------------------------------------
+%% Function: handle_cast/2
+%% Description: Handling cast messages
+%% Returns: {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State} (terminate/2 is called)
+%% --------------------------------------------------------------------
+handle_cast(_Msg, _State) ->
+ {noreply, _State}.
+
+%% --------------------------------------------------------------------
+%% Function: handle_info/2
+%% Description: Handling all non call/cast messages
+%% Returns: {noreply, State} |
+%% {noreply, State, Timeout} |
+%% {stop, Reason, State} (terminate/2 is called)
+%% --------------------------------------------------------------------
+handle_info(_Info, _State) ->
+ {noreply, _State}.
+
+%% --------------------------------------------------------------------
+%% Function: terminate/2
+%% Description: Shutdown the server
+%% Returns: any (ignored by gen_server)
+%% --------------------------------------------------------------------
+terminate(_Reason, _State) ->
+ ok.
+
+%% --------------------------------------------------------------------
+%% Func: code_change/3
+%% Purpose: Convert process state when code is changed
+%% Returns: {ok, NewState}
+%% --------------------------------------------------------------------
+code_change(_OldVsn, _State, _Extra) ->
+ {ok, _State}.
+
+%% --------------------------------------------------------------------
+%%% Internal functions
+%% --------------------------------------------------------------------
+
+
+handle(Req, _Method, [], _Path)->
+ Req:respond(404);
+handle(Req, Method, [Handler|T], Path)->
+ io:format("Trying handler ~p ~n", [Handler]),
+ try apply(Handler, Method, [Req, Path]) of
+ _ ->
+ ok
+ catch
+ error:undef ->
+ io:format("Undef error ~n"),
+ handle(Req, Method, T, Path);
+ error:function_clause ->
+ io:format("Function clause error ~n"),
+ handle(Req, Method, T, Path);
+ Status when is_number(Status)->
+ Req:respond(Status);
+ {Status, Template} when is_number(Status) and is_list(Template)->
+ Req:respond(Status, Template);
+ {Status, Headers, Template} when is_number(Status) and is_list(Headers) and is_list(Template)->
+ Req:respond(Status, Headers, Template)
+ end.
+
+handle(Req, 'GET', Path)->
+ handle(Req, get, Path);
+handle(Req, 'POST', Path)->
+ handle(Req, post, Path);
+handle(Req, 'PUT', Path)->
+ handle(Req, put, Path);
+handle(Req, 'DELETE', Path)->
+ handle(Req, delete, Path);
+handle(Req, 'HEAD', Path)->
+ handle(Req, head, Path);
+handle(Req, Method, Handlers)->
+ io:format("Handling method ~p with handlers ~p ~n", [Method, Handlers]),
+ Path = Req:resource([lowercase, urldecode]),
+ handle(Req, Method, Handlers, Path).
132 test/spooky_test.erl
@@ -0,0 +1,132 @@
+-module(spooky_test).
+
+-compile(export_all).
+-include_lib("eunit/include/eunit.hrl").
+-include("../src/misultin/include/misultin.hrl").
+
+
+request()->
+ {misultin_req, #req{uri={type, "/"}, method='GET'}, self()}.
+
+request(Method)->
+ {misultin_req, #req{uri={type, "/"}, method=Method}, self()}.
+
+request(Method, Uri)->
+ {misultin_req, #req{uri={type, Uri}, method=Method}, self()}.
+
+
+response(StatusCode)->
+ receive
+ {response, Status, _, _} ->
+ ?assert(Status =:= StatusCode)
+ after
+ 200 ->
+ ?assert(timeout)
+ end.
+
+response(StatusCode, Template)->
+ receive
+ {response, Status, _, Response} when is_list(Response)->
+ ?assert(Status =:= StatusCode),
+ ?assert(string:equal(Template, Response))
+ after
+ 200 ->
+ ?assert(timeout)
+ end.
+
+response(StatusCode, Headers, Template)->
+ receive
+ {response, Status, ResponseHeaders, Response} when is_list(Response)->
+ ?assert(Status =:= StatusCode),
+ ?assert(string:equal(Template, Response)),
+ ?assert(Headers =:= ResponseHeaders)
+ after
+ 200 ->
+ ?assert(timeout)
+ end.
+
+
+stop()->
+ true = spooky:stop().
+
+start_atom_test()->
+ {ok, _Pid} = spooky:start_link(spooky_hello_world),
+ stop().
+
+start_list_test()->
+ {ok, _Pid} = spooky:start_link([spooky_get_hello_world, spooky_post_hello_world]),
+ stop().
+
+get_test()->
+ spooky:start_link(spooky_get_hello_world),
+ spooky_server:handle(request()),
+ response(200),
+ spooky_server:handle(request('GET', "/androids")),
+ response(200),
+ spooky_server:handle(request('GET', "/androids/sheep")),
+ response(404),
+ spooky_server:handle(request('POST', "/androids")),
+ response(404),
+ stop().
+
+post_test()->
+ spooky:start_link(spooky_post_hello_world),
+ spooky_server:handle(request('POST')),
+ response(200),
+ spooky_server:handle(request('POST', "/androids")),
+ response(200),
+ spooky_server:handle(request('POST', "/androids/sheep")),
+ response(404),
+ spooky_server:handle(request('GET', "/androids")),
+ response(404),
+ stop().
+
+put_test()->
+ spooky:start_link(spooky_hello_world),
+ spooky_server:handle(request('PUT')),
+ response(200),
+ spooky_server:handle(request('PUT', "/androids")),
+ response(200),
+ spooky_server:handle(request('PUT', "/androids/sheep")),
+ response(404),
+ stop().
+
+delete_test()->
+ spooky:start_link(spooky_hello_world),
+ spooky_server:handle(request('DELETE')),
+ response(200),
+ spooky_server:handle(request('DELETE', "/androids")),
+ response(200),
+ spooky_server:handle(request('DELETE', "/androids/sheep")),
+ response(404),
+ stop().
+
+head_test()->
+ spooky:start_link(spooky_hello_world),
+ spooky_server:handle(request('HEAD')),
+ response(200),
+ spooky_server:handle(request('HEAD', "/androids")),
+ response(200),
+ spooky_server:handle(request('HEAD', "/androids/sheep")),
+ response(404),
+ stop().
+
+multi_test()->
+ spooky:start_link(spooky_multi_hello_world),
+ spooky_server:handle(request()),
+ response(200),
+ spooky_server:handle(request('POST')),
+ response(200),
+ spooky_server:handle(request('PUT')),
+ response(404),
+ stop().
+
+errors_test()->
+ spooky:start_link(spooky_hello_world),
+ spooky_server:handle(request('GET', "/smashingpumpkins")),
+ response(418),
+ spooky_server:handle(request('POST', "/smashingpumpkins")),
+ response(418, "I'm a teapot."),
+ spooky_server:handle(request('PUT', "/smashingpumpkins")),
+ response(418, [{"X-Server", "Teapot"}], "I'm a teapot."),
+ stop().

0 comments on commit 3e0bf61

Please sign in to comment.
Something went wrong with that request. Please try again.