diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..cf563fc --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "ChicagoBoss"] + path = ChicagoBoss + url = https://github.com/evanmiller/ChicagoBoss.git diff --git a/ChicagoBoss b/ChicagoBoss new file mode 160000 index 0000000..16603b5 --- /dev/null +++ b/ChicagoBoss @@ -0,0 +1 @@ +Subproject commit 16603b5ee3126c80a52d364a0afa907503a3b679 diff --git a/Procfile b/Procfile new file mode 100644 index 0000000..f1a2542 --- /dev/null +++ b/Procfile @@ -0,0 +1 @@ +web: ./run.sh diff --git a/README.md b/README.md new file mode 100644 index 0000000..48f25a2 --- /dev/null +++ b/README.md @@ -0,0 +1,41 @@ +Running ChicagoBoss (erlang web framework) on Heroku +---------------------------------------------------- + +N.B. This process was an initial attempt and is subject to change :) + +To create a new project from scratch: + +1. Create a top level project directory. Add an empty ebin/ directory. + - (N.B. also create a dummy file to commit to git) +2. + git init; git submodule add https://github.com/evanmiller/ChicagoBoss.git ChicagoBoss +3. + cd ChicagoBoss; make; make app PROJECT=myapp +4. Add Procfile, run.sh from this project - edit project name in run.sh +5. Edit myapp/init.sh - add the following sections: (running erl with the -sname param causes heroku startup to fail) + + 'start-no-sname') + # Start Boss in production mode with no -sname parameter + echo "starting boss in production mode..." + START=$(./rebar boss c=start_cmd|grep -v "==>"|perl -pe 's/-sname\s+\S+//') + $START + ;; + + 'start-dev-no-sname') + # Start Boss in development mode + START_DEV=$(./rebar boss c=start_dev_cmd|grep -v "==>"|perl -pe 's/-sname\s+\S+//') + $START_DEV + ;; + +6. Deploy! (git add .; git push heroku master) + + + +A few caveats: +- This is based on using the heroku supplied erlang buildpack at: https://github.com/heroku/heroku-buildpack-erlang +- A better solution would be to modify the buildpack to build CB at the compile stage (as opposed to the run stage) +- As such, the above solution may crash at the first deploy due to the app not starting within 60secs. Run: + heroku logs -t + in a seperate window to keep an eye on the deployment/startup process. + + diff --git a/cb-heroku.iml b/cb-heroku.iml new file mode 100644 index 0000000..ef582b1 --- /dev/null +++ b/cb-heroku.iml @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/ebin/.empty b/ebin/.empty new file mode 100644 index 0000000..0c42493 --- /dev/null +++ b/ebin/.empty @@ -0,0 +1 @@ +This ebin dir is empty - it exists to convince Heroku that this is an Erlang app. \ No newline at end of file diff --git a/myapp/boss.config b/myapp/boss.config new file mode 100644 index 0000000..56117e1 --- /dev/null +++ b/myapp/boss.config @@ -0,0 +1,19 @@ +[{boss, [ + {path, "/Users/vorn/dev/erl/cb-heroku/ChicagoBoss"}, + {vm_cookie, "abc123"}, + {applications, [myapp]}, + {db_host, "localhost"}, + {db_port, 1978}, + {db_adapter, mock}, + {log_dir, "log"}, + {server, misultin}, + {port, 8001}, + {session_adapter, mock}, + {session_key, "_boss_session"}, + {session_exp_time, 525600} +]}, +{ myapp, [ + {path, "../myapp"}, + {base_url, "/"} +]} +]. \ No newline at end of file diff --git a/myapp/init-dev.sh b/myapp/init-dev.sh new file mode 100755 index 0000000..2c7e57d --- /dev/null +++ b/myapp/init-dev.sh @@ -0,0 +1,8 @@ +#!/usr/bin/env sh +# +# Chicago Boss Dev Init System +# easy start dev server (most common task) + +cd `dirname $0` + +./init.sh start-dev diff --git a/myapp/init.sh b/myapp/init.sh new file mode 100755 index 0000000..aff8eec --- /dev/null +++ b/myapp/init.sh @@ -0,0 +1,68 @@ +#!/usr/bin/env sh +# +# Chicago Boss Init System +# +# @author: Jose Luis Gordo Romero +# +# ------------------------------------------------------------------- +# The shell commands are automatically generated by the boss rebar +# plugin/driver, all configuration params and paths are in boss.config +# ------------------------------------------------------------------- + +cd `dirname $0` + +case "${1:-''}" in + 'start') + # Start Boss in production mode + echo "starting boss in production mode..." + START=$(./rebar boss c=start_cmd|grep -v "==>") + $START + ;; + + 'start-dev') + # Start Boss in development mode + START_DEV=$(./rebar boss c=start_dev_cmd|grep -v "==>") + $START_DEV + ;; + + 'start-standalone') + # Start Boss in production mode with no -sname parameter + echo "starting boss in production mode..." + START=$(./rebar boss c=start_cmd|grep -v "==>"|perl -pe 's/-sname\s+\S+//') + $START + ;; + + 'start-dev-standalone') + # Start Boss in development mode + START_DEV=$(./rebar boss c=start_dev_cmd|grep -v "==>"|perl -pe 's/-sname\s+\S+//') + $START_DEV + ;; + + 'stop') + # Stop Boss daemon + echo "stopping boss..." + STOP=$(./rebar boss c=stop_cmd|grep -v "==>") + # After hours of shell quoting problems with the erl command, + # eval with the command quoted works!!! + eval "$STOP" + ;; + + 'reload') + # Boss hot code reload <-- only the actual node, not the entire cluster + echo "Hot code reload, (WARN: Only this node)" + RELOAD=$(./rebar boss c=reload_cmd|grep -v "==>") + eval "$RELOAD" + ;; + + 'restart') + # Boss complete restart + echo "Restarting (stop-start) boss..." + $0 stop + $0 start + ;; + *) + echo "Chicago Boss Boot System" + echo "Usage: $SELF start|start-dev|stop|reload|restart" + exit 1 + ;; +esac \ No newline at end of file diff --git a/myapp/priv/init/myapp_01_news.erl b/myapp/priv/init/myapp_01_news.erl new file mode 100644 index 0000000..c5dfc39 --- /dev/null +++ b/myapp/priv/init/myapp_01_news.erl @@ -0,0 +1,97 @@ +-module(myapp_01_news). + +-export([init/0, stop/1]). + +% This script is first executed at server startup and should +% return a list of WatchIDs that should be cancelled in the stop +% function below (stop is executed if the script is ever reloaded). +init() -> + {ok, []}. + +stop(ListOfWatchIDs) -> + lists:map(fun boss_news:cancel_watch/1, ListOfWatchIDs). + +%%%%%%%%%%% Ideas +% boss_news:watch("user-42.*", +% fun +% (updated, {Donald, 'location', OldLocation, NewLocation}) -> +% ; +% (updated, {Donald, 'email_address', OldEmail, NewEmail}) +% end), +% +% boss_news:watch("user-*.status", +% fun(updated, {User, 'status', OldStatus, NewStatus}) -> +% Followers = User:followers(), +% lists:map(fun(Follower) -> +% Follower:notify_status_update(User, NewStatus) +% end, Followers) +% end), +% +% boss_news:watch("users", +% fun +% (created, NewUser) -> +% boss_mail:send(?WEBSITE_EMAIL_ADDRESS, +% ?ADMINISTRATOR_EMAIL_ADDRESS, +% "New account!", +% "~p just created an account!~n", +% [NewUser:name()]); +% (deleted, OldUser) -> +% ok +% end), +% +% boss_news:watch("forum_replies", +% fun +% (created, Reply) -> +% OrignalPost = Reply:original_post(), +% OriginalAuthor = OriginalPost:author(), +% case OriginalAuthor:is_online() of +% true -> +% boss_mq:push(OriginalAuthor:comet_channel(), <<"Someone replied!">>); +% false -> +% case OriginalAuthor:likes_email() of +% true -> +% boss_mail:send("website@blahblahblah", +% OriginalAuthor:email_address(), +% "Someone replied!" +% "~p has replied to your post on ~p~n", +% [(Reply:author()):name(), OriginalPost:title()]); +% false -> +% ok +% end +% end; +% (_, _) -> ok +% end), +% +% boss_news:watch("forum_categories", +% fun +% (created, NewCategory) -> +% boss_mail:send(?WEBSITE_EMAIL_ADDRESS, +% ?ADMINISTRATOR_EMAIL_ADDRESS, +% "New category: "++NewCategory:name(), +% "~p has created a new forum category called \"~p\"~n", +% [(NewCategory:created_by()):name(), NewCategory:name()]); +% (_, _) -> ok +% end), +% +% boss_news:watch("forum_category-*.is_deleted", +% fun +% (updated, {ForumCategory, 'is_deleted', false, true}) -> +% ; +% (updated, {ForumCategory, 'is_deleted', true, false}) -> +% end). + +% Invoking the API directly: +%boss_news:deleted("person-42", OldAttrs), +%boss_news:updated("person-42", OldAttrs, NewAttrs), +%boss_news:created("person-42", NewAttrs) + +% Invoking the API via HTTP (with the admin application installed): +% POST /admin/news_api/deleted/person-42 +% old[status] = something + +% POST /admin/news_api/updated/person-42 +% old[status] = blah +% new[status] = barf + +% POST /admin/news_api/created/person-42 +% new[status] = something diff --git a/myapp/priv/myapp.routes b/myapp/priv/myapp.routes new file mode 100644 index 0000000..c9f51f3 --- /dev/null +++ b/myapp/priv/myapp.routes @@ -0,0 +1,20 @@ +% Routes file. + +% Formats: +% {"/some/route", [{controller, "Controller"}, {action, "Action"}]}. +% {"/some/route", [{controller, "Controller"}, {action, "Action"}, {id, "42"}]}. +% {"/some/route", [{application, some_app}, {controller, "Controller"}, {action, "Action"}, {id, "42"}]}. +% {404, [{controller, "Controller"}, {action, "Action"}]}. +% {404, [{controller, "Controller"}, {action, "Action"}, {id, "42"}]}. +% {404, [{application, some_app}, {controller, "Controller"}, {action, "Action"}, {id, "42"}]}. +% +% Note that routing across applications results in a 302 redirect. + +% Front page +% {"/", [{controller, "world"}, {action, "hello"}]}. + +% 404 File Not Found handler +% {404, [{controller, "world"}, {action, "lost"}]}. + +% 500 Internal Error handler (only invoked in production) +% {500, [{controller, "world"}, {action, "calamity"}]}. diff --git a/myapp/priv/rebar/boss_plugin.erl b/myapp/priv/rebar/boss_plugin.erl new file mode 100644 index 0000000..b469345 --- /dev/null +++ b/myapp/priv/rebar/boss_plugin.erl @@ -0,0 +1,155 @@ +%%%------------------------------------------------------------------- +%%% @author Jose Luis Gordo Romero +%%% @doc Chicago Boss rebar plugin +%%% Manage compilation/configuration/scripts stuff the rebar way +%%% @end +%%%------------------------------------------------------------------- +-module(boss_plugin). + +-export([boss/2, + pre_compile/2, + pre_eunit/2]). + +-define(BOSS_CONFIG, "boss.config"). + +%% ==================================================================== +%% Public API +%% ==================================================================== + +%%-------------------------------------------------------------------- +%% @doc boss command +%% @spec boss(_Config, _AppFile) -> ok | {error, Reason} +%% Boss enabled rebar commands, usage: +%% ./rebar boss c=command +%% @end +%%-------------------------------------------------------------------- +boss(RebarConf, AppFile) -> + case is_base_dir() of + true -> + {ok, BossConf} = init(RebarConf, AppFile), + Command = rebar_config:get_global(c, "help"), + case boss_rebar:run(Command, RebarConf, BossConf, AppFile) of + {error, command_not_found} -> + io:format("ERROR: boss command not found.~n"), + boss_rebar:help(), + halt(1); + {error, Reason} -> + io:format("ERROR: executing ~s task: ~s~n", [Command, Reason]), + halt(1); + ok -> ok + end; + false -> ok + end. + +%%-------------------------------------------------------------------- +%% @doc initializes the rebar boss connector plugin +%% @spec init(Config, AppFile) -> {ok, BossConf} | {error, Reason} +%% Set's the ebin cb_apps and loads the connector +%% @end +%%-------------------------------------------------------------------- +init(_RebarConf, AppFile) -> + %% Compile and load the boss_rebar code, this can't be compiled + %% as a normal boss lib without the rebar source dep + %% The load of ./rebar boss: + %% - Rebar itself searchs in rebar.config for {plugin_dir, ["priv/rebar"]}. + %% - Rebar itself compile this plugin and adds it to the execution chain + %% - This plugin compiles and loads the boss_rebar code in ["cb/priv/rebar"], + %% so we can extend/bugfix/tweak the framework without the need of manually + %% recopy code to user apps + BossPath = case boss_config_value(boss, path) of + {error, _} -> + io:format("FATAL: Failed to read boss=>path config in boss.config.~n"), + halt(1); + Val -> Val + end, + RebarErls = rebar_utils:find_files(filename:join([BossPath, "priv", "rebar"]), ".*\\.erl\$"), + + rebar_log:log(debug, "Auto-loading boss rebar modules ~p~n", [RebarErls]), + + lists:map(fun(F) -> + case compile:file(F, [binary]) of + error -> + io:format("FATAL: Failed compilation of ~s module~n", [F]), + halt(1); + {ok, M, Bin} -> + {module, _} = code:load_binary(M, F, Bin), + rebar_log:log(debug, "Loaded ~s~n", [M]) + end + end, RebarErls), + + BossConf = boss_config(), + + %% add all cb_apps defined in boss.config to code path + %% including the deps ebin dirs + [code:add_path(CodePath) || CodePath <- boss_rebar:all_ebin_dirs(BossConf, AppFile)], + + {ok, BossConf}. + +%%-------------------------------------------------------------------- +%% @doc pre_compile hook +%% @spec pre_compile(_Config, AppFile) -> ok | {error, Reason} +%% Pre compile hook, compile the boss way +%% Compatibility hook, the normal ./rebar compile command works, +%% but only calls the ./rebar boss c=compile and halts (default +%% rebar task never hits) +%% @end +%%-------------------------------------------------------------------- +pre_compile(RebarConf, AppFile) -> + case is_base_dir() of + true -> + {ok, BossConf} = init(RebarConf, AppFile), + boss_rebar:run(compile, RebarConf, BossConf, AppFile), + halt(0); + false -> ok + end. + +%%-------------------------------------------------------------------- +%% @doc pre_eunit hook +%% @spec pre_eunit(RebarConf, AppFile) -> ok | {error, Reason} +%% Pre eunit hook, .eunit compilation the boss way +%% Compatibility hook, the normal ./rebar eunit command works, +%% but only calls the ./rebar boss c=test_eunit and halts +%% (default rebar task never hits) +%% @end +%%-------------------------------------------------------------------- +pre_eunit(RebarConf, AppFile) -> + case is_base_dir() of + true -> + {ok, BossConf} = init(RebarConf, AppFile), + boss_rebar:run(test_eunit, RebarConf, BossConf, AppFile), + halt(0); + false -> ok + end. + +%% =================================================================== +%% Internal functions +%% =================================================================== + +%% Checks if the current dir (rebar execution) is the base_dir +%% Used to prevent run boss tasks in deps directory +is_base_dir() -> + filename:absname(rebar_utils:get_cwd()) =:= rebar_config:get_global(base_dir, undefined). + +%% Gets the boss.config central configuration file +boss_config() -> + {ok, BossConfig} = file:consult(?BOSS_CONFIG), + hd(BossConfig). + +%%-------------------------------------------------------------------- +%% @doc Get Boss config value app, key +%% @spec boss_config_value(App, Key) -> Value | {error, Reason} +%% Searchs in boss config for a given App and Key +%% @end +%%-------------------------------------------------------------------- +boss_config_value(App, Key) -> + case lists:keyfind(App, 1, boss_config()) of + false -> + {error, boss_config_app_not_found}; + {App, AppConfig} -> + case lists:keyfind(Key, 1, AppConfig) of + false -> + {error, boss_config_app_setting_not_found}; + {Key, KeyConfig} -> + KeyConfig + end + end. diff --git a/myapp/priv/static/chicago-boss.png b/myapp/priv/static/chicago-boss.png new file mode 100644 index 0000000..1403a88 Binary files /dev/null and b/myapp/priv/static/chicago-boss.png differ diff --git a/myapp/priv/static/favicon.ico b/myapp/priv/static/favicon.ico new file mode 100644 index 0000000..2dc01d3 Binary files /dev/null and b/myapp/priv/static/favicon.ico differ diff --git a/myapp/rebar b/myapp/rebar new file mode 100755 index 0000000..5328220 Binary files /dev/null and b/myapp/rebar differ diff --git a/myapp/rebar.cmd b/myapp/rebar.cmd new file mode 100644 index 0000000..6c7a1ca --- /dev/null +++ b/myapp/rebar.cmd @@ -0,0 +1,4 @@ +@echo off +setlocal +set rebarscript=%~f0 +escript.exe "%rebarscript:.cmd=%" %* diff --git a/myapp/rebar.config b/myapp/rebar.config new file mode 100644 index 0000000..b9e40c8 --- /dev/null +++ b/myapp/rebar.config @@ -0,0 +1,3 @@ +{plugin_dir, ["priv/rebar"]}. +{plugins, [boss_plugin]}. +{eunit_compile_opts, [{src_dirs, ["src/test"]}]}. \ No newline at end of file diff --git a/myapp/src/mail/myapp_incoming_mail_controller.erl b/myapp/src/mail/myapp_incoming_mail_controller.erl new file mode 100644 index 0000000..dff3df1 --- /dev/null +++ b/myapp/src/mail/myapp_incoming_mail_controller.erl @@ -0,0 +1,8 @@ +-module(myapp_incoming_mail_controller). +-compile(export_all). + +authorize_(User, DomainName, IPAddress) -> + true. + +% post(FromAddress, Message) -> +% ok. diff --git a/myapp/src/mail/myapp_outgoing_mail_controller.erl b/myapp/src/mail/myapp_outgoing_mail_controller.erl new file mode 100644 index 0000000..3ac04c5 --- /dev/null +++ b/myapp/src/mail/myapp_outgoing_mail_controller.erl @@ -0,0 +1,12 @@ +-module(myapp_outgoing_mail_controller). +-compile(export_all). + +%% See http://www.chicagoboss.org/api-mail-controller.html for what should go in here + +test_message(FromAddress, ToAddress, Subject) -> + Headers = [ + {"Subject", Subject}, + {"To", ToAddress}, + {"From", FromAddress} + ], + {ok, FromAddress, ToAddress, Headers, [{address, ToAddress}]}. diff --git a/myapp/src/myapp.app.src b/myapp/src/myapp.app.src new file mode 100644 index 0000000..2a31cb3 --- /dev/null +++ b/myapp/src/myapp.app.src @@ -0,0 +1,8 @@ +{application, myapp, [ + {description, "My Awesome Web Framework"}, + {vsn, "0.0.1"}, + {modules, []}, + {registered, []}, + {applications, [kernel, stdlib, crypto, boss]}, + {env, []} + ]}. diff --git a/myapp/src/view/lib/README b/myapp/src/view/lib/README new file mode 100644 index 0000000..c75b105 --- /dev/null +++ b/myapp/src/view/lib/README @@ -0,0 +1,22 @@ +This directory contains: + +* tag_html/ - template files which are compiled to tags. If you have a file +called "foo.html" and then call {% foo bar=1 %} from another template, the +contents of "foo.html" will be evaluated with the "bar" variable set to 1. + +* tag_modules/ - Erlang modules that export functions to implement tags. If +a module in this directory exports foo/1, then {% foo bar=1 %} will call + + Module:foo([{bar, 1}]) + +* filter_modules/ - Erlang modules that export functions to implement filters. +If a module in this directory exports foo/1, then {% "Example"|foo %} will call + + Module:foo(<<"Example">>) + +If module in this directory exports foo/2, then {% "Example"|foo:42 %} will call + + Module:foo(<<"Example">>, 42) + +You can specify external tag and filter modules in the configuration via the +template_tag_modules and template_filter_modules options. diff --git a/myapp/src/view/lib/filter_modules/myapp_custom_filters.erl b/myapp/src/view/lib/filter_modules/myapp_custom_filters.erl new file mode 100644 index 0000000..c5a2e77 --- /dev/null +++ b/myapp/src/view/lib/filter_modules/myapp_custom_filters.erl @@ -0,0 +1,9 @@ +-module(myapp_custom_filters). +-compile(export_all). + +% put custom filters in here, e.g. +% +% my_reverse(Value) -> +% lists:reverse(binary_to_list(Value)). +% +% "foo"|my_reverse => "foo" diff --git a/myapp/src/view/lib/tag_modules/myapp_custom_tags.erl b/myapp/src/view/lib/tag_modules/myapp_custom_tags.erl new file mode 100644 index 0000000..eab5fed --- /dev/null +++ b/myapp/src/view/lib/tag_modules/myapp_custom_tags.erl @@ -0,0 +1,11 @@ +-module(myapp_custom_tags). +-compile(export_all). + +% put custom tags in here, e.g. +% +% reverse(Variables, Options) -> +% lists:reverse(binary_to_list(proplists:get_value(string, Variables))). +% +% {% reverse string="hello" %} => "olleh" +% +% Variables are the passed-in vars in your template diff --git a/myapp/start-server.bat b/myapp/start-server.bat new file mode 100644 index 0000000..2d5b5fb --- /dev/null +++ b/myapp/start-server.bat @@ -0,0 +1,3 @@ +@ECHO OFF +FOR /F "tokens=*" %%i in ('"rebar.cmd boss c=start_dev_cmd ^| findstr werl"') do set myvar=%%i +START "Erlang Window" %myvar% diff --git a/run.sh b/run.sh new file mode 100755 index 0000000..44b2c9b --- /dev/null +++ b/run.sh @@ -0,0 +1,9 @@ +#!/bin/sh +cd ChicagoBoss +make +cd ../myapp +./init.sh start-standalone +while true; do + sleep 10 +done +