Permalink
Browse files

Initial commit of Chicago Boss.

Includes a "Hello, World!" application.
  • Loading branch information...
0 parents commit d39a1b7a0d7af505de6dac2a8cdbdcdf3faa6f43 Evan Miller committed Sep 28, 2009
5 Controller/hello_controller.erl
@@ -0,0 +1,5 @@
+-module(hello_controller).
+-compile(export_all).
+
+world(Req) ->
+ {ok, [{world, "World"}]}.
5 Emakefile
@@ -0,0 +1,5 @@
+{"src/*", [debug_info, {outdir, "ebin"}]}.
+{"src/drivers/*", [debug_info, {outdir, "ebin"}]}.
+{"deps/erlydtl/src/erlydtl/*", [debug_info, {outdir, "deps/erlydtl/ebin"}]}.
+{"deps/medici/src/*", [debug_info, {outdir, "deps/medici/ebin"}]}.
+{"deps/mochiweb/src/*", [debug_info, {outdir, "deps/mochiweb/ebin"}]}.
27 Makefile
@@ -0,0 +1,27 @@
+ERL=erl
+ERLC=erlc
+DB_APP=boss_db.app
+APP=boss.app
+APPLICATION=boss
+DEPS = erlydtl mochiweb medici
+
+all: ebin/$(APP) ebin/$(DB_APP)
+ $(ERL) -make
+# -for d in $(DEPS); do (cd deps/$$d; $(MAKE) all) done
+
+ebin/$(DB_APP): src/$(DB_APP)
+ -mkdir -p ebin
+ cp $< $@
+
+ebin/$(APP): src/$(APP)
+ -mkdir -p ebin
+ cp $< $@
+
+clean:
+ rm -fv ebin/*.beam
+ -for d in $(DEPS); do (cd deps/$$d; $(MAKE) clean) done
+ rm -fv ebin/$(APP)
+
+edoc:
+ $(ERL) -noshell -eval "edoc:application($(APPLICATION), \".\", [])" \
+ -s init stop
2 Model/greeting.erl
@@ -0,0 +1,2 @@
+-module(greeting, [Id, GreetingText]).
+-compile(export_all).
275 README
@@ -0,0 +1,275 @@
+= GETTING STARTED WITH CHICAGO BOSS =
+
+Build with "make"
+
+You will need:
+* Tokyo Tyrant running a Table database.
+* Erlang R13. (Check your version with erlang:system_info(otp_release) ).
+
+Open up boss.config to set your database info and the port you want to run the server on. When you're ready to rock and roll, run ./start-dev.sh in this directory. There will be a lot of PROGRESS REPORTs which look scary but hopefully everything is running smoothly. With this console you can interact directly with the running server. Next, point your browser to:
+
+http://localhost:8001/hello/world
+
+If all is well you will see "Hello, World!" Now you can get busy.
+
+
+= THE CHICAGO BOSS API: MODEL, VIEW, CONTROLLER, AND DATABASE =
+
+== The Model Directory: BossRecords ==
+
+BossRecords are specially compiled parameterized modules. Important aspects of BossRecords:
+
+ * The first parameter of a BossRecord should always be called Id, and the other parameters should be CamelCased attributes of your data model.
+
+ * All parameters will be available as lower-case, underscored functions, e.g. -module(foo, [Id, TheText]) will generate the getter functions id() and the_text(), and the setters id(NewId) and the_text(NewText). Note that setters do not save the BossRecord.
+
+ * To auto-generate an ID, pass the atom 'id' as the first parameter to "new".
+
+ * Call "new" with strings for all other parameters
+
+ * Parameters that end in "Time" (e.g., CreationTime, UpdateTime) should be passed either erlang:now() or a datetime tuple.
+
+Generated instance functions of a BossRecord include:
+
+ save() -> BossRecord
+
+ Saves the BossRecord to the database. The returned record will have an auto-generated ID if the record's ID was set to 'id'.
+
+ attribute_names() -> [atom()]
+
+ A list of the lower-case BossRecord parameters, e.g. [id, the_text].
+
+ attributes() -> [{atom(), string() | undefined}]
+
+ A proplist of the BossRecord parameters and their values.
+
+ attributes(Proplist) -> BossRecord
+
+ Set multiple record attributes at once. Does not save the record.
+
+ incr(CounterName::atom()) -> integer()
+
+ Atomically increment CounterName by 1. Requires at least one "-counter" attribute, see below.
+
+ incr(CounterName::atom(), Increment::integer()) -> integer()
+
+ Atomically increment CounterName by Increment. Requires at least one "-counter" attribute, see below.
+
+ reset(CounterName::atom()) -> 0
+
+ Reset the value of CounterName to 0. Requires at least one "-counter" attribute, see below.
+
+
+Special associations are generated from the following module attributes:
+
+ -belongs_to(foo).
+
+ Requires a matching FooId in the parameter list. Adds a function foo() which returns the foo BossRecord with ID equal to the current BossRecord's FooId.
+
+ -has_many(bars).
+
+ Generates a function bars() which returns a list all "bar" BossRecords with FooId equal to this BossRecord's ID.
+
+The two above attributes work similar to belongs_to and has_many in Rails. More special attributes:
+
+ -has_up_to({Limit::integer(), bars}).
+ -has_up_to({Limit::integer(), bars, Sort::atom()}).
+ -has_up_to({Limit::integer(), bars, Sort::atom(), SortOrder}).
+
+ SortOrder = str_ascending | str_descending | num_ascending | num_descending
+
+ Generates a function bars() which returns a list of up to Limit "bar" BossRecords with FooId equal to this BossRecord's ID. Optional args:
+
+ Sort: the record attribute to sort the association on
+ SortOrder: how to sort the association
+
+ Note that Time attributes are stored internally as integers, so sort them with num_ascending or num_descending.
+
+ -counter(foo_counter).
+
+ Generates a function foo_counter() which returns the value of the counter, initialized to zero. Each BossRecord may have an unlimited number of counters. Manipulate the counters with "reset" and "incr" above.
+
+SPECIAL NOTE: Everything in the Model directory will be compiled as a BossRecord rather than as a regular Erlang module; you don't need to do or declare anything special. The example at the end of this file should make things clear.
+
+
+== The View Directory: ErlyDTL Templates ==
+
+All files in the View subdirectories are Django Templates. The files should end in .html. Each file is associated with a function of a particular controller (see below). Variables referred to in the View files may be BossRecords, binaries, strings, integers, iolists, gb_hashes, proplists, dicts, or old-fashioned parameterized modules. See the ErlyDTL page for examples and details of how templates work:
+
+ http://code.google.com/p/erlydtl/
+
+
+== The Controller Directory: The BossController API ==
+
+Each URL is associated with a function of a controller. Each controller module should end with "_controller.erl". The URL /foo/bar will call the function foo_controller:bar/1. The one and only argument passed to the function is a Mochiweb request object. The function should return with one of the following:
+
+ ok
+
+ The template will be rendered without any variables.
+
+ {ok, Variables::proplist()}
+
+ Variables will be passed into the associated ErlyDTL template.
+
+ {ok, Variables::proplist(), Headers::proplist()}
+
+ Variables will be passed into the associated ErlyDTL template. Headers are HTTP headers you want to set, but the only allowed one right now is Content-Type (defaults to "text/html").
+
+ {redirect, Location::string()}
+
+ Perform a 302 HTTP redirect to Location.
+
+ {redirect, Location::string(), Headers::proplist()}
+
+ Perform a 302 HTTP redirect to Location and set additional HTTP Headers.
+
+ {action_other, OtherLocation}
+
+ OtherLocation = {ControllerName::atom(), ActionName::atom()}
+
+ Execute the specified controller action, but without performing an HTTP redirect.
+
+ {render_other, OtherLocation}
+
+ OtherLocation = {ControllerName::atom(), ActionName::atom()}
+
+ Render the view from OtherLocation, but don't actually execute the associated controller action.
+
+ {render_other, OtherLocation, Variables}
+
+ OtherLocation = {ControllerName::atom(), ActionName::atom()}
+
+ Render the view from OtherLocation using Variables, but don't actually execute the associated controller action.
+
+
+If a controller exports a function called before_filter/1, then before executing an action in the controller, Chicago Boss will pass the action name as an atom to before_filter/1. before_filter should return a fun that takes a Mochiweb request object as its only argument and (in turn) returns one of the following:
+
+ ok
+
+ Execute the original action as normal
+
+ {ok, ExtraInfo}
+
+ Execute the original action, but pass ExtraInfo as the second argument to the controller function (the first argument is still the Mochiweb request object).
+
+ {redirect, Location}
+
+ Perform a 302 redirect to Location (a string).
+
+Probably most common before_filter looks like:
+
+ before_filter(_) ->
+ fun user_controller:require_login/1.
+
+
+
+Useful functions in the Mochiweb request object include:
+
+ get_qs_value( Key::string() ) -> string() | undefined
+
+ Get the value of a given query string parameter (e.g. "?id=1234")
+
+ get_post_value( Key::string() ) -> string() | undefined
+
+ Get the value of a given POST parameter
+
+ get_header_value( Key::string() ) -> string() | undefined
+
+ Get the value of a given HTTP request header
+
+ get_cookie_value( Key::string() ) -> string() | undefined
+
+ Get the value of a given cookie.
+
+ get(method) -> atom()
+
+ Get the HTTP method ('GET', 'POST', etc.)
+
+
+== Querying the Database: BossDB ==
+
+To interact more directly with the database, the following functions are available:
+
+ boss_db:find( Id::string() ) -> BossRecord
+
+ Find a BossRecord with the specified Id.
+
+ boss_db:find( Type::atom(), Conditions, Max::integer() ) -> [ BossRecord ]
+ boss_db:find( Type::atom(), Conditions, Max::integer(), Skip::integer() ) -> [ BossRecord ]
+ boss_db:find( Type::atom(), Conditions, Max::integer(), Skip::integer(), Sort::atom(), ) -> [ BossRecord ]
+ boss_db:find( Type::atom(), Conditions, Max::integer(), Skip::integer(), Sort::atom(), SortOrder ) -> [ BossRecord ]
+
+ Conditions = [{Attribute::atom(), Value::string()}]
+ SortOrder = str_ascending | str_descending | num_ascending | num_descending
+
+ Query for a list of up to Max number of BossRecords of type Type that exactly match all of the given conditions (attribute = value). Optional args:
+
+ Skip: number of search results to skip
+ Sort: the record attribute to sort on
+ SortOrder: how to sort the results
+
+ Note that Time attributes are stored internally as integers, so sort them with num_ascending or num_descending.
+
+ boss_db:delete( Id::string() ) -> ok | {error, Reason}
+
+ Delete the BossRecord with the given Id.
+
+ boss_db:save_record(BossRecord) -> SavedBossRecord
+
+ Save (that is, create or update) the given BossRecord in the database.
+
+ boss_db:counter( Id::string() ) -> integer()
+
+ Treat the record associated with Id as a counter and return its value. Returns 0 if the record does not exist, so to reset a counter just use "delete".
+
+ boss_db:incr( Id::string() ) -> integer()
+
+ Treat the record associated with Id as a counter and atomically increment its value.
+
+ boss_db:incr( Id::string(), Increment::integer() ) -> integer()
+
+ Treat the record associated with Id as a counter and atomically increment its value by Increment.
+
+
+
+= A DECEPTIVELY SIMPLE EXAMPLE =
+
+Model:
+
+ Model/blog_post.erl:
+
+ -module(blog_post, [Id, Title, Text, AuthorId]).
+ -compile(export_all).
+ -belongs_to(author).
+
+ Model/author.erl:
+
+ -module(author, [Id, Name]).
+ -compile(export_all).
+ -has_up_to({100, blog_posts}).
+
+ number_of_blog_posts() ->
+ length(blog_posts()).
+
+Controller:
+
+ Controller/blog_post_controller.erl:
+
+ -module(blog_post_controller).
+ -compile(export_all).
+
+ generate_and_show(Req) ->
+ Author = author:new(id, <<"My Name">>),
+ SavedAuthor = Author:save(),
+ BlogPost = blog_post:new(id, <<"My Title">>, <<"My Text">>, SavedAuthor:id()),
+ {ok, [{blog_post, BlogPost:save()}]}.
+
+View:
+
+ View/blog_post/generate_and_show.html:
+
+ <h1>{{ blog_post.title }}</h1>
+
+ By {{ blog_post.author.name }} (Post # {{ blog_post.author.number_of_blog_posts }})
+
+ {{ blog_post.title }}
3 TODO
@@ -0,0 +1,3 @@
+* Test coverage
+* URL routes
+* Other DB drivers
1 View/hello/world.html
@@ -0,0 +1 @@
+Hello, {{ world }}!
9 boss.config
@@ -0,0 +1,9 @@
+[{boss, [
+ {db_host, "localhost"},
+ {db_port, 1978},
+ {db_driver, boss_db_driver_tyrant},
+ {log_file, "log/boss_error.log"},
+ {default_controller, forum},
+ {default_action, index},
+ {port, 8001}
+]}].
12 src/boss.app
@@ -0,0 +1,12 @@
+{application, boss,
+ [{description, "boss"},
+ {vsn, "0.01"},
+ {modules, [
+ boss,
+ boss_app,
+ boss_controller
+ ]},
+ {registered, []},
+ {mod, {boss_app, []}},
+ {env, []},
+ {applications, [kernel, stdlib, crypto]}]}.
29 src/boss.erl
@@ -0,0 +1,29 @@
+%% @author Evan Miller <emmiller@gmail.com>
+%% @copyright 2009 author.
+
+%% @doc TEMPLATE.
+
+-module(boss).
+-author('Evan Miller <emmiller@gmail.com>').
+-export([start/0, stop/0]).
+
+ensure_started(App) ->
+ case application:start(App) of
+ ok ->
+ ok;
+ {error, {already_started, App}} ->
+ ok
+ end.
+
+%% @spec start() -> ok
+%% @doc Start the boss server.
+start() ->
+ ensure_started(crypto),
+ application:start(boss).
+
+%% @spec stop() -> ok
+%% @doc Stop the boss server.
+stop() ->
+ Res = application:stop(boss),
+ application:stop(crypto),
+ Res.
21 src/boss_app.erl
@@ -0,0 +1,21 @@
+%% @author Evan Miller <emmiller@gmail.com>
+%% @copyright YYYY author.
+
+%% @doc Callbacks for the rr application.
+
+-module(boss_app).
+-author('Evan Miller <emmiller@gmail.com>').
+
+-behaviour(application).
+-export([start/2,stop/1]).
+
+
+%% @spec start(_Type, _StartArgs) -> ServerRet
+%% @doc application start callback for rr.
+start(_Type, _StartArgs) ->
+ boss_sup:start_link().
+
+%% @spec stop(_State) -> ServerRet
+%% @doc application stop callback for rr.
+stop(_State) ->
+ ok.
305 src/boss_controller.erl
@@ -0,0 +1,305 @@
+-module(boss_controller).
+-export([mochiweb_request/1, start/0, start/1, stop/0, render_view/2, process_request/1]).
+
+start() ->
+ start([]).
+
+start(Config) ->
+ {ok, DBPort} = application:get_env(db_port),
+ {ok, DBDriver} = application:get_env(db_driver),
+ {ok, DBHost} = application:get_env(db_host),
+ {ok, LogFile} = application:get_env(log_file),
+ boss_db:start([ {port, DBPort}, {driver, DBDriver}, {host, DBHost} ]),
+ {ok, boss_error_log} = disk_log:open([{name, boss_error_log}, {file, LogFile}]),
+ mochiweb_http:start([{loop, fun(Req) -> mochiweb_request(Req) end} | Config]).
+
+stop() ->
+ disk_log:close(boss_error_log),
+ boss_db:stop(),
+ mochiweb_http:stop().
+
+mochiweb_request(Req) ->
+ Req:respond(process_request(Req)).
+
+process_request(Req) ->
+ Result = case parse_path(Req:get(path)) of
+ {ok, {Controller, Action}} ->
+ trap_load_and_execute({Controller, Action}, Req);
+ Else ->
+ Else
+ end,
+ process_result(Result).
+
+parse_path("/") ->
+ {ok, Controller} = application:get_env(default_controller),
+ {ok, Action} = application:get_env(default_action),
+ {ok, {Controller, Action}};
+parse_path("/" ++ Url) ->
+ Tokens = string:tokens(Url, "/"),
+ case length(Tokens) of
+ 2 ->
+ {ok, {list_to_atom(lists:nth(1, Tokens)), list_to_atom(lists:nth(2, Tokens))}};
+ _ ->
+ {not_found, "File not found"}
+ end;
+parse_path(_) ->
+ {not_found, "File not found"}.
+
+process_result({error, Payload}) ->
+ disk_log:balog(boss_error_log, list_to_binary(format_now(erlang:now()) ++
+ " Error : "++io_lib:print(Payload)++"\n\n")),
+ {500, [{"Content-Type", "text/html"}], "Error: <pre>" ++ io_lib:print(Payload) ++ "</pre>"};
+process_result({not_found, Payload}) ->
+ {404, [{"Content-Type", "text/html"}], Payload};
+process_result({redirect, Where, Headers}) ->
+ {302, Headers ++ [{"Location", Where}], ""};
+process_result({redirect, Where}) ->
+ {302, [{"Location", Where}], ""};
+process_result({ok, Payload, Headers}) ->
+ {200, proplists:delete("Content-Type", Headers) ++
+ [{"Content-Type", proplists:get_value("Content-Type", Headers, "text/html")}],
+ Payload};
+process_result({ok, Payload}) ->
+ {200, [{"Content-Type", "text/html"}], Payload}.
+
+trap_load_and_execute(Arg1, Arg2) ->
+ case catch load_and_execute(Arg1, Arg2) of
+ {'EXIT', Reason} ->
+ {error, Reason};
+ Ok ->
+ Ok
+ end.
+
+load_and_execute({doc, ModelName}, _Req) ->
+ case load_models() of
+ ok ->
+ {ModelName, Edoc} = boss_record_compiler:edoc_module(
+ model_path(atom_to_list(ModelName)++".erl"), [{private, true}]),
+ {ok, edoc:layout(Edoc)};
+ Error ->
+ Error
+ end;
+load_and_execute({Controller, Action}, Req) ->
+ case load_controller(Controller) of
+ ok ->
+ case load_models() of
+ ok ->
+ execute_action({Controller, Action}, Req);
+ Else ->
+ Else
+ end;
+ Else ->
+ Else
+ end.
+
+execute_action(Location, Req) ->
+ execute_action(Location, Req, []).
+
+execute_action({Controller, Action} = Location, Req, LocationTrail) ->
+ Module = list_to_atom(lists:concat([Controller, "_controller"])),
+ case lists:member(Location, LocationTrail) of
+ true ->
+ {error, "Circular redirect!"};
+ _ ->
+ BeforeFilter = case lists:member({before_filter, 1}, Module:module_info(exports)) of
+ true ->
+ case Module:before_filter(Action) of
+ ok -> ok;
+ Function when is_function(Function) -> Function(Req)
+ end;
+ false ->
+ ok
+ end,
+ case BeforeFilter of
+ ok ->
+ case lists:member({Action, 1}, Module:module_info(exports)) of
+ true -> process_action_result({Location, Req, LocationTrail},
+ Module:Action(Req));
+ _ -> render_view(Location)
+ end;
+ {ok, Info} ->
+ case lists:member({Action, 2}, Module:module_info(exports)) of
+ true -> process_action_result({Location, Req, LocationTrail},
+ Module:Action(Req, Info));
+ _ -> render_view(Location)
+ end;
+ Other ->
+ Other
+ end
+ end.
+
+process_action_result(Info, ok) ->
+ process_action_result(Info, {ok, []});
+process_action_result(Info, {ok, Data}) ->
+ process_action_result(Info, {ok, Data, []});
+process_action_result({Location, _, _}, {ok, Data, Headers}) ->
+ render_view(Location, Data, Headers);
+
+process_action_result(Info, {render_other, OtherLocation}) ->
+ process_action_result(Info, {render_other, OtherLocation, []});
+process_action_result(_, {render_other, OtherLocation, Data}) ->
+ render_view(OtherLocation, Data);
+
+process_action_result({_, Req, LocationTrail}, {action_other, OtherLocation}) ->
+ execute_action(OtherLocation, Req, [OtherLocation | LocationTrail]);
+process_action_result(_, Else) ->
+ Else.
+
+compile_controller(Module, ModulePath) ->
+ CompileResult = compile:file(filename:rootname(ModulePath),
+ [{outdir, filename:join([root_dir(), "ebin"])}, return_errors]),
+ case CompileResult of
+ {ok, _} ->
+ code:purge(Module),
+ {module, Module} = code:load_file(Module),
+ ok;
+ {error, ErrorList, WarningList} ->
+ {error, ["Failed to compile " ++ ModulePath ++ ". ", ErrorList, WarningList]}
+ end.
+
+compile_view(Controller, Template) ->
+ erlydtl_compiler:compile(
+ view_path(Controller, Template),
+ view_module(Controller, Template),
+ [{doc_root, view_path(Controller)}, {compiler_options, []}]).
+
+compile_model(ModulePath) ->
+ boss_record_compiler:compile(ModulePath).
+
+load_controller(ModuleName) ->
+ Module = list_to_atom(lists:concat([ModuleName, "_controller"])),
+ case module_older_than(Module, [controller_path(Module)]) of
+ true ->
+ compile_controller(Module, controller_path(Module));
+ _ ->
+ ok
+ end.
+
+load_models() ->
+ {ok, Models} = file:list_dir(model_path()),
+ ErrorList = lists:foldl(
+ fun(Path, Errors) ->
+ case filename:basename(Path, ".erl") of
+ "." ++ _ ->
+ Errors;
+ ModuleName ->
+ Module = list_to_atom(ModuleName),
+ AbsPath = model_path(Path),
+ case module_older_than(Module, [AbsPath]) of
+ true ->
+ case compile_model(AbsPath) of
+ ok ->
+ Errors;
+ {error, Error} ->
+ [Error | Errors];
+ {error, NewErrors, _NewWarnings} when is_list(NewErrors) ->
+ NewErrors ++ Errors
+ end;
+ _ ->
+ Errors
+ end
+ end
+ end, [], Models),
+ case length(ErrorList) of
+ 0 ->
+ ok;
+ _ ->
+ {error, ErrorList}
+ end.
+
+render_view(Location) ->
+ render_view(Location, []).
+
+render_view(Location, Variables) ->
+ render_view(Location, Variables, []).
+
+render_view({Controller, Template}, Variables, Headers) ->
+ Module = view_module(Controller, Template),
+ Result = case module_is_loaded(Module) of
+ true ->
+ case module_older_than(Module, lists:map(fun
+ ({File, _CheckSum}) ->
+ File;
+ (File) ->
+ File
+ end, [Module:source() | Module:dependencies()])) of
+ true ->
+ compile_view(Controller, Template);
+ false ->
+ ok
+ end;
+ false ->
+ compile_view(Controller, Template)
+ end,
+ case Result of
+ ok ->
+ case Module:render(Variables) of
+ {ok, Payload} ->
+ {ok, Payload, Headers};
+ Err ->
+ Err
+ end;
+ Err ->
+ Err
+ end.
+
+module_is_loaded(Module) ->
+ case code:is_loaded(Module) of
+ {file, _} ->
+ true;
+ _ ->
+ false
+ end.
+
+module_older_than(Module, Files) when is_atom(Module) ->
+ case code:is_loaded(Module) of
+ {file, Loaded} ->
+ module_older_than(Loaded, Files);
+ _ ->
+ case code:load_file(Module) of
+ {module, _} ->
+ case code:is_loaded(Module) of
+ {file, Loaded} ->
+ module_older_than(Loaded, Files)
+ end;
+ {error, _} ->
+ true
+ end
+ end;
+
+module_older_than(Module, Files) when is_list(Module) ->
+ case filelib:last_modified(Module) of
+ 0 ->
+ true;
+ CompileDate ->
+ module_older_than(CompileDate, Files)
+ end;
+
+module_older_than(_Date, []) ->
+ false;
+
+module_older_than(CompileDate, [File|Rest]) ->
+ CompileSeconds = calendar:datetime_to_gregorian_seconds(CompileDate),
+ ModificationSeconds = calendar:datetime_to_gregorian_seconds(
+ filelib:last_modified(File)),
+ (ModificationSeconds >= CompileSeconds) orelse module_older_than(CompileDate, Rest).
+
+view_module(Controller, Template) ->
+ list_to_atom(lists:concat([Controller, "_view_", Template])).
+
+root_dir() -> filename:join([filename:dirname(code:which(?MODULE)), ".."]).
+
+view_path() -> filename:join([root_dir(), "View"]).
+view_path(Controller) -> filename:join([view_path(), Controller]).
+view_path(Controller, Template) -> filename:join([view_path(Controller), lists:concat([Template, ".html"])]).
+
+model_path() -> filename:join([root_dir(), "Model"]).
+model_path(Model) -> filename:join([model_path(), Model]).
+
+controller_path() -> filename:join([root_dir(), "Controller"]).
+controller_path(Module) -> filename:join([controller_path(), lists:concat([Module, ".erl"])]).
+
+format_now(Time) ->
+ {{Year, Month, Day}, {Hour, Minute, Second}} = calendar:now_to_local_time(Time),
+ integer_to_list(Year) ++ "." ++ integer_to_list(Month) ++ "." ++ integer_to_list(Day) ++
+ " "++integer_to_list(Hour)++":"++integer_to_list(Minute)++":"++integer_to_list(Second).
13 src/boss_db.app
@@ -0,0 +1,13 @@
+{application, boss_db,
+ [{description, "BossDB Database Layer"},
+ {vsn, "0.01"},
+ {modules, [
+ boss_db,
+ boss_db_app,
+ boss_db_controller,
+ boss_db_sup
+ ]},
+ {registered, []},
+ {mod, {boss_db_app, []}},
+ {env, []},
+ {applications, [kernel, stdlib]}]}.
95 src/boss_db.erl
@@ -0,0 +1,95 @@
+%% @doc Chicago Boss database abstraction
+
+-module(boss_db).
+
+-export([start/0, start/1, stop/0]).
+
+-export([find/1, find/3, find/4, find/5, find/6]).
+
+-export([counter/1, incr/1, incr/2, delete/1, save_record/1, type/1]).
+
+start() ->
+ start([]).
+
+start(_Options) ->
+ application:start(boss_db).
+
+stop() ->
+ application:stop(boss_db).
+
+%% @spec find(Id) -> {error, Reason} | BossRecord
+%% @doc Find a BossRecord with the specified Id.
+find(Key) ->
+ gen_server:call(boss_db, {find, Key}).
+
+%% @spec find(Type::atom(), Conditions, Max::integer()) -> [ BossRecord ]
+%% Conditions = [{Key::atom(), Value::string()}]
+%% @doc Query for BossRecords. Returns up to Max number of BossRecords of type
+%% Type matching all of the given Conditions (attribute = value).
+find(Type, Conditions, Max) ->
+ gen_server:call(boss_db, {find, Type, Conditions, Max}).
+
+%% @spec find( Type::atom(), Conditions, Max::integer(), Skip::integer() ) -> [ BossRecord ]
+%% Conditions = [{Key::atom(), Value::string()}]
+%% @doc Query for BossRecords. Returns up to Max number of BossRecords of type
+%% Type matching all of the given Conditions (attribute = value), skipping the
+%% first Skip results.
+find(Type, Conditions, Max, Skip) ->
+ gen_server:call(boss_db, {find, Type, Conditions, Max, Skip}).
+
+%% @spec find( Type::atom(), Conditions, Max::integer(), Skip::integer(), Sort::atom() ) -> [ BossRecord ]
+%% Conditions = [{Key::atom(), Value::string()}]
+%% @doc Query for BossRecords. Returns up to Max number of BossRecords of type
+%% Type matching all of the given Conditions (attribute = value), skipping the
+%% first Skip results, sorted on the attribute Sort.
+find(Type, Conditions, Max, Skip, Sort) ->
+ gen_server:call(boss_db, {find, Type, Conditions, Max, Skip, Sort}).
+
+%% @spec find( Type::atom(), Conditions, Max::integer(), Skip::integer(), Sort::atom(), SortOrder ) -> [ BossRecord ]
+%% Conditions = [{Key::atom(), Value::string()}]
+%% SortOrder = num_ascending | num_descending | str_ascending | str_descending
+%% @doc Query for BossRecords. Returns up to Max number of BossRecords of type
+%% Type matching all of the given Conditions (attribute = value), skipping the
+%% first Skip results, sorted on the attribute Sort. SortOrder specifies whether
+%% to treat values as strings or as numbers, and whether to sort ascending or
+%% descending.
+%%
+%% Note that Time attributes are stored internally as numbers, so you should sort them numerically.
+
+find(Type, Conditions, Max, Skip, Sort, SortOrder) ->
+ gen_server:call(boss_db, {find, Type, Conditions, Max, Skip, Sort, SortOrder}).
+
+%% @spec counter( Id::string() ) -> integer()
+%% @doc Treat the record associated with Id as a counter and return its value.
+%% Returns 0 if the record does not exist, so to reset a counter just use
+%% "delete".
+counter(Key) ->
+ gen_server:call(boss_db, {counter, Key}).
+
+%% @spec incr( Id::string() ) -> integer()
+%% @doc Treat the record associated with Id as a counter and atomically increment its value by 1.
+incr(Key) ->
+ gen_server:call(boss_db, {incr, Key}).
+
+%% @spec incr( Id::string(), Increment::integer() ) -> integer()
+%% @doc Treat the record associated with Id as a counter and atomically increment its value by Increment.
+incr(Key, Count) ->
+ gen_server:call(boss_db, {incr, Key, Count}).
+
+%% @spec delete( Id::string() ) -> ok | {error, Reason}
+%% @doc Delete the BossRecord with the given Id.
+delete(Key) ->
+ gen_server:call(boss_db, {delete, Key}).
+
+%% @spec save_record( BossRecord ) -> SavedBossRecord
+%% @doc Save (that is, create or update) the given BossRecord in the database.
+save_record(Record) ->
+ gen_server:call(boss_db, {save_record, Record}).
+
+%% @spec type( Id::string() ) -> Type::atom()
+%% @doc Returns the type of the BossRecord with Id, or undefined if the record does not exist.
+type(Key) ->
+ case find(Key) of
+ {error, _} -> undefined;
+ Record -> element(1, Record)
+ end.
10 src/boss_db_app.erl
@@ -0,0 +1,10 @@
+-module(boss_db_app).
+-behaviour(application).
+
+-export([start/2, stop/1]).
+
+start(_Type, StartArgs) ->
+ boss_db_sup:start_link(StartArgs).
+
+stop(_State) ->
+ ok.
70 src/boss_db_controller.erl
@@ -0,0 +1,70 @@
+-module(boss_db_controller).
+
+-behaviour(gen_server).
+
+-export([start_link/0]).
+
+-export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]).
+
+-record(state, {driver}).
+
+start_link() ->
+ gen_server:start_link({local, boss_db}, ?MODULE, [], []).
+
+init(Options) ->
+ Driver = proplists:get_value(driver, Options, boss_db_driver_tyrant),
+ ok = Driver:start(),
+ {ok, #state{driver = Driver}}.
+
+handle_call({find, Key}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:find(Key), State};
+
+handle_call({find, Type, Conditions, Max}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:find(Type, Conditions, Max), State};
+
+handle_call({find, Type, Conditions, Max, Skip}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:find(Type, Conditions, Max, Skip), State};
+
+handle_call({find, Type, Conditions, Max, Skip, Sort}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:find(Type, Conditions, Max, Skip, Sort), State};
+
+handle_call({find, Type, Conditions, Max, Skip, Sort, SortOrder}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:find(Type, Conditions, Max, Skip, Sort, SortOrder), State};
+
+handle_call({counter, Counter}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:counter(Counter), State};
+
+handle_call({incr, Key}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:incr(Key), State};
+
+handle_call({incr, Key, Count}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:incr(Key, Count), State};
+
+handle_call({delete, Id}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:delete(Id), State};
+
+handle_call({save_record, Record}, _From, State) ->
+ Driver = State#state.driver,
+ {reply, Driver:save_record(Record), State}.
+
+handle_cast(_Request, State) ->
+ {noreply, State}.
+
+terminate(_Reason, State) ->
+ Driver = State#state.driver,
+ Driver:stop().
+
+code_change(_OldVsn, State, _Extra) ->
+ {ok, State}.
+
+handle_info(_Info, State) ->
+ {noreply, State}.
7 src/boss_db_driver.erl
@@ -0,0 +1,7 @@
+-module(boss_db_driver).
+-export([behaviour_info/1]).
+
+behaviour_info(callbacks) ->
+ [{find, 1}, {find, 2}, {delete, 1}, {save_record, 1}];
+behaviour_info(_Other) ->
+ undefined.
139 src/boss_db_driver_tyrant.erl
@@ -0,0 +1,139 @@
+-module(boss_db_driver_tyrant).
+-behaviour(boss_db_driver).
+-export([start/0, stop/0, find/1, find/3, find/4, find/5, find/6]).
+-export([counter/1, incr/1, incr/2, delete/1, save_record/1]).
+
+start() ->
+ medici:start().
+
+stop() ->
+ medici:stop().
+
+find(Id) when is_list(Id) ->
+ find(list_to_binary(Id));
+
+find(Id) when is_binary(Id) ->
+ Type = infer_type_from_id(Id),
+ case medici:get(Id) of
+ Record when is_list(Record) ->
+ case model_is_loaded(Type) of
+ true ->
+ activate_record(Record, Type);
+ false ->
+ {error, {module_not_loaded, Type}}
+ end;
+ {error, Reason} ->
+ {error, Reason}
+ end;
+
+find(_Id) -> {error, invalid_id}.
+
+find(Type, Conditions, Max) when is_atom(Type) and is_list(Conditions) and is_integer(Max) ->
+ find(Type, Conditions, Max, 0).
+
+find(Type, Conditions, Max, Skip) ->
+ find(Type, Conditions, Max, Skip, primary).
+
+find(Type, Conditions, Max, Skip, Sort) ->
+ find(Type, Conditions, Max, Skip, Sort, str_ascending).
+
+find(Type, Conditions, Max, Skip, Sort, SortOrder) ->
+ case model_is_loaded(Type) of
+ true ->
+ Query = build_query(Type, Conditions, Max, Skip, Sort, SortOrder),
+ lists:map(fun({_Id, Record}) -> activate_record(Record, Type) end,
+ medici:mget(medici:search(Query)));
+ false ->
+ []
+ end.
+
+counter(Id) when is_list(Id) ->
+ counter(list_to_binary(Id));
+counter(Id) when is_binary(Id) ->
+ case medici:get(Id) of
+ Record when is_list(Record) ->
+ list_to_integer(binary_to_list(
+ proplists:get_value(<<"_num">>, Record, <<"0">>)));
+ {error, _Reason} -> 0
+ end.
+
+incr(Id) ->
+ medici:addint(Id, 1).
+
+incr(Id, Count) ->
+ medici:addint(Id, Count).
+
+delete(Id) when is_list(Id) ->
+ delete(list_to_binary(Id));
+delete(Id) when is_binary(Id) ->
+ medici:out(Id).
+
+save_record(Record) when is_tuple(Record) ->
+ Type = element(1, Record),
+ Id = case Record:id() of
+ id ->
+ atom_to_list(Type) ++ "-" ++ binary_to_list(medici:genuid());
+ Defined when is_list(Defined) ->
+ Defined
+ end,
+ RecordWithId = Record:id(Id),
+ Columns = lists:map(fun
+ (A) -> {attribute_to_colname(A, Type),
+ case RecordWithId:A() of
+ Val when is_list(Val) ->
+ list_to_binary(Val);
+ {MegaSec, Sec, MicroSec} when is_integer(MegaSec) andalso is_integer(Sec) andalso is_integer(MicroSec) ->
+ pack_now({MegaSec, Sec, MicroSec});
+ {{_, _, _} = Date, {_, _, _} = Time} ->
+ pack_datetime({Date, Time})
+ end}
+ end,
+ RecordWithId:attribute_names()),
+ ok = medici:put(list_to_binary(Id),
+ [{attribute_to_colname('_type', Type), list_to_binary(atom_to_list(Type))}|Columns]),
+ RecordWithId.
+
+infer_type_from_id(Id) when is_binary(Id) ->
+ infer_type_from_id(binary_to_list(Id));
+infer_type_from_id(Id) when is_list(Id) ->
+ list_to_atom(hd(string:tokens(Id, "-"))).
+
+activate_record(Record, Type) ->
+ DummyRecord = apply(Type, new, lists:seq(1, proplists:get_value(new, Type:module_info(exports)))),
+ apply(Type, new, lists:map(fun
+ (Key) ->
+ Val = proplists:get_value(attribute_to_colname(Key, Type), Record, <<"">>),
+ case lists:suffix("_time", atom_to_list(Key)) of
+ true -> unpack_datetime(Val);
+ false -> binary_to_list(Val)
+ end
+ end, DummyRecord:attribute_names())).
+
+model_is_loaded(Type) ->
+ case code:is_loaded(Type) of
+ {file, _Loaded} ->
+ Exports = Type:module_info(exports),
+ case proplists:get_value(attribute_names, Exports) of
+ 1 -> true;
+ _ -> false
+ end;
+ false -> false
+ end.
+
+attribute_to_colname(Attribute, _Type) ->
+ list_to_binary(atom_to_list(Attribute)).
+
+build_query(Type, Conditions, Max, Skip, Sort, SortOrder) ->
+ Query = lists:foldl(fun({K, V}, Acc) ->
+ medici:query_add_condition(Acc, attribute_to_colname(K, Type), str_eq, [list_to_binary(V)])
+ end, [], [{'_type', atom_to_list(Type)} | Conditions]),
+ medici:query_order(medici:query_limit(Query, Max, Skip), atom_to_list(Sort), SortOrder).
+
+pack_datetime({Date, Time}) ->
+ list_to_binary(integer_to_list(calendar:datetime_to_gregorian_seconds({Date, Time}))).
+
+pack_now(Now) -> pack_datetime(calendar:now_to_datetime(Now)).
+
+unpack_datetime(<<"">>) -> calendar:gregorian_seconds_to_datetime(0);
+unpack_datetime(Bin) -> calendar:universal_time_to_local_time(
+ calendar:gregorian_seconds_to_datetime(list_to_integer(binary_to_list(Bin)))).
23 src/boss_db_sup.erl
@@ -0,0 +1,23 @@
+-module(boss_db_sup).
+-author('emmiller@gmail.com').
+
+-behaviour(supervisor).
+
+-export([start_link/0, start_link/1]).
+
+-export([init/1]).
+
+start_link() ->
+ supervisor:start_link(?MODULE, []).
+
+start_link(StartArgs) ->
+ supervisor:start_link(?MODULE, StartArgs).
+
+init(StartArgs) ->
+ {ok, {{one_for_one, 10, 10}, [
+ {db_controller, {boss_db_controller, start_link, StartArgs},
+ permanent,
+ 2000,
+ worker,
+ [boss_db_controller]}
+ ]}}.
343 src/boss_record_compiler.erl
@@ -0,0 +1,343 @@
+-module(boss_record_compiler).
+-author('emmiller@gmail.com').
+-define(DATABASE_MODULE, boss_db).
+
+-export([compile/1, compile/2, edoc_module/1, edoc_module/2]).
+
+%% @spec compile( File::string() ) -> ok | {error, Reason}
+%% @equiv compile(File, [])
+compile(File) ->
+ compile(File, []).
+
+%% @spec compile( File::string(), Options ) -> ok | {error, Reason}
+%% @doc Compile an Erlang source file as a BossRecord. Options:
+%% `compiler_options' - Passed directly to compile:forms/2. Defaults to [verbose, return_errors].
+compile(File, Options) ->
+ case parse(File) of
+ {ok, Forms} ->
+ case compile_to_binary(trick_out_forms(Forms), Options) of
+ {ok, Module, Bin} ->
+ case proplists:get_value(out_dir, Options) of
+ undefined -> ok;
+ OutDir ->
+ BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]),
+ file:write_file(BeamFile, Bin)
+ end;
+ Error ->
+ Error
+ end;
+ Error ->
+ Error
+ end.
+
+%% @spec edoc_module( File::string() ) -> {Module::atom(), EDoc}
+%% @equiv edoc_module(File, [])
+edoc_module(File) ->
+ edoc_module(File, []).
+
+%% @spec edoc_module( File::string(), Options ) -> {Module::atom(), EDoc}
+%% @doc Return an `edoc_module()' for the given Erlang source file when
+%% compiled as a BossRecord.
+edoc_module(File, Options) ->
+ {ok, Forms} = parse(File),
+ edoc_extract:source(trick_out_forms(Forms), edoc:read_comments(File),
+ File, edoc_lib:get_doc_env([]), Options).
+
+parse(File) ->
+ case epp:parse_file(File, [filename:dirname(File)], []) of
+ {ok, Forms} ->
+ case proplists:get_value(error, Forms) of
+ undefined ->
+ {ok, Forms};
+ {Line, _Module, ErrorDescription} ->
+ {error, {File, Line, ErrorDescription}}
+ end;
+ Error ->
+ Error
+ end.
+
+compile_to_binary(Forms, Options) ->
+ case compile:forms(erl_syntax:revert_forms(Forms),
+ proplists:get_value(compiler_options, Options, [verbose, return_errors])) of
+ {ok, Module1, Bin} ->
+ code:purge(Module1),
+ case code:load_binary(Module1, atom_to_list(Module1) ++ ".erl", Bin) of
+ {module, _} -> {ok, Module1, Bin};
+ _ -> {error, lists:concat(["code reload failed: ", Module1])}
+ end;
+ OtherError ->
+ OtherError
+ end.
+
+trick_out_forms([{attribute, _, file, {_FileName, _FileNum}}|Rest]) ->
+ trick_out_forms(Rest);
+
+trick_out_forms([
+ {attribute, _, module, {ModuleName, Parameters}}
+ | _T] = Forms) ->
+ trick_out_forms(Forms, ModuleName, Parameters).
+
+trick_out_forms(Forms, ModuleName, Parameters) ->
+ Attributes = proplists:get_value(attributes, erl_syntax_lib:analyze_forms(Forms)),
+ [{eof, _Line}|OtherForms] = lists:reverse(Forms),
+ Counters = lists:foldl(
+ fun
+ ({counter, Counter}, Acc) -> [Counter|Acc];
+ (_, Acc) -> Acc
+ end, [], Attributes),
+
+ lists:reverse(OtherForms) ++
+ association_forms(ModuleName, Attributes) ++
+ counter_getter_forms(Counters) ++
+ counter_reset_forms(Counters) ++
+ counter_incr_forms(Counters) ++
+ save_forms(ModuleName, Parameters) ++
+ set_attributes_forms(ModuleName, Parameters) ++
+ get_attributes_forms(ModuleName, Parameters) ++
+ attribute_names_forms(ModuleName, Parameters) ++
+ parameter_getter_forms(Parameters) ++
+ parameter_setter_forms(ModuleName, Parameters) ++
+ [].
+
+save_forms(ModuleName, Parameters) ->
+ [erl_syntax:add_precomments([erl_syntax:comment(
+ [lists:concat(["% @spec save() -> Saved", inflector:camelize(atom_to_list(ModuleName))]),
+ lists:concat(["% @doc Saves this `", ModuleName, "' record to the database. The returned record"]),
+ "% will have an auto-generated ID if the record's ID was set to 'id'."])],
+ erl_syntax:function(
+ erl_syntax:atom(save),
+ [erl_syntax:clause([], none,
+ [erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(save_record),
+ [erl_syntax:tuple([erl_syntax:atom(ModuleName)|
+ lists:map(fun(P) ->
+ erl_syntax:variable(P)
+ end, Parameters)
+ ])
+ ])])]))].
+
+parameter_getter_forms(Parameters) ->
+ lists:map(fun(P) ->
+ erl_syntax:add_precomments([erl_syntax:comment(
+ [lists:concat(["% @spec ", parameter_to_colname(P), "() -> ", P]),
+ lists:concat(["% @doc Returns the value of `", P, "'"])])],
+ erl_syntax:function(
+ erl_syntax:atom(parameter_to_colname(P)),
+ [erl_syntax:clause([], none, [erl_syntax:variable(P)])]))
+ end, Parameters).
+
+parameter_setter_forms(ModuleName, Parameters) ->
+ lists:map(
+ fun(P) ->
+ erl_syntax:add_precomments([erl_syntax:comment(
+ [
+ lists:concat(["% @spec ", parameter_to_colname(P), "( ", P, "::",
+ case lists:suffix("Time", atom_to_list(P)) of
+ true -> "tuple()";
+ false -> "string()"
+ end, " ) -> ", inflector:camelize(atom_to_list(ModuleName))]),
+ lists:concat(["% @doc Set the value of `", P, "'."])])],
+ erl_syntax:function(
+ erl_syntax:atom(parameter_to_colname(P)),
+ [erl_syntax:clause([erl_syntax:variable("NewValue")], none,
+ [
+ erl_syntax:application(
+ erl_syntax:atom(ModuleName),
+ erl_syntax:atom(new),
+ lists:map(
+ fun
+ (Param) when Param =:= P ->
+ erl_syntax:variable("NewValue");
+ (Other) ->
+ erl_syntax:variable(Other)
+ end, Parameters))
+ ])]))
+ end, Parameters).
+
+get_attributes_forms(ModuleName, Parameters) ->
+ [erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec attributes() -> [{Key::atom(), Value::string() | undefined}]",
+ lists:concat(["% @doc A proplist of the `", ModuleName, "' parameters and their values."])])],
+ erl_syntax:function(
+ erl_syntax:atom(attributes),
+ [erl_syntax:clause([], none,
+ [erl_syntax:list(lists:map(fun(P) ->
+ erl_syntax:tuple([
+ erl_syntax:atom(parameter_to_colname(P)),
+ erl_syntax:variable(P)])
+ end, Parameters))])]))].
+
+set_attributes_forms(ModuleName, Parameters) ->
+ [erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec attributes(Proplist) -> "++inflector:camelize(atom_to_list(ModuleName)),
+ "% @doc Set multiple record attributes at once. Does not save the record."])],
+ erl_syntax:function(
+ erl_syntax:atom(attributes),
+ [erl_syntax:clause([erl_syntax:variable("NewAttributes")], none,
+ [erl_syntax:application(
+ erl_syntax:atom(ModuleName),
+ erl_syntax:atom(new),
+ lists:map(fun(P) ->
+ erl_syntax:application(
+ erl_syntax:atom(proplists),
+ erl_syntax:atom(get_value),
+ [erl_syntax:atom(parameter_to_colname(P)),
+ erl_syntax:variable("NewAttributes"),
+ erl_syntax:variable(P)])
+ end, Parameters))])]))].
+
+association_forms(ModuleName, Attributes) ->
+ lists:foldl(
+ fun
+ ({has_many, HasMany}, Acc) ->
+ [has_many_forms(HasMany, ModuleName)|Acc];
+ ({has_up_to, {Limit, HasMany}}, Acc) ->
+ [has_many_forms(HasMany, ModuleName, Limit)|Acc];
+ ({has_up_to, {Limit, HasMany, Sort}}, Acc) ->
+ [has_many_forms(HasMany, ModuleName, Limit, Sort)|Acc];
+ ({has_up_to, {Limit, HasMany, Sort, SortOrder}}, Acc) ->
+ [has_many_forms(HasMany, ModuleName, Limit, Sort, SortOrder)|Acc];
+ ({belongs_to, BelongsTo}, Acc) ->
+ [belongs_to_forms(BelongsTo, ModuleName)|Acc];
+ (_, Acc) ->
+ Acc
+ end, [], Attributes).
+
+attribute_names_forms(ModuleName, Parameters) ->
+ [ erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec attribute_names() -> [atom()]",
+ lists:concat(["% @doc A list of the lower-case `", ModuleName, "' parameters."])])],
+ erl_syntax:function(
+ erl_syntax:atom(attribute_names),
+ [erl_syntax:clause([], none, [erl_syntax:list(lists:map(
+ fun(P) -> erl_syntax:atom(parameter_to_colname(P)) end,
+ Parameters))])]))].
+
+has_many_forms(HasMany, ModuleName) ->
+ has_many_forms(HasMany, ModuleName, 1000000).
+
+has_many_forms(HasMany, ModuleName, Limit) ->
+ has_many_forms(HasMany, ModuleName, Limit, primary).
+
+has_many_forms(HasMany, ModuleName, Limit, Sort) ->
+ has_many_forms(HasMany, ModuleName, Limit, Sort, str_ascending).
+
+has_many_forms(HasMany, ModuleName, Limit, Sort, SortOrder) ->
+ Type = inflector:singularize(atom_to_list(HasMany)),
+ erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec "++atom_to_list(HasMany)++"() -> [ "++Type++" ]",
+ lists:concat(["% @doc Retrieves ", HasMany, " with `", ModuleName, "_id' ",
+ "set to the `Id' of this `", ModuleName, "'"])])],
+ erl_syntax:function(erl_syntax:atom(HasMany),
+ [erl_syntax:clause([], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(find),
+ [erl_syntax:atom(Type),
+ erl_syntax:list([
+ erl_syntax:tuple([
+ erl_syntax:atom(
+ atom_to_list(ModuleName) ++ "_id"),
+ erl_syntax:variable("Id")])
+ ]),
+ erl_syntax:integer(Limit),
+ erl_syntax:integer(0),
+ erl_syntax:atom(Sort),
+ erl_syntax:atom(SortOrder)
+ ])])])).
+
+belongs_to_forms(BelongsTo, ModuleName) ->
+ erl_syntax:add_precomments([erl_syntax:comment(
+ [lists:concat(["% @spec ", BelongsTo, "() -> ",
+ inflector:camelize(atom_to_list(BelongsTo))]),
+ lists:concat(["% @doc Retrieves the ", BelongsTo,
+ " with `Id' equal to the `",
+ inflector:camelize(atom_to_list(BelongsTo)), "Id'",
+ " of this ", ModuleName])])],
+ erl_syntax:function(erl_syntax:atom(BelongsTo),
+ [erl_syntax:clause([], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(find),
+ [erl_syntax:variable(inflector:camelize(atom_to_list(BelongsTo)) ++ "Id")]
+ )])])).
+
+counter_getter_forms(Counters) ->
+ lists:map(
+ fun(Counter) ->
+ erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec "++atom_to_list(Counter)++"() -> integer()",
+ "% @doc Retrieve the value of the `"++atom_to_list(Counter)++"' counter"])],
+ erl_syntax:function(erl_syntax:atom(Counter),
+ [erl_syntax:clause([], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(counter),
+ [erl_syntax:infix_expr(
+ erl_syntax:variable("Id"),
+ erl_syntax:operator("++"),
+ erl_syntax:string("-counter-" ++ atom_to_list(Counter))
+ )])])])) end, Counters).
+
+counter_reset_forms([]) ->
+ [];
+counter_reset_forms(Counters) ->
+ [erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec reset( Counter::atom() ) -> ok | {error, Reason}",
+ "% @doc Reset a counter to zero"])],
+ erl_syntax:function(erl_syntax:atom(reset),
+ lists:map(
+ fun(Counter) ->
+ erl_syntax:clause([erl_syntax:atom(Counter)], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(delete),
+ [counter_name_forms(Counter)])])
+ end, Counters)))].
+
+counter_incr_forms([]) ->
+ [];
+counter_incr_forms(Counters) ->
+ [ erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec incr( CounterName::atom() ) -> integer()",
+ "@doc Atomically increment a counter by 1."])],
+ erl_syntax:function(erl_syntax:atom(incr),
+ lists:map(
+ fun(Counter) ->
+ erl_syntax:clause([erl_syntax:atom(Counter)], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(incr),
+ [counter_name_forms(Counter)])])
+ end, Counters))),
+ erl_syntax:add_precomments([erl_syntax:comment(
+ ["% @spec incr( CounterName::atom(), Increment::integer() ) ->"++
+ " integer()",
+ "% @doc Atomically increment a counter by the specified increment"])],
+ erl_syntax:function(erl_syntax:atom(incr),
+ lists:map(
+ fun(Counter) ->
+ erl_syntax:clause([erl_syntax:atom(Counter),
+ erl_syntax:variable("Amount")], none, [
+ erl_syntax:application(
+ erl_syntax:atom(?DATABASE_MODULE),
+ erl_syntax:atom(incr),
+ [counter_name_forms(Counter),
+ erl_syntax:variable("Amount")])])
+ end, Counters)))].
+
+counter_name_forms(CounterVariable) ->
+ erl_syntax:infix_expr(
+ erl_syntax:infix_expr(
+ erl_syntax:variable("Id"),
+ erl_syntax:operator("++"),
+ erl_syntax:string("-counter-")),
+ erl_syntax:operator("++"),
+ erl_syntax:application(
+ erl_syntax:atom('erlang'),
+ erl_syntax:atom('atom_to_list'),
+ [erl_syntax:atom(CounterVariable)])).
+
+parameter_to_colname(Parameter) when is_atom(Parameter) ->
+ string:to_lower(inflector:underscore(atom_to_list(Parameter))).
52 src/boss_sup.erl
@@ -0,0 +1,52 @@
+%% @author Evan Miller <emmiller@gmail.com>
+%% @copyright YYYY author.
+
+%% @doc Supervisor for the boss application.
+
+-module(boss_sup).
+-author('Evan Miller <emmiller@gmail.com>').
+
+-behaviour(supervisor).
+
+%% External exports
+-export([start_link/0, upgrade/0]).
+
+%% supervisor callbacks
+-export([init/1]).
+
+%% @spec start_link() -> ServerRet
+%% @doc API for starting the supervisor.
+start_link() ->
+ supervisor:start_link({local, ?MODULE}, ?MODULE, []).
+
+%% @spec upgrade() -> ok
+%% @doc Add processes if necessary.
+upgrade() ->
+ {ok, {_, Specs}} = init([]),
+
+ Old = sets:from_list(
+ [Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]),
+ New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]),
+ Kill = sets:subtract(Old, New),
+
+ sets:fold(fun (Id, ok) ->
+ supervisor:terminate_child(?MODULE, Id),
+ supervisor:delete_child(?MODULE, Id),
+ ok
+ end, ok, Kill),
+
+ [supervisor:start_child(?MODULE, Spec) || Spec <- Specs],
+ ok.
+
+%% @spec init([]) -> SupervisorTree
+%% @doc supervisor callback.
+init([]) ->
+ Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end,
+ Port = case application:get_env(port) of {ok, P} -> P; undefined -> 8001 end,
+ WebConfig = [ {ip, Ip}, {port, Port} ],
+ Web = {boss_controller,
+ {boss_controller, start, [WebConfig]},
+ permanent, 5000, worker, dynamic},
+
+ Processes = [Web],
+ {ok, {{one_for_one, 10, 10}, Processes}}.
315 src/inflector.erl
@@ -0,0 +1,315 @@
+%% Copyright (c) 2008 Luke Galea www.ideaforge.org
+
+%% 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.
+
+%% Inflector - v0.1
+
+%% @author Luke Galea <luke@ideaforge.org>
+%% @copyright 2008 Luke Galea.
+
+%% @doc A string inflection and general convenience library inspired by Ruby/Rails's ActiveSupport Inflector. Converts strings from plural to singular, etc.
+
+-module(inflector).
+-author('Luke Galea <luke@ideaforge.org>').
+-export([pluralize/1, singularize/1, camelize/1, lower_camelize/1, titleize/1,
+ capitalize/1, humanize/1, underscore/1, dasherize/1, tableize/1, moduleize/1,
+ foreign_key/1, ordinalize/1, cached_re/2]).
+
+-include_lib("eunit/include/eunit.hrl").
+
+%% External API
+singularize(Word) ->
+ pluralize_or_singularize( Word, singulars() ).
+
+pluralize(Word) ->
+ pluralize_or_singularize( Word, plurals() ).
+
+camelize(LowerCaseAndUnderscoredWord) ->
+ lists:flatten(
+ lists:map(
+ fun ([L|Rest]) -> [ string:to_upper(L) | Rest ] end,
+ underscore_tokens( LowerCaseAndUnderscoredWord ) ) ).
+
+lower_camelize(LowerCaseAndUnderscoredWord) ->
+ [First|Rest] = underscore_tokens( LowerCaseAndUnderscoredWord ),
+ First ++ camelize( lists:flatten(Rest) ).
+
+%% Capitalizes every word
+titleize(WordOrSentence) ->
+ string:join( lists:map( fun capitalize/1, string:tokens( WordOrSentence, "_ " ) ), " " ).
+
+%% Capitalizes the first letter, lower cases the rest
+capitalize([F|Rest]) ->
+ [string:to_upper(F) | string:to_lower(Rest)].
+
+%% Capitalizes the first word and turns underscores into spaces
+humanize(Word) ->
+ [First|Rest] = token_words(Word),
+ string:join( [capitalize(First) | Rest], " ").
+
+underscore(CamelCasedWord) ->
+ RE1 = re_compile("([A-Z]+)([A-Z][a-z])"),
+ RE2 = re_compile("([a-z\\d])([A-Z])"),
+ string:to_lower(
+ re_replace(
+ re_replace( CamelCasedWord, RE1, "\\1_\\2" ),
+ RE2, "\\1_\\2" ) ).
+
+dasherize(UnderscoredWord) ->
+ lists:map( fun ($_) -> $-; (C) -> C end, UnderscoredWord ).
+
+tableize(ModuleName) ->
+ pluralize( underscore( ModuleName ) ).
+
+foreign_key(ClassName) ->
+ underscore(ClassName) ++ "_id".
+
+moduleize(TableName) ->
+ camelize(singularize(TableName)).
+
+
+ordinalize(N) ->
+ lists:flatten( ord(N) ).
+
+ord(N) when (N rem 100 >= 11) and (N rem 100 =< 13) -> io_lib:format("~Bth", N);
+ord(N) when (N rem 10) =:= 1 -> io_lib:format("~Bst", [N]);
+ord(N) when (N rem 10) =:= 2 -> io_lib:format("~Bnd", [N]);
+ord(N) when (N rem 10) =:= 3 -> io_lib:format("~Brd", [N]);
+ord(N) -> io_lib:format("~Bth", [N]).
+
+
+%% Helpers
+re_compile( RE ) ->
+ { ok, Compiled } = cached_re( RE, [] ),
+ Compiled.
+
+re_replace( In, RE, Out ) ->
+ re:replace( In, RE, Out, [{return, list}, global] ).
+
+underscore_tokens(S) ->
+ string:tokens( S, "_" ).
+
+token_words(S) ->
+ string:tokens( S, "_ " ).
+
+pluralize_or_singularize( Word, List ) ->
+ case is_uncountable(Word) of
+ true -> Word;
+ false -> replace(Word, List )
+ end.
+
+is_uncountable(Word) ->
+ lists:member(Word, uncountables()).
+
+replace(Word, [] ) ->
+ Word;
+replace(Word, [ {Regex, Replacement} | Remainder ] ) ->
+ { ok, RE } = cached_re(Regex, [caseless]),
+ case re:run( Word, RE ) of
+ { match, _ } ->
+ re_replace( Word, RE, Replacement );
+ nomatch ->
+ replace(Word, Remainder)
+ end.
+
+%% Cached Regular Expressions
+cached_re( RE, Options ) ->
+ CachePid = re_cache(),
+ CachePid ! { get, self(), RE, Options },
+ receive
+ { CachePid, CompiledRE } ->
+ CompiledRE
+ end.
+
+re_cache() ->
+ case whereis( re_cache ) of
+ undefined ->
+ Pid = spawn_link( fun() -> re_cache_loop( ets:new(cached_regexps,[]) ) end ),
+ register( re_cache, Pid ),
+ Pid;
+ Pid -> Pid
+ end.
+
+re_cache_loop( CachedREs ) ->
+ receive
+ { get, Caller, RE, Options } ->
+ Caller ! { self(), re_find_or_compile( CachedREs, RE, Options ) },
+ re_cache_loop( CachedREs )
+ end.
+
+re_find_or_compile( CachedREs, RE, Options ) ->
+ case ets:lookup( CachedREs, { RE, Options } ) of
+ [] ->
+ CompiledRE = re:compile( RE, Options ),
+ true = ets:insert( CachedREs, { { RE, Options }, CompiledRE } ),
+ CompiledRE;
+ [ { { RE, Options }, StoredRE } ] -> StoredRE
+ end.
+
+%% Rules
+plurals() ->
+ irregulars() ++
+ [ {"(quiz)$", "\\1zes" },
+ {"^(ox)$", "\\1en" },
+ {"(quiz)$", "\\1zes" },
+ {"^(ox)$", "\\1en" },
+ {"([m|l])ouse$", "\\1ice" },
+ {"(matr|vert|ind)ix|ex$", "\\1ices" },
+ {"(x|ch|ss|sh)$", "\\1es" },
+ {"([^aeiouy]|qu)y$", "\\1ies" },
+ {"(hive)$", "\\1s" },
+ {"(?:([^f])fe|([lr])f)$", "\\1\\1ves" },
+ {"sis$", "ses" },
+ {"([ti])um$", "\\1a" },
+ {"(buffal|tomat)o$", "\\1oes" },
+ {"(bu)s$", "\\1ses" },
+ {"(alias|status)$", "\\1es" },
+ {"(octop|vir)us$", "\\1i" },
+ {"(ax|test)is$", "\\1es" },
+ {"s$", "s" },
+ {"$", "s" } ].
+
+singulars() ->
+ [ {"(quiz)zes$", "\\1" },
+ {"(matr)ices$", "\\1ix" },
+ {"(vert|ind)ices$", "\\1ex" },
+ {"(ox)en", "\\1" },
+ {"(alias|status)es$", "\\1" },
+ {"(octop|vir)i$", "\\1us" },
+ {"(cris|ax|test)es$", "\\1is" },
+ {"(shoe)s$", "\\1" },
+ {"(o)es$", "\\1" },
+ {"(bus)es$", "\\1" },
+ {"([m|l])ice$", "\\1ouse" },
+ {"(x|ch|ss|sh)es$", "\\1" },
+ {"(m)ovies$", "\\1ovie" },
+ {"(s)eries$", "\\1eries"},
+ {"([^aeiouy]|qu)ies$", "\\1y" },
+ {"([lr])ves$", "\\1f" },
+ {"(tive)s$", "\\1" },
+ {"(hive)s$", "\\1" },
+ {"([^f])ves$", "\\1fe" },
+ {"(^analy)ses$", "\\1sis" },
+ {"((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$", "\\1\\2sis"},
+ {"([ti])a$", "\\1um" },
+ {"(n)ews$", "\\1ews" },
+ {"s$", "" } ] ++ reversed_irregulars().
+
+irregulars() ->
+ [{"move", "moves" },
+ {"sex", "sexes" },
+ {"child", "children"},
+ {"man", "men" },
+ {"person", "people" }].
+
+reversed_irregulars() ->
+ F = fun ({A, B}) -> {B, A} end,
+ lists:map(F, irregulars()).
+
+uncountables() ->
+ ["sheep",
+ "fish",
+ "series",
+ "species",
+ "money",
+ "rice",
+ "information",
+ "equipment" ].
+
+
+%% Tests
+replace_test() ->
+ SampleList = [ {"abc", "def"},
+ {"a(b|c)d", "e\\1e"},
+ {"howdy", "doody"} ],
+ "I know my defs" = replace( "I know my abcs", SampleList ),
+ "Nothing changed" = replace( "Nothing changed", SampleList ),
+ "Blah ebe" = replace("Blah abd", SampleList ),
+ "Howdy ece" = replace("Howdy acd", SampleList ),
+ "doody ho" = replace("howdy ho", SampleList ),
+ "doody" = replace("howdy", SampleList ).
+
+singularize_test() ->
+ "dog" = singularize("dogs"),
+ "mouse" = singularize("mice"),
+ "bus" = singularize("buses"),
+ "sex" = singularize("sexes"),
+ "Sex" = singularize("Sexes"),
+ "sheep" = singularize("sheep"),
+ "child" = singularize("children"),
+ "dog" = singularize("dog").
+
+pluralize_test() ->
+ "dogs" = pluralize("dog"),
+ "dogs" = pluralize("dogs"),
+ "buses" = pluralize("bus"),
+ "sexes" = pluralize("sex"),
+ "sheep" = pluralize("sheep"),
+ "children" = pluralize("child").
+
+camelize_test() ->
+ "CamelCase" = camelize("camel_case").
+
+lower_camelize_test() ->
+ "camelCase" = lower_camelize("camel_case").
+
+humanize_test() ->
+ "Employee salary" = humanize("employee_salary").
+
+titleize_test() ->
+ "Army Of Darkness" = titleize("army of darkness"),
+ "Army Of Darkness" = titleize("army_of_darkness").
+
+capitalize_test() ->
+ "This" = capitalize("this"),
+ "This" = capitalize("tHiS"),
+ "This" = capitalize("THIS").
+
+underscore_test() ->
+ "this_is_a_test" = underscore("ThisIsATest").
+
+dasherize_test() ->
+ "this-has-dashes-now" = dasherize("this_has_dashes_now").
+
+tableize_test() ->
+ "raw_scaled_scorers" = tableize("RawScaledScorer"),
+ "egg_and_hams" = tableize("egg_and_ham"),
+ "fancy_categories" = tableize("fancyCategory").
+
+moduleize_test() ->
+ "FancyCategory" = moduleize("fancy_categories"),
+ "FancyCategory" = moduleize("fancy_category").
+
+ordinalize_test() ->
+ "1st" = ordinalize(1),
+ "2nd" = ordinalize(2),
+ "1002nd" = ordinalize(1002),
+ "4th" = ordinalize(4),
+ "104th" = ordinalize(104).
+
+foreign_key_test() ->
+ "message_id" = foreign_key("Message").
+
+cached_re_test() ->
+ {ok, RE1} = cached_re("Abcdefg", []),
+ {ok, RE2} = cached_re("yuuuu", []),
+ {ok, RE1_1} = cached_re("Abcdefg", []),
+ true = (RE1 =:= RE1_1),
+ false = (RE2 =:= RE1),
+ "QQQ?UUU" == re_replace( "QQQAbcdefgUUU", RE1_1, "?" ).
4 src/overview.edoc
@@ -0,0 +1,4 @@
+@author Evan Miller
+@doc Chicago Boss is an MVC framework for Erlang.
+@see boss_record_compiler
+@see boss_db
3 start-dev.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd `dirname $0`
+exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s reloader -s boss -config boss
3 start.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+cd `dirname $0`
+exec erl -pa $PWD/ebin $PWD/deps/*/ebin -boot start_sasl -s boss -sname john -detached -config boss
3 start_tyrant.sh
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+sudo -u ttserver ttserver -host 127.0.0.1 -dmn -pid /var/run/ttserver/ttserver.pid -log /var/log/ttserver/ttserver.log /var/lib/ttserver/test.tct

0 comments on commit d39a1b7

Please sign in to comment.