├── priv ├── empty ├── test_session_config │ ├── mock.config │ ├── cache.config │ └── mnesia.config ├── clean_for_release.sh └── elixir │ └── boss │ ├── flash.ex │ ├── repo.ex │ ├── session.ex │ └── web_controller.ex ├── test ├── good.dtl ├── boss_elixir_compiler_test.erl ├── bad.dtl ├── std.hrl ├── boss_assert_test.erl ├── boss_files_test.erl ├── boss_mq_test.erl ├── boss_web_controller_handle_request_test.erl ├── boss_controller_compiler_test.erl ├── boss_web_controller_test.erl └── boss_load_test.erl ├── skel ├── priv │ ├── migrations │ │ └── .gitignore │ ├── static │ │ ├── favicon.ico │ │ ├── chicago-boss.png │ │ └── edoc │ │ │ ├── erlang.png │ │ │ └── chicagoboss.png │ ├── boss.routes │ └── init │ │ └── news.erl ├── rebar ├── src │ ├── mail │ │ ├── view │ │ │ ├── test_message.txt │ │ │ └── test_message.html │ │ ├── incoming_mail_controller.erl │ │ └── outgoing_mail_controller.erl │ ├── project.app.src │ └── view │ │ └── lib │ │ ├── filter_modules │ │ └── custom_filters.erl │ │ ├── tag_modules │ │ └── custom_tags.erl │ │ └── README ├── rebar.cmd ├── init-dev.sh ├── start-server.bat ├── rebar.config └── init.sh ├── rebar ├── doc-src ├── api-request.html ├── chicago-boss.png ├── trivial_boss_record.erl ├── api-mq.html ├── api-session.html ├── api.html ├── api-view.html ├── boss.css ├── api-test.html ├── api-websocket.html └── api-news.html ├── rebar.cmd ├── .gitignore ├── src ├── overview.edoc ├── boss │ ├── chicago_boss_types.hrl │ ├── boss_session_test.erl │ ├── boss_model_manager_adapter.erl │ ├── compiler_adapters │ │ ├── boss_compiler_adapter_erlang.erl │ │ ├── boss_compiler_adapter_elixir.erl │ │ └── boss_compiler_adapter_lfe.erl │ ├── template_adapters │ │ ├── boss_template_adapter_jade.erl │ │ ├── boss_template_adapter_eex.erl │ │ └── boss_template_adapter_erlydtl.erl │ ├── boss_app.erl │ ├── types.erl │ ├── boss_session_adapter.erl │ ├── boss_router_sup.erl │ ├── boss_mq_sup.erl │ ├── boss_service_handler.erl │ ├── boss_mail_sup.erl │ ├── boss_session_mock_sup.erl │ ├── boss_translator_sup.erl │ ├── mq_adapters │ │ └── boss_mq_adapter_tinymq.erl │ ├── boss_html_errors_template.dtl │ ├── filters │ │ ├── boss_lang_filter.erl │ │ ├── boss_cache_vars_filter.erl │ │ └── boss_cache_page_filter.erl │ ├── boss_html_error_template.dtl │ ├── boss_session_sup.erl │ ├── boss.erl │ ├── model_manager_adapters │ │ ├── boss_model_manager_ecto.erl │ │ └── boss_model_manager_boss_db.erl │ ├── boss_mail_controller.erl │ ├── boss_web.hrl │ ├── boss_model_manager.erl │ ├── boss_translator.erl │ ├── boss_migrate.erl │ ├── boss_flash.erl │ ├── boss_web.erl │ ├── boss_html_doc_template.dtl │ ├── boss_mail_driver_smtp.erl │ ├── boss_elixir_compiler.erl │ ├── boss_env.erl │ ├── session_adapters │ │ ├── boss_session_adapter_mock.erl │ │ ├── boss_session_adapter_cache.erl │ │ └── boss_session_adapter_mnesia.erl │ ├── boss_mq_controller.erl │ ├── boss_sup.erl │ ├── boss_controller_lib.erl │ ├── boss_web_controller_cowboy.erl │ ├── boss_translator_controller.erl │ ├── boss_mochicow_handler.erl │ ├── boss_json.erl │ ├── boss_erlydtl_tags.erl │ ├── boss_router.erl │ ├── boss_session.erl │ ├── boss_service_sup.erl │ ├── boss_session_controller.erl │ ├── boss_mail_driver_mock.erl │ ├── boss_web_controller_util.erl │ ├── boss_mq.erl │ ├── boss_session_mock_controller.erl │ └── boss_session_test_app.erl ├── tests │ └── ct │ │ └── make_app_SUITE.erl ├── boss.app.src └── boss_test │ └── boss_test.erl ├── ebin ├── bson.app ├── boss_db_test.app ├── mongodb.app ├── boss_session_test.app ├── boss_translator.app ├── erlydtl.app └── medici.app ├── .travis.yml ├── dialyzer.sh ├── include ├── simple_bridge.hrl ├── httpd_r12b5.hrl └── yaws_api.hrl ├── mix.exs ├── make-plt.sh ├── LICENSE ├── README_LFE.md ├── travis-dialyzer.sh ├── README_TESTS.md ├── ct └── boss_mq_SUITE.erl ├── windows-make.bat ├── CODING_STANDARDS.md ├── bin └── boss ├── skel.template ├── Makefile ├── rebar.config ├── README_ELIXIR.md └── README_DATABASE.md /priv/empty: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /test/good.dtl: -------------------------------------------------------------------------------- 1 | {{test}} 2 | -------------------------------------------------------------------------------- /skel/priv/migrations/.gitignore: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/rebar -------------------------------------------------------------------------------- /skel/rebar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/skel/rebar -------------------------------------------------------------------------------- /skel/src/mail/view/test_message.txt: -------------------------------------------------------------------------------- 1 | This is a plain-text email sent to {{ address }}. 2 | -------------------------------------------------------------------------------- /doc-src/api-request.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 | {% endblock %} 4 | -------------------------------------------------------------------------------- /rebar.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set rebarscript=%~f0 4 | escript.exe "%rebarscript:.cmd=%" %* 5 | -------------------------------------------------------------------------------- /skel/rebar.cmd: -------------------------------------------------------------------------------- 1 | @echo off 2 | setlocal 3 | set rebarscript=%~f0 4 | escript.exe "%rebarscript:.cmd=%" %* 5 | -------------------------------------------------------------------------------- /doc-src/chicago-boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/doc-src/chicago-boss.png -------------------------------------------------------------------------------- /skel/priv/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/skel/priv/static/favicon.ico -------------------------------------------------------------------------------- /doc-src/trivial_boss_record.erl: -------------------------------------------------------------------------------- 1 | -module(boss_record, [Id]). 2 | -compile(export_all). 3 | -counter(trivial_counter). 4 | -------------------------------------------------------------------------------- /skel/priv/static/chicago-boss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/skel/priv/static/chicago-boss.png -------------------------------------------------------------------------------- /skel/priv/static/edoc/erlang.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/skel/priv/static/edoc/erlang.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ebin/*.beam 2 | ebin/boss.app 3 | *.beam 4 | deps 5 | erl_crash.dump 6 | *~ 7 | #*# 8 | .*# 9 | *# 10 | .eunit 11 | -------------------------------------------------------------------------------- /skel/priv/static/edoc/chicagoboss.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/requirement/ChicagoBoss/master/skel/priv/static/edoc/chicagoboss.png -------------------------------------------------------------------------------- /src/overview.edoc: -------------------------------------------------------------------------------- 1 | @author Evan Miller 2 | @doc Chicago Boss is an MVC framework for Erlang. 3 | @see boss_record_compiler 4 | @see boss_db 5 | -------------------------------------------------------------------------------- /src/boss/chicago_boss_types.hrl: -------------------------------------------------------------------------------- 1 | %% NO Executable code, type declarations only 2 | 3 | -type execution_mode() :: 'development' | 'production'. 4 | -------------------------------------------------------------------------------- /test/boss_elixir_compiler_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_elixir_compiler_test). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include("../src/boss/boss_web.hrl"). 4 | -------------------------------------------------------------------------------- /test/bad.dtl: -------------------------------------------------------------------------------- 1 |

{{test}

2 |
3 | This is a broken DTL file, to ensure that the system does something 4 | that makes sense when we hit one 5 |
6 | 7 | -------------------------------------------------------------------------------- /skel/init-dev.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Chicago Boss Dev Init System 4 | # easy start dev server (most common task) 5 | 6 | cd `dirname $0` 7 | 8 | ./init.sh start-dev 9 | -------------------------------------------------------------------------------- /skel/src/mail/view/test_message.html: -------------------------------------------------------------------------------- 1 |

This is an HTML email to {{ address }}.

2 | 3 |

Click here to visit the home page.

4 | -------------------------------------------------------------------------------- /priv/test_session_config/mock.config: -------------------------------------------------------------------------------- 1 | [{boss_session_test, 2 | [ 3 | {session_adapter, mock}, 4 | {session_key, "_boss_session"}, 5 | {session_exp_time, 525600} 6 | ] 7 | }]. 8 | -------------------------------------------------------------------------------- /priv/test_session_config/cache.config: -------------------------------------------------------------------------------- 1 | [{boss_session_test, 2 | [ 3 | {session_adapter, cache}, 4 | {session_key, "_boss_session"}, 5 | {session_exp_time, 525600} 6 | ] 7 | }]. 8 | -------------------------------------------------------------------------------- /priv/test_session_config/mnesia.config: -------------------------------------------------------------------------------- 1 | [{boss_session_test, 2 | [ 3 | {session_adapter, mnesia}, 4 | {session_key, "_boss_session"}, 5 | {session_exp_time, 525600} 6 | ] 7 | }]. 8 | -------------------------------------------------------------------------------- /test/std.hrl: -------------------------------------------------------------------------------- 1 | -include_lib("proper/include/proper.hrl"). 2 | -include_lib("eunit/include/eunit.hrl"). 3 | -include("../src/boss/boss_web.hrl"). 4 | -import(prop_runner,[gen/2]). 5 | 6 | -compile(export_all). 7 | -------------------------------------------------------------------------------- /ebin/bson.app: -------------------------------------------------------------------------------- 1 | {application, bson, 2 | [{description, "BSON are JSON-like objects with a standard binary serialization. See bsonspec.org."}, 3 | {vsn, "0"}, 4 | {modules, [bson, bson_binary, bson_tests]} 5 | ]}. 6 | -------------------------------------------------------------------------------- /skel/start-server.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | CALL rebar.cmd get-deps 3 | CALL rebar.cmd compile 4 | FOR /F "tokens=*" %%i in ('"rebar.cmd boss c=start_dev_cmd ^| findstr werl"') do set myvar=%%i 5 | START "Erlang Window" %myvar% 6 | -------------------------------------------------------------------------------- /skel/src/mail/incoming_mail_controller.erl: -------------------------------------------------------------------------------- 1 | -module({{appid}}_incoming_mail_controller). 2 | -compile(export_all). 3 | 4 | authorize_(User, DomainName, IPAddress) -> 5 | true. 6 | 7 | % post(FromAddress, Message) -> 8 | % ok. 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: erlang 2 | otp_release: 3 | - R16B02 4 | - R16B01 5 | 6 | script: 7 | - rebar get-deps 8 | - rebar compile 9 | - rebar eunit -v skip_deps=true 10 | - mkdir plt 11 | - ./travis-dialyzer.sh 12 | notifications: 13 | email: false -------------------------------------------------------------------------------- /skel/src/project.app.src: -------------------------------------------------------------------------------- 1 | {application, {{appid}}, [ 2 | {description, "My Awesome Web Framework"}, 3 | {vsn, "0.0.1"}, 4 | {modules, []}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib, crypto]}, 7 | {env, []} 8 | ]}. 9 | -------------------------------------------------------------------------------- /src/boss/boss_session_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_test). 2 | -export([start/0, start/1, stop/0]). 3 | 4 | start() -> 5 | start([]). 6 | 7 | start([]) -> 8 | application:start(boss_session_test). 9 | 10 | stop() -> 11 | application:stop(boss_session_test). 12 | -------------------------------------------------------------------------------- /skel/rebar.config: -------------------------------------------------------------------------------- 1 | {deps, [ 2 | {boss, ".*", {git, "git://github.com/ChicagoBoss/ChicagoBoss.git", {tag, "v0.8.10"}}} 3 | ]}. 4 | {plugin_dir, ["priv/rebar"]}. 5 | {plugins, [boss_plugin]}. 6 | {eunit_compile_opts, [{src_dirs, ["src/test"]}]}. 7 | {lib_dirs, ["{{src}}/deps/elixir/lib"]}. 8 | -------------------------------------------------------------------------------- /skel/src/view/lib/filter_modules/custom_filters.erl: -------------------------------------------------------------------------------- 1 | -module({{appid}}_custom_filters). 2 | -compile(export_all). 3 | 4 | % put custom filters in here, e.g. 5 | % 6 | % my_reverse(Value) -> 7 | % lists:reverse(binary_to_list(Value)). 8 | % 9 | % "foo"|my_reverse => "oof" 10 | -------------------------------------------------------------------------------- /dialyzer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | export PATH=$PATH:/usr/local/bin:/usr/bin 4 | PLT=plt/cb.plt 5 | 6 | echo "" 7 | dialyzer ebin/ \ 8 | -Werror_handling \ 9 | -Wno_undefined_callbacks \ 10 | -Wrace_conditions \ 11 | --fullpath \ 12 | --plt $PLT # -Wunmatched_returns -n 13 | # 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/tests/ct/make_app_SUITE.erl: -------------------------------------------------------------------------------- 1 | -module(make_app_SUITE). 2 | -include_lib("common_test/include/ct.hrl"). 3 | -export([all/0]). 4 | -export([make_app/1]). 5 | 6 | all() -> [make_app]. 7 | 8 | %% This would be a make_app/1 not a make_app/0 9 | make_app(_Config) -> 10 | os:cmd("cd ../../ && make app PROJECT=testproj"). 11 | 12 | -------------------------------------------------------------------------------- /ebin/boss_db_test.app: -------------------------------------------------------------------------------- 1 | {application, boss_db_test, 2 | [{description, "Chicago Boss DB Testing Application"}, 3 | {vsn, "0.1"}, 4 | {modules, [ 5 | boss_db_test, 6 | boss_db_test_app 7 | ]}, 8 | {registered, []}, 9 | {mod, {boss_db_test_app, []}}, 10 | {env, []}, 11 | {applications, [kernel, stdlib]}]}. 12 | -------------------------------------------------------------------------------- /ebin/mongodb.app: -------------------------------------------------------------------------------- 1 | {application, mongodb, 2 | [{description, "Client interface to MongoDB, also known as the driver. See www.mongodb.org"}, 3 | {vsn, "0.0"}, 4 | {modules, [mongodb_app, mongo, mongo_protocol, mongo_connect, mongo_query, mongo_cursor, mvar, mongodb_tests]}, 5 | {registered, []}, 6 | {applications, [kernel, stdlib]}, 7 | {mod, {mongodb_app, []}} 8 | ]}. -------------------------------------------------------------------------------- /skel/src/view/lib/tag_modules/custom_tags.erl: -------------------------------------------------------------------------------- 1 | -module({{appid}}_custom_tags). 2 | -compile(export_all). 3 | 4 | % put custom tags in here, e.g. 5 | % 6 | % reverse(Variables, Options) -> 7 | % lists:reverse(binary_to_list(proplists:get_value(string, Variables))). 8 | % 9 | % {% reverse string="hello" %} => "olleh" 10 | % 11 | % Variables are the passed-in vars in your template 12 | -------------------------------------------------------------------------------- /ebin/boss_session_test.app: -------------------------------------------------------------------------------- 1 | {application, boss_session_test, 2 | [{description, "Chicago Boss Session Testing Application"}, 3 | {vsn, "0.1"}, 4 | {modules, [ 5 | boss_session_test, 6 | boss_session_test_app 7 | ]}, 8 | {registered, []}, 9 | {mod, {boss_session_test_app, []}}, 10 | {env, []}, 11 | {applications, [kernel, stdlib]}]}. 12 | -------------------------------------------------------------------------------- /include/simple_bridge.hrl: -------------------------------------------------------------------------------- 1 | -ifndef(debug_print). 2 | -define(debug_print, true). 3 | -define(PRINT(Var), error_logger:info_msg("DEBUG: ~p:~p~n~p~n ~p~n", [?MODULE, ?LINE, ??Var, Var])). 4 | -endif. 5 | 6 | -record(cookie, { name, value, path="/", minutes_to_live=20 }). 7 | -record(header, { name, value }). 8 | -record(response, { statuscode=200, headers=[], cookies=[], data=[] }). 9 | -record(uploaded_file, { original_name, temp_file, size }). -------------------------------------------------------------------------------- /ebin/boss_translator.app: -------------------------------------------------------------------------------- 1 | {application, boss_translator, 2 | [{description, "BossTranslator I18n engine"}, 3 | {vsn, "0.01"}, 4 | {modules, [ 5 | boss_translator, 6 | boss_translator_app, 7 | boss_translator_controller, 8 | boss_translator_sup 9 | ]}, 10 | {registered, []}, 11 | {mod, {boss_translator_app, []}}, 12 | {env, []}, 13 | {applications, [kernel, stdlib]}]}. 14 | -------------------------------------------------------------------------------- /src/boss/boss_model_manager_adapter.erl: -------------------------------------------------------------------------------- 1 | -module (boss_model_manager_adapter). 2 | -export ([behaviour_info/1]). 3 | 4 | %% @spec behaviour_info( atom() ) -> [ {Function::atom(), Arity::integer()} ] | undefined 5 | behaviour_info (callbacks) -> 6 | [ {start, 0}, {stop, 0}, {compile, 2}, {edoc_module, 2}, 7 | {is_model_instance, 2}, {dummy_instance, 1}, 8 | {to_json, 1} %, {from_json, 1} 9 | ]; 10 | behaviour_info (_Other) -> undefined. 11 | -------------------------------------------------------------------------------- /skel/src/mail/outgoing_mail_controller.erl: -------------------------------------------------------------------------------- 1 | -module({{appid}}_outgoing_mail_controller). 2 | -compile(export_all). 3 | 4 | %% See http://www.chicagoboss.org/api-mail-controller.html for what should go in here 5 | 6 | test_message(FromAddress, ToAddress, Subject) -> 7 | Headers = [ 8 | {"Subject", Subject}, 9 | {"To", ToAddress}, 10 | {"From", FromAddress} 11 | ], 12 | {ok, FromAddress, ToAddress, Headers, [{address, ToAddress}]}. 13 | -------------------------------------------------------------------------------- /mix.exs: -------------------------------------------------------------------------------- 1 | defmodule Boss.Mixfile do 2 | use Mix.Project 3 | 4 | def project do 5 | [ app: :boss, 6 | version: "0.8.7", 7 | deps: deps(Mix.env), 8 | name: "Chicago Boss", 9 | source_url: "https://github.com/evanmiller/ChicagoBoss", 10 | elixir: "~> 0.10.3" ] 11 | end 12 | 13 | def application do 14 | [] 15 | end 16 | 17 | defp deps(_) do 18 | [ { :ecto, github: "elixir-lang/ecto" }, 19 | { :postgrex, github: "ericmj/postgrex" } ] 20 | end 21 | end 22 | -------------------------------------------------------------------------------- /src/boss.app.src: -------------------------------------------------------------------------------- 1 | {application, boss, 2 | [ 3 | {description, "Chicago Boss web framework, now serving three flavors of Comet"}, 4 | {vsn, "0.8.7"}, 5 | {registered, [ 6 | boss_mq, boss_mq_sup, 7 | boss_session, boss_session_sup, 8 | boss_session_mock, boss_session_mock_sup 9 | ]}, 10 | {modules, []}, 11 | {applications, [ 12 | kernel, 13 | stdlib, 14 | crypto 15 | ]}, 16 | {mod, {boss_app, []}}, 17 | {env, []} 18 | ]}. 19 | -------------------------------------------------------------------------------- /src/boss/compiler_adapters/boss_compiler_adapter_erlang.erl: -------------------------------------------------------------------------------- 1 | -module(boss_compiler_adapter_erlang). 2 | -compile(export_all). 3 | 4 | file_extensions() -> ["erl"]. 5 | 6 | controller_module(AppName, Controller) -> lists:concat([AppName, "_", Controller, "_controller"]). 7 | 8 | module_name_for_file(_AppName, File) -> filename:basename(File, ".erl"). 9 | 10 | compile_controller(File, Options) -> 11 | boss_controller_compiler:compile(File, Options). 12 | 13 | compile(File, Options) -> 14 | boss_compiler:compile(File, Options). 15 | -------------------------------------------------------------------------------- /priv/clean_for_release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Trim the fat 4 | 5 | rm -rf deps/*/.git 6 | rm -rf deps/*/rebar 7 | rm -rf deps/erlydtl/tests 8 | rm -rf deps/mochicow/examples 9 | rm -rf deps/epgsql/test_data 10 | rm -rf deps/misultin/examples 11 | rm -rf deps/boss_db/priv/test_* 12 | rm -rf deps/cowboy/test 13 | rm -rf deps/cowboy/examples 14 | rm -rf deps/gen_smtp/testdata 15 | rm -rf deps/mochiweb/support 16 | rm -rf deps/mochiweb/examples 17 | rm -rf deps/pmod_transform/tests 18 | rm -rf deps/jaderl/tests 19 | sed -i "s/{vsn, git}/{vsn, \"0\"}/" deps/*/src/*.app.src 20 | -------------------------------------------------------------------------------- /src/boss/template_adapters/boss_template_adapter_jade.erl: -------------------------------------------------------------------------------- 1 | -module(boss_template_adapter_jade). 2 | -compile(export_all). 3 | 4 | file_extensions() -> ["jade"]. 5 | 6 | translatable_strings(_Module) -> []. 7 | 8 | source(_Module) -> "". 9 | 10 | dependencies(_Module) -> []. 11 | 12 | render(Module, Variables, Options) -> 13 | Module:render(Variables, Options). 14 | 15 | compile_file(ViewPath, Module, Options) -> 16 | OutDir = proplists:get_value(out_dir, Options), 17 | ok = jaderl:compile(ViewPath, Module, [{out_dir, OutDir}]), 18 | {ok, Module}. 19 | 20 | -------------------------------------------------------------------------------- /src/boss/boss_app.erl: -------------------------------------------------------------------------------- 1 | %% @author Evan Miller 2 | %% @copyright YYYY author. 3 | 4 | %% @doc Callbacks for the rr application. 5 | 6 | -module(boss_app). 7 | -author('Evan Miller '). 8 | 9 | -behaviour(application). 10 | -export([start/2,stop/1]). 11 | 12 | 13 | %% @spec start(_Type, _StartArgs) -> ServerRet 14 | %% @doc application start callback for boss. 15 | start(_Type, _StartArgs) -> 16 | boss_sup:start_link(). 17 | 18 | %% @spec stop(_State) -> ServerRet 19 | %% @doc application stop callback for boss. 20 | stop(_State) -> 21 | ok. 22 | -------------------------------------------------------------------------------- /src/boss/types.erl: -------------------------------------------------------------------------------- 1 | -module(types). 2 | 3 | -type execution_mode() :: 'development' | 'production'. 4 | -type application() :: atom(). 5 | -type language() :: any(). 6 | -type webserver() :: 'cowboy' | 'mochiweb_http'. 7 | -type cb_node() :: node(). 8 | -type controller() :: any(). 9 | -type compiler_adapters() :: 'boss_compiler_adapter_elixir' | 'boss_compiler_adapter_erlang' | 'boss_compiler_adapter_lfe'. 10 | -export_type([execution_mode/0, application/0, language/0, webserver/0, cb_node/0]). 11 | -export_type([controller/0, compiler_adapters/0]). 12 | -------------------------------------------------------------------------------- /src/boss/boss_session_adapter.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_adapter). 2 | -export([behaviour_info/1]). 3 | 4 | %% @spec behaviour_info( atom() ) -> [ {Function::atom(), Arity::integer()} ] | undefined 5 | behaviour_info(callbacks) -> 6 | [ 7 | {start, 0}, 8 | {start, 1}, 9 | {stop, 1}, 10 | {init, 1}, 11 | {session_exists, 2}, 12 | {create_session, 3}, 13 | {lookup_session, 2}, 14 | {lookup_session_value, 3}, 15 | {set_session_value, 4}, 16 | {delete_session, 2}, 17 | {delete_session_value, 3} 18 | ]; 19 | behaviour_info(_Other) -> 20 | undefined. 21 | -------------------------------------------------------------------------------- /src/boss/boss_router_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_router_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | start_link([]). 11 | 12 | start_link(StartArgs) -> 13 | supervisor:start_link(?MODULE, StartArgs). 14 | 15 | init(StartArgs) -> 16 | {ok, {{one_for_one, 10, 10}, [ 17 | {router_controller, {boss_router_controller, start_link, [StartArgs]}, 18 | permanent, 19 | 2000, 20 | worker, 21 | [boss_router_controller]} 22 | ]}}. 23 | -------------------------------------------------------------------------------- /src/boss/boss_mq_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mq_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | start_link([]). 11 | 12 | start_link(StartArgs) -> 13 | supervisor:start_link({global, ?MODULE}, ?MODULE, StartArgs). 14 | 15 | init(StartArgs) -> 16 | {ok, {{one_for_one, 10, 10}, [ 17 | {mq_controller, {boss_mq_controller, start_link, [StartArgs]}, 18 | permanent, 19 | 2000, 20 | worker, 21 | [boss_mq_controller]} 22 | ]}}. 23 | 24 | -------------------------------------------------------------------------------- /src/boss/boss_service_handler.erl: -------------------------------------------------------------------------------- 1 | %%% @author mihawk 2 | %%% @copyright (C) 2012, mihawk 3 | %%% @doc 4 | %%% 5 | %%% @end 6 | %%% Created : 23 Jul 2012 by mihawk 7 | 8 | -module(boss_service_handler). 9 | -export([behaviour_info/1]). 10 | 11 | %% @spec behaviour_info( atom() ) -> [ {Function::atom(), Arity::integer()} ] | undefined 12 | behaviour_info(callbacks) -> 13 | [ 14 | {init, 0}, 15 | {handle_join, 3}, 16 | {handle_close, 4}, 17 | {handle_incoming, 4}, 18 | {handle_info, 2}, 19 | {terminate, 2} 20 | ]; 21 | behaviour_info(_Other) -> 22 | undefined. 23 | -------------------------------------------------------------------------------- /make-plt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | clear 3 | 4 | PLT=plt/cb.plt 5 | echo $PLT 6 | 7 | if [ ! -f $PLT ]; then 8 | dialyzer --build_plt --apps kernel stdlib mnesia inets ssl crypto \ 9 | erts public_key runtime_tools compiler asn1 hipe gs\ 10 | syntax_tools edoc xmerl public_key inets \ 11 | --statistics\ 12 | --output_plt $PLT 13 | rm deps/riak_core/ebin/*.beam 14 | echo "********************************************************************************" 15 | dialyzer --add_to_plt deps/*/ebin --plt $PLT 16 | echo "********************************************************************************" 17 | echo "" 18 | fi 19 | -------------------------------------------------------------------------------- /src/boss/boss_mail_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mail_sup). 2 | -author('emmiller@gmail.com'). 3 | 4 | -behaviour(supervisor). 5 | 6 | -export([start_link/0, start_link/1]). 7 | 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | start_link([]). 12 | 13 | start_link(StartArgs) -> 14 | supervisor:start_link({local, ?MODULE}, ?MODULE, StartArgs). 15 | 16 | init(StartArgs) -> 17 | {ok, {{one_for_one, 10, 10}, [ 18 | {mail_controller, {boss_mail_controller, start_link, [StartArgs]}, 19 | permanent, 20 | 2000, 21 | worker, 22 | [boss_mail_controller]} 23 | ]}}. 24 | -------------------------------------------------------------------------------- /src/boss/boss_session_mock_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_mock_sup). 2 | 3 | -behaviour(supervisor). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1]). 8 | 9 | start_link() -> 10 | start_link([]). 11 | 12 | start_link(StartArgs) -> 13 | supervisor:start_link({global, ?MODULE}, ?MODULE, StartArgs). 14 | 15 | init(StartArgs) -> 16 | {ok, {{one_for_one, 10, 10}, [ 17 | {session_mock_controller, {boss_session_mock_controller, start_link, [StartArgs]}, 18 | permanent, 19 | 2000, 20 | worker, 21 | [boss_session_mock_controller]} 22 | ]}}. 23 | -------------------------------------------------------------------------------- /src/boss/boss_translator_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_translator_sup). 2 | -author('emmiller@gmail.com'). 3 | 4 | -behaviour(supervisor). 5 | 6 | -export([start_link/0, start_link/1]). 7 | 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | start_link([]). 12 | 13 | start_link(StartArgs) -> 14 | supervisor:start_link(?MODULE, StartArgs). 15 | 16 | init(StartArgs) -> 17 | {ok, {{one_for_one, 10, 10}, [ 18 | {translator_controller, {boss_translator_controller, start_link, [StartArgs]}, 19 | permanent, 20 | 2000, 21 | worker, 22 | [boss_translator_controller]} 23 | ]}}. 24 | -------------------------------------------------------------------------------- /src/boss/mq_adapters/boss_mq_adapter_tinymq.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mq_adapter_tinymq). 2 | -export([start/0, start/1, stop/1]). 3 | -export([pull/4, poll/3, push/3, now/2]). 4 | 5 | start() -> 6 | start([]). 7 | 8 | start(_Options) -> 9 | application:start(tinymq), 10 | {ok, undefined}. 11 | 12 | stop(_) -> 13 | application:stop(tinymq), 14 | ok. 15 | 16 | pull(_, Channel, Timestamp, Subscriber) -> 17 | tinymq:subscribe(Channel, Timestamp, Subscriber). 18 | 19 | poll(_, Channel, Timestamp) -> 20 | tinymq:poll(Channel, Timestamp). 21 | 22 | push(_, Channel, Message) -> 23 | tinymq:push(Channel, Message). 24 | 25 | now(_, Channel) -> 26 | tinymq:now(Channel). 27 | -------------------------------------------------------------------------------- /test/boss_assert_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_assert_test). 2 | -include_lib("proper/include/proper.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | http_ok_test() -> 6 | 7 | Functions = [http_ok, http_partial_content, http_redirect, 8 | http_not_modified, http_bad_request, http_not_found, 9 | email_received, 10 | email_not_received,email_has_text,email_has_html, 11 | email_is_text_only, email_is_html_only, email_is_multipart], 12 | 13 | [begin 14 | ?assert(proper:check_spec({boss_assert, Function, 1}, 15 | [{to_file, user}])) 16 | end|| Function <-Functions], 17 | ok. 18 | -------------------------------------------------------------------------------- /src/boss/boss_html_errors_template.dtl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oops! 5 | 6 | 7 | 8 |

We got a problem!

9 | {% for filename, error in errors %} 10 |

In file {{ filename }}:

11 |
    12 | {% for lineno, module, msg in error %} 13 |
  • Line {{ lineno }}: {{ msg }}
  • 14 | {% endfor %} 15 |
16 | {% endfor %} 17 |
18 |
19 |

{% now "D, j F Y g:i a" %}

20 | 21 | 22 | -------------------------------------------------------------------------------- /ebin/erlydtl.app: -------------------------------------------------------------------------------- 1 | %% -*- mode: erlang -*- 2 | {application, erlydtl, 3 | [{description, "ErlyDTL implements most but not all of the Django Template Language"}, 4 | {vsn, "0.6.1"}, 5 | {modules, [ 6 | erlydtl, 7 | erlydtl_compiler, 8 | erlydtl_dateformat, 9 | erlydtl_dateformat_tests, 10 | erlydtl_deps, 11 | erlydtl_example_variable_storage, 12 | erlydtl_filters, 13 | erlydtl_functional_tests, 14 | erlydtl_parser, 15 | erlydtl_runtime, 16 | erlydtl_scanner, 17 | erlydtl_unittests 18 | ]}, 19 | {applications, [kernel, stdlib]}, 20 | {registered, []} 21 | ]}. 22 | -------------------------------------------------------------------------------- /src/boss/filters/boss_lang_filter.erl: -------------------------------------------------------------------------------- 1 | -module(boss_lang_filter). 2 | -export([config_default_value/0, config_key/0, before_filter/2, after_filter/3]). 3 | 4 | config_default_value() -> auto. 5 | config_key() -> lang. 6 | 7 | before_filter(auto, RequestContext) -> 8 | {ok, proplists:delete(language, RequestContext)}; 9 | before_filter(Language, RequestContext) -> 10 | {ok, proplists:delete(language, RequestContext) ++ [{language, Language}]}. 11 | 12 | after_filter({ok, Payload, Headers}, auto, _) -> 13 | {ok, Payload, Headers}; 14 | after_filter({ok, Payload, Headers}, Language, _) -> 15 | {ok, Payload, boss_web_controller:merge_headers(Headers, [{"Content-Language", Language}])}; 16 | after_filter(Other, _, _) -> Other. 17 | -------------------------------------------------------------------------------- /src/boss/boss_html_error_template.dtl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Oops! 5 | 6 | 7 | 8 |
9 |

We got a problem!

10 |
{{ extra_message }}
11 |

Error:

12 |
{{ error }}
13 |

Request:

14 |
{{ request }}
15 |

Application info:

16 |
{{ appinfo }}
17 |

{% now "D, j F Y g:i a" %}

18 |
19 | 20 | 21 | -------------------------------------------------------------------------------- /ebin/medici.app: -------------------------------------------------------------------------------- 1 | %% Medici application resource file. 2 | %% 3 | %% Add runtime options in the list value for the options property of 4 | %% env or set them via a config file or the medici:start() function. 5 | %% 6 | {application, medici, 7 | [{description, "Medici Tokyo Tyrant interface"}, 8 | {vsn, "0.6"}, 9 | {modules, [ 10 | medici, 11 | medici_app, 12 | medici_sup, 13 | medici_controller, 14 | medici_native_controller, 15 | medici_conn_sup, 16 | medici_conn, 17 | medici_native_conn, 18 | medici_port_srv, 19 | medici_port_sup, 20 | principe, 21 | principe_table]}, 22 | {registered, []}, 23 | {applications, [kernel, stdlib]}, 24 | {mod, {medici_app, []}}, 25 | {env, [{options, []}]} 26 | ]}. 27 | -------------------------------------------------------------------------------- /src/boss/compiler_adapters/boss_compiler_adapter_elixir.erl: -------------------------------------------------------------------------------- 1 | -module(boss_compiler_adapter_elixir). 2 | -compile(export_all). 3 | 4 | -define(SEPARATOR, "."). 5 | 6 | file_extensions() -> ["ex"]. 7 | 8 | controller_module(AppName, Controller) -> 9 | lists:concat(["Elixir", ?SEPARATOR, inflector:camelize(atom_to_list(AppName)), ?SEPARATOR, 10 | inflector:camelize(Controller), "Controller"]). 11 | 12 | module_name_for_file(AppName, File) -> 13 | "Elixir" ++ ?SEPARATOR ++ inflector:camelize(atom_to_list(AppName)) ++ 14 | ?SEPARATOR ++ inflector:camelize(filename:basename(File, ".ex")). 15 | 16 | compile_controller(File, Options) -> 17 | boss_elixir_compiler:compile(File, Options). 18 | 19 | compile(File, Options) -> 20 | boss_elixir_compiler:compile(File, Options). 21 | -------------------------------------------------------------------------------- /src/boss/boss_session_sup.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_sup). 2 | -author('Jose Luis Gordo Romero jose.gordo@tractis.com bassed on Evan Miller boss_db_sup.erl'). 3 | 4 | -behaviour(supervisor). 5 | 6 | -export([start_link/0, start_link/1]). 7 | 8 | -export([init/1]). 9 | 10 | start_link() -> 11 | start_link([]). 12 | 13 | start_link(StartArgs) -> 14 | supervisor:start_link({local, ?MODULE}, ?MODULE, StartArgs). 15 | 16 | init(StartArgs) -> 17 | Args = [{name, {local, boss_session_pool}}, 18 | {worker_module, boss_session_controller}, 19 | {size, 20}, 20 | {max_overflow, 40} 21 | | StartArgs], 22 | PoolSpec = {session_controller, 23 | {poolboy, start_link, [Args]}, 24 | permanent, 2000, worker, [poolboy]}, 25 | {ok, {{one_for_one, 10, 10}, [PoolSpec]}}. 26 | -------------------------------------------------------------------------------- /priv/elixir/boss/flash.ex: -------------------------------------------------------------------------------- 1 | defmodule Boss.Flash do 2 | @moduledoc """ 3 | Storage for temporary ("Flash") messages associated with sessions. 4 | """ 5 | 6 | @spec get_and_clear(:string) :: [any] 7 | @doc "Retrieve and remove all messages for the given session_id" 8 | def get_and_clear(session_id) do 9 | :boss_flash.get_and_clear(:erlang.binary_to_list(session_id)) 10 | end 11 | 12 | @spec add(:string, :atom, any) :: :ok | {:error, any} 13 | @doc "Add a message to the flash message stack for session_id" 14 | def add(session_id, type, title) do 15 | :boss_flash.add(:erlang.binary_to_list(session_id)) 16 | end 17 | 18 | @spec add(:string, :atom, any, any) :: :ok | {:error, any} 19 | @doc "Add a message to the flash message stack for session_id" 20 | def add(session_id, type, title, message) do 21 | :boss_flash.add(:erlang.binary_to_list(session_id), type, title, message) 22 | end 23 | end 24 | -------------------------------------------------------------------------------- /priv/elixir/boss/repo.ex: -------------------------------------------------------------------------------- 1 | defmodule Boss.Repo.Sup do 2 | use Supervisor.Behaviour 3 | 4 | def start_link do 5 | :supervisor.start_link({ :local, __MODULE__ }, __MODULE__, []) 6 | end 7 | 8 | def init([]) do 9 | tree = [ worker(Boss.Repo, []) ] 10 | supervise(tree, strategy: :one_for_all) 11 | end 12 | end 13 | 14 | defmodule Boss.Repo do 15 | use Ecto.Repo, adapter: Ecto.Adapters.Postgres 16 | 17 | def url do 18 | user = :erlang.list_to_binary(:boss_env.get_env(:db_username, :erlang.binary_to_list("root"))) 19 | pass = :erlang.list_to_binary(:boss_env.get_env(:db_password, [])) 20 | host = :erlang.list_to_binary(:boss_env.get_env(:db_host, :erlang.binary_to_list("localhost"))) 21 | port = :boss_env.get_env(:db_port, 5432) 22 | name = :erlang.list_to_binary(:boss_env.get_env(:db_database, :erlang.binary_to_list("boss"))) 23 | 24 | "ecto://#{user}:#{pass}@#{host}:#{port}/#{name}" 25 | end 26 | end 27 | -------------------------------------------------------------------------------- /skel/src/view/lib/README: -------------------------------------------------------------------------------- 1 | This directory contains: 2 | 3 | * tag_html/ - template files which are compiled to tags. If you have a file 4 | called "foo.html" and then call {% foo bar=1 %} from another template, the 5 | contents of "foo.html" will be evaluated with the "bar" variable set to 1. 6 | 7 | * tag_modules/ - Erlang modules that export functions to implement tags. If 8 | a module in this directory exports foo/1, then {% foo bar=1 %} will call 9 | 10 | Module:foo([{bar, 1}]) 11 | 12 | * filter_modules/ - Erlang modules that export functions to implement filters. 13 | If a module in this directory exports foo/1, then {% "Example"|foo %} will call 14 | 15 | Module:foo(<<"Example">>) 16 | 17 | If module in this directory exports foo/2, then {% "Example"|foo:42 %} will call 18 | 19 | Module:foo(<<"Example">>, 42) 20 | 21 | You can specify external tag and filter modules in the configuration via the 22 | template_tag_modules and template_filter_modules options. 23 | -------------------------------------------------------------------------------- /src/boss/boss.erl: -------------------------------------------------------------------------------- 1 | %% @author Evan Miller 2 | %% @copyright 2009 author. 3 | 4 | %% @doc TEMPLATE. 5 | 6 | -module(boss). 7 | -author('Evan Miller '). 8 | -export([start/0, stop/0]). 9 | 10 | ensure_started(App) -> 11 | case application:start(App) of 12 | ok -> 13 | ok; 14 | {error, {already_started, App}} -> 15 | ok 16 | end. 17 | 18 | %% @spec start() -> ok 19 | %% @doc Start the boss server. 20 | start() -> 21 | is_compatable(erlang:system_info(otp_release)), 22 | ensure_started(crypto), 23 | ensure_started(mimetypes), 24 | application:start(boss). 25 | 26 | %% @spec stop() -> ok 27 | %% @doc Stop the boss server. 28 | stop() -> 29 | Res = application:stop(boss), 30 | application:stop(mimetypes), 31 | application:stop(crypto), 32 | Res. 33 | 34 | is_compatable("R16B03") -> 35 | lager:emergency("Chicago Boss is not comptable with 16R03"), 36 | erlang:halt(1, "Chicago Boss is not comptable with 16R03"); 37 | is_compatable(_) -> 38 | ok. 39 | -------------------------------------------------------------------------------- /skel/priv/boss.routes: -------------------------------------------------------------------------------- 1 | % Routes file. 2 | 3 | % Formats: 4 | % {"/some/route", [{controller, "Controller"}, {action, "Action"}]}. 5 | % {"/some/route", [{controller, "Controller"}, {action, "Action"}, {id, "42"}]}. 6 | % {"/(some|any)/route/(\\d+)", [{controller, '$1'}, {action, "Action"}, {id, '$2'}]}. 7 | % {"/some/route/(?\\d+)", [{controller, "Controller"}, {action, "Action"}, {id, '$route_id'}]}. 8 | % {"/some/route", [{application, some_app}, {controller, "Controller"}, {action, "Action"}, {id, "42"}]}. 9 | % 10 | % {404, [{controller, "Controller"}, {action, "Action"}]}. 11 | % {404, [{controller, "Controller"}, {action, "Action"}, {id, "42"}]}. 12 | % {404, [{application, some_app}, {controller, "Controller"}, {action, "Action"}, {id, "42"}]}. 13 | % 14 | % Note that routing across applications results in a 302 redirect. 15 | 16 | % Front page 17 | % {"/", [{controller, "world"}, {action, "hello"}]}. 18 | 19 | % 404 File Not Found handler 20 | % {404, [{controller, "world"}, {action, "lost"}]}. 21 | 22 | % 500 Internal Error handler (only invoked in production) 23 | % {500, [{controller, "world"}, {action, "calamity"}]}. 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2009-2011 Evan Miller (except where otherwise noted) 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README_LFE.md: -------------------------------------------------------------------------------- 1 | Using LFE with Chicago Boss 2 | == 3 | 4 | Lisp-Flavoured Erlang is a Lisp-y language for the Erlang VM, which you're 5 | welcome to use if you like both parentheses and metaprogramming. More info on 6 | the LFE project is here: http://lfe.github.io/ 7 | 8 | Setup 9 | -- 10 | 11 | LFE is included with Chicago Boss. You don't need to do anything special except 12 | give your source files the extension ".lfe" instead of ".erl". 13 | 14 | Controller API 15 | -- 16 | 17 | The controller API is pretty much the same as the CB Erlang API, but note that 18 | LFE does not support the auto-routing magic where URL parameters are inferred 19 | from the controller arguments. 20 | 21 | Example controller module: 22 | 23 | (defmodule (myapp_cool_controller req session_id) 24 | (export all)) 25 | 26 | (defun index (http_method tokens) 27 | (tuple 'output '"Yeee-haw!")) 28 | 29 | ...then visit /cool/index for a special greeting. 30 | 31 | Model Files 32 | -- 33 | 34 | LFE is not supported in BossDB model files (src/model). 35 | 36 | Library Files 37 | -- 38 | 39 | LFE is supported for library files sitting in your project's src/lib/ 40 | directory. 41 | -------------------------------------------------------------------------------- /src/boss/model_manager_adapters/boss_model_manager_ecto.erl: -------------------------------------------------------------------------------- 1 | -module(boss_model_manager_ecto). 2 | -behaviour(boss_model_manager_adapter). 3 | 4 | -export([ 5 | start/0, 6 | stop/0, 7 | compile/2, 8 | edoc_module/2, 9 | is_model_instance/2, 10 | dummy_instance/1, 11 | to_json/1 %, from_json/1 12 | ]). 13 | 14 | start() -> 15 | pgsql = boss_env:get_env(db_adapter, pgsql), 16 | 'Elixir.Boss.Repo.Sup':start_link(). 17 | 18 | stop() -> 19 | ok. 20 | 21 | compile(ModulePath, Options) -> 22 | Files = [list_to_binary(ModulePath)], 23 | [CompiledModule|_] = case proplists:get_value(out_dir, Options) of 24 | undefined -> 25 | 'Elixir.Kernel.ParallelCompiler':files(Files); 26 | OutDir -> 27 | 'Elixir.Kernel.ParallelCompiler':files_to_path(Files, OutDir) 28 | end, 29 | {ok, CompiledModule}. 30 | 31 | edoc_module(_ModulePath, _Options) -> 32 | not_implemented. 33 | 34 | is_model_instance(Object, AvailableModels) -> 35 | lists:member(element(1, Object), AvailableModels). 36 | 37 | dummy_instance(Model) -> 38 | Model:new([]). 39 | 40 | to_json(Object) -> 41 | {struct, Object:'__record__'(fields)}. 42 | -------------------------------------------------------------------------------- /travis-dialyzer.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | 4 | PLT=plt/cb-$RANDOM.plt 5 | echo "PLT File $PLT" 6 | export PATH=$PATH:/usr/local/bin:/usr/bin 7 | echo "Building PLT, may take a few minutes" 8 | dialyzer --build_plt --apps kernel stdlib\ 9 | --output_plt $PLT > /dev/null 10 | for app in mnesia inets ssl crypto \ 11 | erts public_key runtime_tools compiler asn1 hipe\ 12 | syntax_tools 13 | do 14 | echo $app 15 | dialyzer --add_to_plt --apps $app\ 16 | --plt $PLT > /dev/null 17 | done 18 | rm -f deps/riak_*/ebin/*_pb.beam 19 | echo "********************************************************************************" 20 | for app in $(ls deps/) 21 | do 22 | echo "Adding $app" 23 | dialyzer --add_to_plt --apps deps/$app \ 24 | --plt $PLT > /dev/null 25 | 26 | 27 | done 28 | echo "********************************************************************************" 29 | echo "" 30 | 31 | dialyzer ebin/ \ 32 | -Werror_handling \ 33 | -Wno_undefined_callbacks \ 34 | -Wrace_conditions \ 35 | --statistics -n \ 36 | --fullpath \ 37 | -n \ 38 | --plt $PLT # -Wunmatched_returns 39 | # 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/boss/boss_mail_controller.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mail_controller). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 8 | 9 | -record(state, {driver, connection}). 10 | 11 | start_link() -> 12 | start_link([]). 13 | 14 | start_link(Args) -> 15 | gen_server:start_link({local, boss_mail}, ?MODULE, Args, []). 16 | 17 | init(Options) -> 18 | MailDriver = proplists:get_value(driver, Options, boss_mail_driver_smtp), 19 | {ok, Conn} = MailDriver:start(), 20 | {ok, #state{driver = MailDriver, connection = Conn}}. 21 | 22 | handle_call({deliver, FromAddress, ToAddress, BodyFun, ResultFun}, _From, State) -> 23 | Driver = State#state.driver, 24 | {reply, Driver:deliver(State#state.connection, 25 | FromAddress, ToAddress, BodyFun, ResultFun), State}. 26 | 27 | handle_cast(_Request, State) -> 28 | {noreply, State}. 29 | 30 | terminate(_Reason, State) -> 31 | Driver = State#state.driver, 32 | Driver:stop(State#state.connection). 33 | 34 | code_change(_OldVsn, State, _Extra) -> 35 | {ok, State}. 36 | 37 | handle_info(_Info, State) -> 38 | {noreply, State}. 39 | -------------------------------------------------------------------------------- /README_TESTS.md: -------------------------------------------------------------------------------- 1 | Testing with EUnit and Proper 2 | 3 | I have been adding tests to ChicagoBoss and BossDB via Eunit and 4 | Proper. There are generally 2 types of tests unit tests with EUnit and 5 | property based tests with Proper. 6 | 7 | Propety based tests randomly generate a large number of random test 8 | cases and apply the code then validate the responses against rules. In 9 | some cases these tests can be derived directly from the function's 10 | -spec declaration, in others you may need to write an explicit 11 | property to test the issue. 12 | 13 | If you are writting properties you can run them by including the file 14 | "std.hrl" in your test file then creating a function like this, Where 15 | each element in the list is a function or a tuple showing the name and 16 | arity of a function. 17 | 18 | ---- 19 | spec_test_() -> 20 | gen([ 21 | fun prop_pull_recieve/0, 22 | fun prop_pull_recieve_timeout/0, 23 | fun prop_pull_recieve_error/0, 24 | {stop, 0}, 25 | {convert_to_ms, 1}, 26 | {make_queue_options, 0} 27 | ], boss_mq). 28 | 29 | ---- 30 | 31 | By default it will run the tests in parallel and run 100 instances of 32 | each test. You can adjust those if you need to. 33 | -------------------------------------------------------------------------------- /src/boss/boss_web.hrl: -------------------------------------------------------------------------------- 1 | 2 | -define(DEBUGPRINT(A), error_logger:info_report("~~o)> " ++ A)). 3 | -define(BUILTIN_CONTROLLER_FILTERS, [boss_lang_filter, boss_cache_page_filter, boss_cache_vars_filter]). 4 | -define(DEFAULT_WEB_SERVER, cowboy). 5 | -define(PRINT(N,V), 6 | lager:notice(" ~s ~p", [ N,V])). 7 | -include_lib("eunit/include/eunit.hrl"). 8 | 9 | -record(boss_app_info, { 10 | application ::atom(), 11 | base_url ::string(), 12 | static_prefix ::string(), 13 | doc_prefix ::string(), 14 | domains ::all|[string()], 15 | init_data ::[{atom(), any()}] , 16 | router_sup_pid ::pid(), 17 | router_pid ::pid(), 18 | translator_sup_pid ::pid(), 19 | translator_pid ::pid(), 20 | model_modules = [] ::[atom()], 21 | view_modules = [] ::[atom()], 22 | controller_modules = [] ::[atom()] 23 | }). 24 | 25 | -record(state, { 26 | applications = [] ::[ #boss_app_info{}], 27 | service_sup_pid ::pid(), 28 | http_pid ::pid(), 29 | smtp_pid ::pid(), 30 | is_master_node = false ::boolean() 31 | }). 32 | 33 | 34 | -ifdef(TEST). 35 | -compile(export_all). 36 | -endif. 37 | -------------------------------------------------------------------------------- /src/boss/boss_model_manager.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% 3 | %% Module: boss_model -- description 4 | %% Created: 01-MAY-2012 16:32 5 | %% Author: tmr 6 | %% 7 | 8 | -module (boss_model_manager). 9 | -behaviour (boss_model_manager_adapter). 10 | -export ([ 11 | start/0, 12 | stop/0, 13 | compile/2, 14 | edoc_module/2, 15 | is_model_instance/2, 16 | dummy_instance/1, 17 | to_json/1 %, from_json/1 18 | ]). 19 | 20 | get_adapter() -> 21 | list_to_atom (lists:concat (["boss_model_manager_", 22 | boss_env:get_env (model_manager, boss_db)])). 23 | 24 | start() -> 25 | (get_adapter ()):start(). 26 | 27 | stop() -> 28 | (get_adapter ()):stop(). 29 | 30 | compile(ModulePath, CompilerOptions) -> 31 | (get_adapter ()):compile (ModulePath, CompilerOptions). 32 | 33 | edoc_module(ModulePath, Options) -> 34 | (get_adapter ()):edoc_module (ModulePath, Options). 35 | 36 | is_model_instance(Object, AvailableModels) -> 37 | (get_adapter ()):is_model_instance (Object, AvailableModels). 38 | 39 | dummy_instance(Model) -> 40 | (get_adapter ()):dummy_instance (Model). 41 | 42 | to_json(Object) -> 43 | (get_adapter ()):to_json (Object). 44 | 45 | %from_json (Data) -> 46 | % (get_adapter ()):from_json (Data). 47 | 48 | %% vim: fdm=syntax:fdn=3:tw=74:ts=2:syn=erlang 49 | -------------------------------------------------------------------------------- /src/boss/boss_translator.erl: -------------------------------------------------------------------------------- 1 | %% @doc Chicago Boss translator service 2 | 3 | -module(boss_translator). 4 | 5 | -export([start/0, start/1, stop/0]). 6 | 7 | -export([ 8 | is_loaded/2, 9 | lookup/3, 10 | fun_for/2, 11 | reload/2, 12 | reload_all/1 13 | ]). 14 | 15 | start() -> 16 | start([]). 17 | 18 | start(Options) -> 19 | boss_translator_sup:start_link(Options). 20 | 21 | stop() -> 22 | ok. 23 | 24 | %% @spec lookup(Key::string(), Locale::string()) -> Translation::string() | undefined 25 | lookup(Pid, Key, Locale) -> 26 | gen_server:call(Pid, {lookup, Key, Locale}). 27 | 28 | %% @spec is_loaded(Locale::string()) -> true | false 29 | is_loaded(Pid, Locale) -> 30 | gen_server:call(Pid, {is_loaded, Locale}). 31 | 32 | %% @spec reload(Locale::string()) -> ok | {error, Reason} 33 | reload(Pid, Locale) -> 34 | gen_server:call(Pid, {reload, Locale}). 35 | 36 | %% @spec reload_all() -> ok | {error, Reason} 37 | reload_all(Pid) -> 38 | gen_server:call(Pid, reload_all). 39 | 40 | %% @spec fun_for(Locale::string()) -> TranslationFun::function() | none 41 | fun_for(Pid, Locale) -> 42 | case is_loaded(Pid, Locale) of 43 | true -> fun(String) -> ?MODULE:lookup(Pid, String, Locale) end; 44 | false -> none 45 | end. 46 | -------------------------------------------------------------------------------- /src/boss/compiler_adapters/boss_compiler_adapter_lfe.erl: -------------------------------------------------------------------------------- 1 | -module(boss_compiler_adapter_lfe). 2 | -compile(export_all). 3 | 4 | file_extensions() -> ["lfe"]. 5 | 6 | controller_module(AppName, Controller) -> lists:concat([AppName, "_", Controller, "_controller"]). 7 | 8 | module_name_for_file(_AppName, File) -> filename:basename(File, ".lfe"). 9 | 10 | compile_controller(File, Options) -> 11 | do_compile(File, Options). 12 | 13 | compile(File, Options) -> 14 | do_compile(File, Options). 15 | 16 | do_compile(File, Options) -> 17 | CompilerOptions = lfe_compiler_options(Options), 18 | case lfe_comp:file(File, CompilerOptions) of 19 | {ok, Module, Binary, _Warnings} -> 20 | {module, Module} = code:load_binary(Module, File, Binary), 21 | ok = case proplists:get_value(out_dir, Options) of 22 | undefined -> ok; 23 | OutDir -> 24 | OutFile = filename:join([OutDir, filename:basename(File, ".lfe") ++ ".beam"]), 25 | file:write_file(OutFile, Binary) 26 | end, 27 | {ok, Module}; 28 | Other -> 29 | Other 30 | end. 31 | 32 | lfe_compiler_options(Options) -> 33 | [verbose, return, binary] ++ proplists:get_value(compiler_options, Options, []). 34 | -------------------------------------------------------------------------------- /ct/boss_mq_SUITE.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author Zachary Kessin <> 3 | %%% @copyright (C) 2013, Zachary Kessin 4 | %%% @doc 5 | %%% 6 | %%% @end 7 | %%% Created : 16 Dec 2013 by Zachary Kessin <> 8 | %%%------------------------------------------------------------------- 9 | -module(boss_mq_SUITE). 10 | 11 | %% Note: This directive should only be used in test suites. 12 | -compile(export_all). 13 | 14 | -include_lib("common_test/include/ct.hrl"). 15 | 16 | suite() -> 17 | [{timetrap,{minutes,10}}]. 18 | 19 | 20 | init_per_suite(Config) -> 21 | Config. 22 | 23 | 24 | end_per_suite(_Config) -> 25 | ok. 26 | 27 | init_per_group(_GroupName, Config) -> 28 | Config. 29 | 30 | end_per_group(_GroupName, _Config) -> 31 | ok. 32 | 33 | 34 | init_per_testcase(_TestCase, Config) -> 35 | Config. 36 | 37 | 38 | end_per_testcase(_TestCase, _Config) -> 39 | ok. 40 | 41 | groups() -> 42 | []. 43 | 44 | all() -> 45 | 46 | [test_poll]. 47 | 48 | 49 | my_test_case() -> 50 | []. 51 | 52 | 53 | my_test_case(_Config) -> 54 | ok. 55 | test_poll() -> 56 | []. 57 | test_poll(_Config) -> 58 | boss_mq:push("test", "Test Message"), 59 | {ok, _T, "Test Message"} = boss_mq:poll("test", last), 60 | ok. 61 | -------------------------------------------------------------------------------- /priv/elixir/boss/session.ex: -------------------------------------------------------------------------------- 1 | defmodule Boss.Session do 2 | @moduledoc """ 3 | Get and set values in Boss's session storage. 4 | """ 5 | 6 | @doc """ 7 | Retrieve all keys and values in the session storage associated with a given 8 | session ID. Returns a proplist 9 | """ 10 | @spec get_session_data(:string) :: [{any, any}] 11 | def get_session_data(session_id) do 12 | :boss_session.get_session_data(:erlang.binary_to_list(session_id)) 13 | end 14 | 15 | @doc """ 16 | Retrieve the value for a given key associated with a given session ID 17 | """ 18 | @spec get_session_data(:string, :term) :: any 19 | def get_session_data(session_id, key) do 20 | :boss_session.get_session_data(:erlang.binary_to_list(session_id), key) 21 | end 22 | 23 | @doc """ 24 | Set the value for a key associated with a given session ID 25 | """ 26 | @spec set_session_data(:string, :term, :term) :: :ok | {:error, any} 27 | def set_session_data(session_id, key, value) do 28 | :boss_session.set_session_data(:erlang.binary_to_list(session_id), key, value) 29 | end 30 | 31 | @doc """ 32 | Remove any value for the given key and session ID 33 | """ 34 | @spec remove_session_data(:string, :term) :: :ok | {:error, any} 35 | def remove_session_data(session_id, key) do 36 | :boss_session.remove_session_data(:erlang.binary_to_list(session_id), key) 37 | end 38 | end 39 | -------------------------------------------------------------------------------- /test/boss_files_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_files_test). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("../src/boss/boss_web.hrl"). 5 | 6 | make_extentions_test() -> 7 | Extentions = boss_files:make_extentions(), 8 | 9 | ?assert(is_list(Extentions)). 10 | 11 | lookup_module_by_adapater_test() -> 12 | ?assertEqual([], boss_files:lookup_module_by_adapater([], [], [], undefined)), 13 | Modules = boss_files:lookup_module_by_adapater(test_app, "test_file", [], 14 | boss_compiler_adapter_erlang), 15 | ?assertEqual(["test_file"], Modules). 16 | 17 | 18 | make_modules_test() -> 19 | _Dir = ".", 20 | _Application = 'test', 21 | _ModuleAcc = [], 22 | _ExtentionPropList = boss_files:make_extentions(), 23 | ok. 24 | 25 | 26 | 27 | find_file_4_test() -> 28 | ?assertEqual([test], boss_files:find_file([], undefined, [test], undefined)). 29 | 30 | 31 | make_modules_itterator_test()-> 32 | Extentions = boss_files:make_extentions(), 33 | Itter = boss_files:make_modules_itterator(Extentions, test), 34 | ?assert(is_function(Itter, 2)), 35 | ?assertEqual([test], Itter(".test", [test])), 36 | ?assertEqual([test], Itter(".", [test])), 37 | ?assertEqual(["test"], Itter("test.erl", [])), 38 | ?assertEqual(["Elixir.Test.Test"], Itter("test.ex", [])), 39 | ?assertEqual(["test"], Itter("test.lfe", [])), 40 | ?assertEqual([], Itter("test", [])). 41 | 42 | 43 | -------------------------------------------------------------------------------- /test/boss_mq_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mq_test). 2 | -include("std.hrl"). 3 | 4 | 5 | convert_to_ms_test() -> 6 | ?assertEqual(infinity, boss_mq:convert_to_ms(infinity)), 7 | ?assertEqual(1000, boss_mq:convert_to_ms(1)). 8 | 9 | 10 | spec_test_() -> 11 | gen([ 12 | fun prop_pull_recieve/0, 13 | fun prop_pull_recieve_timeout/0, 14 | fun prop_pull_recieve_error/0, 15 | {stop, 0}, 16 | {convert_to_ms, 1}, 17 | {make_queue_options, 0} 18 | ], boss_mq). 19 | 20 | -type date() :: {1970..2030, 1..12, 1..31}. 21 | -type time() :: {0..23, 0..59, 0..60}. 22 | -type datetime() :: {date(), time()}. 23 | prop_pull_recieve() -> 24 | ?FORALL({Msg, PullTime}, 25 | {{refrence, datetime(), jsx:json_term()}, 26 | datetime()}, 27 | begin 28 | self() ! Msg, 29 | {ok, NewTimeStamp, Messages} = boss_mq:pull_recieve(100, {ok, PullTime}), 30 | {refrence, NewTimeStamp, Messages} =:= Msg 31 | 32 | end). 33 | prop_pull_recieve_timeout() -> 34 | ?FORALL({ Pulltime}, 35 | {datetime()}, 36 | begin 37 | {ok, Pulltime, []} =:= boss_mq:pull_recieve(1, {ok, Pulltime}) 38 | end). 39 | 40 | prop_pull_recieve_error() -> 41 | ?FORALL( 42 | Error, 43 | {error, string()}, 44 | 45 | begin 46 | Error =:= boss_mq:pull_recieve(1, Error) 47 | end). 48 | -------------------------------------------------------------------------------- /windows-make.bat: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | REM @echo 1st:%1 2nd:%2 3rd:%3 4th:%4 3 | 4 | SET PWD=%cd% 5 | SET APPNAME=%3 6 | for %%* in (.) do set PARENTDIR=%%~n* 7 | 8 | IF /I [%1] EQU [] CALL :all 9 | IF /I "%1"=="all" CALL :all 10 | IF /I "%1"=="boss" CALL :boss 11 | IF /I "%1"=="clean" CALL :clean 12 | IF /I "%1"=="get-deps" CALL :get-deps 13 | IF /I "%1"=="deps" CALL :deps 14 | IF /I "%1"=="test" CALL :test 15 | IF /I "%1"=="app" IF /I "%2"=="project" IF [%3] NEQ [] CALL :app 16 | 17 | :: End of main program 18 | GOTO End 19 | 20 | 21 | :all 22 | CALL rebar.cmd get-deps 23 | CALL rebar.cmd compile 24 | GOTO :EOF 25 | 26 | :boss 27 | CALL rebar.cmd compile skip_deps=true 28 | GOTO :EOF 29 | 30 | :clean 31 | CALL rebar.cmd clean 32 | GOTO :EOF 33 | 34 | :get-deps 35 | CALL rebar.cmd get-deps 36 | GOTO :EOF 37 | 38 | :deps 39 | CALL rebar.cmd compile 40 | GOTO :EOF 41 | 42 | :test 43 | CALL rebar.cmd skip_deps=true eunit 44 | GOTO :EOF 45 | 46 | :: example how to invoke: windows-make.bat app PROJECT=awesomename 47 | :app 48 | CALL rebar.cmd create template=skel dest=../%APPNAME% src=../%PARENTDIR% appid=%APPNAME% skip_deps=true 49 | mkdir ..\%APPNAME%\deps\ 50 | :: copy ChicagoBoss into deps\boss of the new app 51 | xcopy ..\%PARENTDIR%\*.* ..\%APPNAME%\deps\boss\ /E /H /R /Y 52 | :: move boss deps folders into app deps folder 53 | SET src_folder=..\%APPNAME%\deps\boss\deps 54 | SET tar_folder=..\%APPNAME%\deps 55 | for /f %%a IN ('dir "%src_folder%" /b') do move %src_folder%\%%a %tar_folder% 56 | GOTO :EOF 57 | 58 | :End 59 | :: End of batch file 60 | -------------------------------------------------------------------------------- /CODING_STANDARDS.md: -------------------------------------------------------------------------------- 1 | # Contributing to Chicago Boss 2 | Zachary Kessin 3 | Dec 30 2013 4 | 5 | If you would like to contribute to Chicago Boss or the related 6 | projects including boss_db, tiny_pq and so on then we welcome your 7 | contributions. However in order to keep our code quality as high as 8 | we can we ask that everyone read this guide before sending pull 9 | requests. (And if you don't get it perfect we won't bite your head 10 | off) 11 | 12 | ## Simple Pull Requests and named branches 13 | 14 | Please keep your pull requests simple, if you want to make changes 15 | to the mongo db driver and add a driver for couch db do the first on a 16 | *mongo* branch and the second on a *couch* branch, and then send 2 17 | pull requests. 18 | 19 | ## Make it pass Dialyzer 20 | 21 | I run dialyzer on everything, if it breaks it probably means that I 22 | have more work to do, so try to make it pass. 23 | 24 | ## Tests are required 25 | I have been adding tests with proper to boss_db and they will show up 26 | in boss sooner or later. While historically most of the code does not 27 | have any sort of tests we need to start adding it. Unit tests are 28 | good, property based tests are better. 29 | 30 | ## No deeply nested code 31 | If your code has more than 1 level of nested case, fun or foldl, It 32 | needs to be re factored. Really keep the functions short. 33 | 34 | ## Documentation is required 35 | Please use EDoc (http://www.erlang.org/doc/apps/edoc/chapter.html) 36 | compatible comments for your functions and modules. In-code comments 37 | are welcome too. 38 | -------------------------------------------------------------------------------- /src/boss/boss_migrate.erl: -------------------------------------------------------------------------------- 1 | %% boss_migrate handles the chicago boss specific bits of migrations, 2 | %% in collaboration with boss_db. 3 | 4 | -module(boss_migrate). 5 | 6 | -export([make/2, 7 | run/1, 8 | redo/2]). 9 | 10 | %% Returns a sorted list of all files in priv/migrations/. 11 | migration_list(App) -> 12 | lists:sort(filelib:wildcard(filename:join([boss_files:root_priv_dir(App), "migrations", "*.erl"]))). 13 | 14 | %% Create a migration. MigrationName is an atom to use as the name of 15 | %% the migration. 16 | make(App, MigrationName) when is_atom(MigrationName) -> 17 | {MegaSeconds, Seconds, _Microsecs} = erlang:now(), 18 | Filename = filename:join([boss_files:root_priv_dir(App), "migrations", 19 | io_lib:format("~p~p_~s.erl", [MegaSeconds, Seconds, MigrationName])]), 20 | file:write_file(Filename, io_lib:format("%% Migration: ~p~n~n{~p,~n fun(up) -> undefined;~n (down) -> undefined~n end}.~n", [MigrationName, MigrationName])). 21 | 22 | %% Run the migrations. 23 | run(App) -> 24 | boss_db:migrate(load_migrations(App)). 25 | 26 | % Redo {down, up} a specific migration. 27 | redo(App, Tag) -> 28 | Fun = proplists:get_value(Tag, load_migrations(App)), 29 | boss_db:transaction(fun () -> 30 | boss_db:migrate({Tag, Fun}, down), 31 | boss_db:migrate({Tag, Fun}, up) 32 | end). 33 | 34 | load_migrations(App) -> 35 | lists:map(fun(File) -> 36 | lager:info("Reading migration file: ~p~n", [File]), 37 | {ok, Terms} = file:script(File), 38 | Terms 39 | end, migration_list(App)). 40 | 41 | -------------------------------------------------------------------------------- /doc-src/api-mq.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

BossMQ is an abstraction layer for channel-based messaging that can be used to implement real-time notifications (i.e. Comet). With BossMQ, any controller action can function as a long-polling endpoint simply by calling boss_mq:pull/2:

4 | 5 |
 6 | receive_chat('GET', []) ->
 7 |    {ok, Timestamp, Messages} = boss_mq:pull("my-channel", now)
 8 |    {output, Messages}.
 9 | 
10 | 11 |

The call to pull/2 blocks until a message arrives on "my-channel". Because of Erlang's lightweight process model, you usually don't need to worry if pull/2 takes a long time to complete.

12 | 13 |

To send a message to a channel, you call boss_mq:push/2:

14 | 15 |
16 | boss_mq:push("my-channel", <<"Secret Message">>) 17 |
18 | 19 |

Currently, only an in-memory message queue is supported, so all messaging must occur in the same CB cluster. Additional adapters will be added in the future to support more complex installations.

20 | 21 |

boss_mq API

22 | 23 | {% for function in functions %} 24 | {% if function.description_long %} 25 |
26 |
27 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 28 |
29 |

{{ function.description_long }}

30 |
31 | {% endif %} 32 | {% endfor %} 33 | 34 | {% endblock %} 35 | -------------------------------------------------------------------------------- /src/boss/boss_flash.erl: -------------------------------------------------------------------------------- 1 | -module(boss_flash). 2 | -export([get_and_clear/1, add/5, add/4, add/3]). 3 | 4 | %% @spec get_and_clear(SessionID) -> [Message] 5 | %% @doc Retrieve the current flash messages for `SessionID' and flush the message stack. 6 | get_and_clear(SessionID) -> 7 | case boss_session:get_session_data(SessionID, boss_flash) of 8 | undefined -> []; 9 | BossFlash -> 10 | boss_session:remove_session_data(SessionID, boss_flash), 11 | [{boss_flash, lists:reverse(BossFlash)}] 12 | end. 13 | 14 | %% @spec add(SessionID, Type, Title) -> ok | {error, Reason} 15 | %% @doc Add a message to the flash message stack for `SessionID'. 16 | add(SessionID, Type, Title) -> 17 | add(SessionID, Type, Title, undefined, undefined). 18 | 19 | %% @spec add(SessionID, Type, Title, Message) -> ok | {error, Reason} 20 | %% @doc Add a message to the flash message stack for `SessionID'. 21 | add(SessionID, Type, Title, Message) -> 22 | add(SessionID, Type, Title, Message, undefined). 23 | 24 | %% @spec add(SessionID, Type, Title, Message, Data) -> ok | {error, Reason} 25 | %% @doc Add a message to the flash message stack for `SessionID'. 26 | add(SessionID, Type, Title, Message, Data) -> 27 | Msg = [{method, atom_to_list(Type)}, {title, Title}, {message, Message}, {data, Data}], 28 | Flash = case boss_session:get_session_data(SessionID, boss_flash) of 29 | undefined -> 30 | [Msg]; 31 | ExistingFlash -> 32 | [Msg|ExistingFlash] 33 | end, 34 | boss_session:set_session_data(SessionID, boss_flash, Flash). 35 | -------------------------------------------------------------------------------- /src/boss/boss_web.erl: -------------------------------------------------------------------------------- 1 | -module(boss_web). 2 | 3 | -export([reload_routes/0, 4 | reload_translation/1, 5 | reload_all_translations/0, 6 | reload_init_scripts/0, 7 | get_all_routes/0, 8 | get_all_models/0, 9 | get_all_applications/0, 10 | base_url/1, 11 | domains/1, 12 | static_prefix/1, 13 | translator_pid/1, 14 | router_pid/1, 15 | application_info/1]). 16 | 17 | reload_routes() -> 18 | gen_server:call(boss_web, reload_routes). 19 | 20 | reload_translation(Locale) -> 21 | gen_server:call(boss_web, {reload_translation, Locale}). 22 | 23 | reload_all_translations() -> 24 | gen_server:call(boss_web, reload_all_translations). 25 | 26 | reload_init_scripts() -> 27 | gen_server:call(boss_web, reload_init_scripts). 28 | 29 | get_all_routes() -> 30 | gen_server:call(boss_web, get_all_routes). 31 | 32 | get_all_models() -> 33 | gen_server:call(boss_web, get_all_models). 34 | 35 | get_all_applications() -> 36 | gen_server:call(boss_web, get_all_applications). 37 | 38 | base_url(App) -> 39 | gen_server:call(boss_web, {base_url, App}). 40 | 41 | domains(App) -> 42 | gen_server:call(boss_web, {domains, App}). 43 | 44 | static_prefix(App) -> 45 | gen_server:call(boss_web, {static_prefix, App}). 46 | 47 | translator_pid(AppName) -> 48 | gen_server:call(boss_web, {translator_pid, AppName}). 49 | 50 | router_pid(AppName) -> 51 | gen_server:call(boss_web, {router_pid, AppName}). 52 | 53 | application_info(App) -> 54 | gen_server:call(boss_web, {application_info, App}). 55 | -------------------------------------------------------------------------------- /src/boss/boss_html_doc_template.dtl: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | {{ _doc }} ({{ application }}) 5 | 6 | 7 | 8 |
9 |

Documentation

10 |

This is EDoc autogenerated documentation for your models and web controllers. 11 | If you are looking for ChicagoBoss API, it could be found here. 12 | Wiki is here. 13 |

14 |
This section available only in development mode(i.e. then started with init-dev.sh) and does not available in production
15 | 16 |

Models

17 |
    18 | {% for model in models %} 19 |
  • {{ model }}
  • 20 | {% endfor %} 21 |
22 |
23 |
24 |

Web controllers

25 |
    26 | {% for controller in controllers %} 27 |
  • {{ controller }}
  • 28 | {% endfor %} 29 |
30 |

Templates

31 |
    32 | {{unknown_filter}} 33 |
34 |
35 |
36 |

{% now "D, j F Y g:i a" %}

37 |
38 | 39 | 40 | -------------------------------------------------------------------------------- /src/boss/boss_mail_driver_smtp.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mail_driver_smtp). 2 | -export([start/0, stop/1, deliver/5]). 3 | 4 | start() -> 5 | OptionsBase = [{ssl, false}, {hostname, smtp_util:guess_FQDN()}, {retries, 1}], 6 | {ok, OptionsBase ++ get_tls() ++ get_host() ++ get_port() ++ get_credentials()}. 7 | 8 | get_tls() -> 9 | case application:get_env(mail_relay_use_tls) of 10 | {ok, Setting} -> 11 | [{tls, Setting}]; 12 | undefined -> 13 | [{tls, if_available}] 14 | end. 15 | 16 | get_host() -> 17 | case application:get_env(mail_relay_host) of 18 | {ok, Relay} -> 19 | [{relay, Relay}]; 20 | undefined -> 21 | [] 22 | end. 23 | 24 | get_port() -> 25 | case application:get_env(mail_relay_port) of 26 | {ok, Port} -> 27 | [{port, Port}]; 28 | undefined -> 29 | [] 30 | end. 31 | 32 | get_credentials() -> 33 | case application:get_env(mail_relay_username) of 34 | {ok, UserName} -> 35 | [{auth, always}, {username, UserName}, {password, boss_env:get_env(mail_relay_password, "")}]; 36 | _ -> 37 | [{auth, never}] 38 | end. 39 | 40 | stop(_) -> 41 | ok. 42 | 43 | deliver(Options, FromAddress, ToAddress, BodyFun, ResultFun) -> 44 | MailOptions = case proplists:lookup(relay, Options) of 45 | none -> 46 | [_User, Host] = string:tokens(ToAddress, "@"), 47 | [{relay, Host} | Options]; 48 | _ -> 49 | Options 50 | end, 51 | Email = {FromAddress, [ToAddress], BodyFun}, 52 | gen_smtp_client:send(Email, MailOptions, ResultFun). 53 | -------------------------------------------------------------------------------- /src/boss/boss_elixir_compiler.erl: -------------------------------------------------------------------------------- 1 | -module(boss_elixir_compiler). 2 | -export([compile/2]). 3 | 4 | -type compile_return() :: {ok, module()}|{error, [any()]}. 5 | 6 | -spec load_code(module(),binary()) -> 'ok' | {'error',string()}. 7 | -spec compile(string(),[any()]) -> 8 | compile_return(). 9 | -spec compile_to_outdir(module(), 10 | binary(), 11 | undefined|string()) -> 12 | compile_return(). 13 | -spec handle_write_result(module(),binary() | string(), 'ok' | {'error',atom()}) -> 14 | compile_return(). 15 | 16 | load_code(Module, Bin) -> 17 | code:purge(Module), 18 | case code:load_binary(Module, atom_to_list(Module) ++ ".ex", Bin) of 19 | {module, _} -> ok; 20 | _ -> {error, lists:concat(["code reload failed: ", Module])} 21 | end. 22 | 23 | compile(FilePath, Options) -> 24 | [{Module, Binary}] = elixir_compiler:file(list_to_binary(FilePath)), 25 | load_code(Module, Binary), 26 | OutDir = proplists:get_value(out_dir, Options), 27 | compile_to_outdir(Module, Binary, OutDir). 28 | 29 | 30 | compile_to_outdir(Module, _Binary, undefined) -> 31 | {ok, Module}; 32 | compile_to_outdir(Module, Binary, OutDir) -> 33 | BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]), 34 | WriteResult = file:write_file(BeamFile, Binary), 35 | handle_write_result(Module, BeamFile, WriteResult). 36 | 37 | 38 | handle_write_result(Module, _BeamFile, ok) -> 39 | {ok, Module}; 40 | handle_write_result(_Module, BeamFile, {error, Reason}) -> 41 | {error, lists:flatten( 42 | io_lib:format("Beam generation of '~s' failed: ~p", 43 | [BeamFile, file:format_error(Reason)]))}. 44 | -------------------------------------------------------------------------------- /src/boss/boss_env.erl: -------------------------------------------------------------------------------- 1 | -module(boss_env). 2 | 3 | -export([boss_env/0, setup_boss_env/0, get_env/2, get_env/3]). 4 | -export([master_node/0, is_master_node/0, is_developing_app/1]). 5 | 6 | 7 | -spec boss_env() -> any(). 8 | -spec get_env(atom(),_) -> any(). 9 | -spec get_env(atom(),atom(),_) -> any(). 10 | -spec is_developing_app(types:application()) -> boolean(). 11 | -spec is_master_node() -> boolean(). 12 | -spec master_node() -> any(). 13 | -spec setup_boss_env() -> types:execution_mode(). 14 | 15 | boss_env() -> 16 | case get(boss_environment) of 17 | undefined -> setup_boss_env(); 18 | Val -> Val 19 | end. 20 | 21 | setup_boss_env() -> 22 | case boss_load:module_is_loaded(reloader) of 23 | true -> put(boss_environment, development), development; 24 | false -> put(boss_environment, production), production 25 | end. 26 | 27 | get_env(App, Key, Default) when is_atom(App), is_atom(Key) -> 28 | case application:get_env(App, Key) of 29 | {ok, Val} -> Val; 30 | _ -> Default 31 | end. 32 | 33 | get_env(Key, Default) when is_atom(Key) -> 34 | get_env(boss, Key, Default). 35 | 36 | master_node() -> 37 | case boss_env:get_env(master_node, erlang:node()) of 38 | MasterNode when is_list(MasterNode) -> 39 | list_to_atom(MasterNode); 40 | MasterNode -> MasterNode 41 | end. 42 | 43 | is_master_node() -> 44 | master_node() =:= erlang:node(). 45 | 46 | is_developing_app(AppName) -> 47 | BossEnv = boss_env:boss_env(), 48 | DevelopingApp = boss_env:get_env(developing_app, undefined), 49 | AppName =:= DevelopingApp andalso BossEnv =:= development. 50 | -------------------------------------------------------------------------------- /src/boss/session_adapters/boss_session_adapter_mock.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_adapter_mock). 2 | -behaviour(boss_session_adapter). 3 | -export([start/0, start/1, stop/1, init/1]). 4 | -export([session_exists/2, create_session/3, lookup_session/2]). 5 | -export([lookup_session_value/3, set_session_value/4, delete_session/2, delete_session_value/3]). 6 | 7 | start() -> 8 | start([]). 9 | 10 | start(_Options) -> 11 | {ok, undefined}. 12 | 13 | stop(undefined) -> 14 | ok; 15 | stop(MockSup) -> 16 | exit(MockSup), 17 | ok. 18 | 19 | init(Options) -> 20 | case boss_env:is_master_node() of 21 | true -> 22 | boss_session_mock_sup:start_link(Options); 23 | false -> 24 | ok 25 | end. 26 | 27 | session_exists(_, undefined) -> 28 | false; 29 | session_exists(_, SessionID) -> 30 | gen_server:call({global, boss_session_mock}, {session_exists, SessionID}). 31 | 32 | create_session(_, SessionID, Data) -> 33 | gen_server:call({global, boss_session_mock}, {create_session, SessionID, Data}). 34 | 35 | lookup_session(_, SessionID) -> 36 | gen_server:call({global, boss_session_mock}, {lookup_session, SessionID}). 37 | 38 | lookup_session_value(_, SessionID, Key) -> 39 | gen_server:call({global, boss_session_mock}, {lookup_session_value, SessionID, Key}). 40 | 41 | set_session_value(_, SessionID, Key, Value) -> 42 | gen_server:call({global, boss_session_mock}, {set_session_value, SessionID, Key, Value}). 43 | 44 | delete_session(_, SessionID) -> 45 | gen_server:call({global, boss_session_mock}, {delete_session, SessionID}). 46 | 47 | delete_session_value(_, SessionID, Key) -> 48 | gen_server:call({global, boss_session_mock}, {delete_session_value, SessionID, Key}). 49 | -------------------------------------------------------------------------------- /bin/boss: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Chicago Boss Executable 4 | # 5 | # @author: Pius Uzamere 6 | # 7 | # Allows the user to easily create a new Chicago Boss application from anywhere, 8 | # as long as the Chicago Boss directory is in the user's path. 9 | # 10 | # Useful when installing Chicago Boss via Homebrew or similar methods. 11 | 12 | 13 | working_directory="$PWD" 14 | init_script="$working_directory/init.sh" 15 | 16 | # Find the actual directory of Chicago Boss, even if the binary is symlinked. 17 | 18 | pushd . > /dev/null 19 | bossdir="${BASH_SOURCE[0]}"; 20 | if ([ -h "${bossdir}" ]) then 21 | while([ -h "${bossdir}" ]) do cd `dirname "$bossdir"`; bossdir=`readlink "${bossdir}"`; done 22 | fi 23 | cd `dirname ${bossdir}` > /dev/null 24 | bossdir=`pwd`; 25 | popd > /dev/null 26 | cd "$bossdir/.." 27 | bossdir=$PWD 28 | 29 | case "${1:-''}" in 30 | 'new') 31 | if [ $# -eq 2 ] 32 | then 33 | echo "Hold on tight, creating new Chicago Boss app $2 ..." 34 | make app PROJECT=$2 PREFIX="$working_directory/" -f "$bossdir/Makefile" 35 | else 36 | echo Please specify an app name to create a new Chicago Boss application. 37 | echo "Usage: boss new APP_NAME or boss start|start-dev|stop|reload|restart|attach" 38 | fi 39 | ;; 40 | 41 | 'start'|'start-dev'|'stop'|'reload'|'restart'|'attach') 42 | if [ -f $init_script ] 43 | then 44 | sh $init_script $1 45 | else 46 | echo Sorry, \"boss $1\" may only be run from the root directory of a Chicago Boss application. 47 | fi 48 | ;; 49 | 50 | *) 51 | echo "Chicago Boss." 52 | echo "Usage: boss new APP_NAME or boss start|start-dev|stop|reload|restart|attach" 53 | exit 1 54 | ;; 55 | esac -------------------------------------------------------------------------------- /src/boss/boss_mq_controller.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mq_controller). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 8 | 9 | -record(state, {adapter, connection}). 10 | 11 | start_link() -> 12 | start_link([]). 13 | 14 | start_link(Args) -> 15 | gen_server:start_link({global, boss_mq}, ?MODULE, Args, []). 16 | 17 | init(Options) -> 18 | Adapter = proplists:get_value(adapter, Options, boss_mq_adapter_tinymq), 19 | {ok, Conn} = Adapter:start(Options), 20 | {ok, #state{adapter = Adapter, connection = Conn}}. 21 | 22 | handle_call({pull, Channel, Timestamp, Subscriber}, _From, State) -> 23 | Adapter = State#state.adapter, 24 | Conn = State#state.connection, 25 | {reply, Adapter:pull(Conn, Channel, Timestamp, Subscriber), State}; 26 | 27 | handle_call({poll, Channel, Timestamp}, _From, State) -> 28 | Adapter = State#state.adapter, 29 | Conn = State#state.connection, 30 | {reply, Adapter:poll(Conn, Channel, Timestamp), State}; 31 | 32 | handle_call({push, Channel, Message}, _From, State) -> 33 | Adapter = State#state.adapter, 34 | Conn = State#state.connection, 35 | {reply, Adapter:push(Conn, Channel, Message), State}; 36 | 37 | handle_call({now, Channel}, _From, State) -> 38 | Adapter = State#state.adapter, 39 | Conn = State#state.connection, 40 | {reply, Adapter:now(Conn, Channel), State}. 41 | 42 | 43 | handle_cast(_Request, State) -> 44 | {noreply, State}. 45 | 46 | terminate(_Reason, State) -> 47 | Adapter = State#state.adapter, 48 | Conn = State#state.connection, 49 | Adapter:stop(Conn). 50 | 51 | code_change(_OldVsn, State, _Extra) -> 52 | {ok, State}. 53 | 54 | handle_info(_Info, State) -> 55 | {noreply, State}. 56 | -------------------------------------------------------------------------------- /src/boss/boss_sup.erl: -------------------------------------------------------------------------------- 1 | %% @author Evan Miller 2 | %% @copyright YYYY author. 3 | 4 | %% @doc Supervisor for the boss application. 5 | 6 | -module(boss_sup). 7 | -author('Evan Miller '). 8 | 9 | -behaviour(supervisor). 10 | 11 | %% External exports 12 | -export([start_link/0, upgrade/0]). 13 | 14 | %% supervisor callbacks 15 | -export([init/1]). 16 | 17 | %% @spec start_link() -> ServerRet 18 | %% @doc API for starting the supervisor. 19 | start_link() -> 20 | supervisor:start_link({local, ?MODULE}, ?MODULE, []). 21 | 22 | %% @spec upgrade() -> ok 23 | %% @doc Add processes if necessary. 24 | upgrade() -> 25 | {ok, {_, Specs}} = init([]), 26 | 27 | Old = sets:from_list( 28 | [Name || {Name, _, _, _} <- supervisor:which_children(?MODULE)]), 29 | New = sets:from_list([Name || {Name, _, _, _, _, _} <- Specs]), 30 | Kill = sets:subtract(Old, New), 31 | 32 | sets:fold(fun (Id, ok) -> 33 | supervisor:terminate_child(?MODULE, Id), 34 | supervisor:delete_child(?MODULE, Id), 35 | ok 36 | end, ok, Kill), 37 | 38 | [supervisor:start_child(?MODULE, Spec) || Spec <- Specs], 39 | ok. 40 | 41 | %% @spec init([]) -> SupervisorTree 42 | %% @doc supervisor callback. 43 | init([]) -> 44 | Ip = case os:getenv("MOCHIWEB_IP") of false -> "0.0.0.0"; Any -> Any end, 45 | Port = case application:get_env(port) of 46 | {ok, {env, PortVar}} -> 47 | case os:getenv(PortVar) of 48 | false -> 8001; 49 | PortStr -> list_to_integer(PortStr) 50 | end; 51 | {ok, P} when is_integer(P) -> P; 52 | undefined -> 8001 53 | end, 54 | WebConfig = [ {ip, Ip}, {port, Port} ], 55 | Web = {boss_web_controller, 56 | {boss_web_controller, start_link, [WebConfig]}, 57 | permanent, 5000, worker, dynamic}, 58 | 59 | Processes = [Web], 60 | {ok, {{one_for_one, 10, 10}, Processes}}. 61 | -------------------------------------------------------------------------------- /src/boss/boss_controller_lib.erl: -------------------------------------------------------------------------------- 1 | -module(boss_controller_lib). 2 | -export([convert_params_to_tokens/3]). 3 | 4 | convert_params_to_tokens(Variables, ControllerModule, Action) -> 5 | DummyController = apply(ControllerModule, new, lists:seq(1, proplists:get_value(new, ControllerModule:module_info(exports)))), 6 | Routes = case lists:member({'_routes', 1}, ControllerModule:module_info(exports)) of 7 | true -> DummyController:'_routes'(); 8 | false -> [] 9 | end, 10 | lists:foldr(fun 11 | ({RouteName, RouteTokens}, {Acc, Vars}) when RouteName =:= Action -> 12 | Result = lists:foldr(fun 13 | (_, false) -> 14 | false; 15 | (Token, {Acc1, Vars1}) when is_atom(Token) -> 16 | CamelCase = atom_to_list(Token), 17 | Underscore = list_to_atom(string:to_lower(inflector:underscore(CamelCase))), 18 | case proplists:get_value(Underscore, Vars1) of 19 | undefined -> 20 | false; 21 | Value -> 22 | {[Value|Acc1], proplists:delete(Underscore, Vars1)} 23 | end; 24 | (Token, {Acc1, Vars1}) -> 25 | {[Token|Acc1], Vars1} 26 | end, {[], Variables}, RouteTokens), 27 | case Result of 28 | false -> 29 | {Acc, Vars}; 30 | {Acc1, Vars1} -> 31 | case length(Vars1) =< length(Vars) of 32 | true -> 33 | {Acc1, Vars1}; 34 | false -> 35 | {Acc, Vars} 36 | end 37 | end; 38 | (_, {Acc, Vars}) -> 39 | {Acc, Vars} 40 | end, {[], Variables}, Routes). 41 | -------------------------------------------------------------------------------- /src/boss/session_adapters/boss_session_adapter_cache.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_adapter_cache). 2 | -behaviour(boss_session_adapter). 3 | -export([start/0, start/1, stop/1, init/1]). 4 | 5 | -export([session_exists/2, create_session/3, lookup_session/2]). 6 | -export([lookup_session_value/3, set_session_value/4, delete_session/2, delete_session_value/3]). 7 | 8 | -record(conn, { 9 | prefix, 10 | exp_time 11 | }). 12 | 13 | start() -> 14 | start([]). 15 | 16 | start(_Options) -> 17 | {ok, #conn{ prefix = sess }}. 18 | 19 | stop(_Conn) -> 20 | ok. 21 | 22 | init(_) -> 23 | ok. 24 | 25 | session_exists(_, undefined) -> 26 | false; 27 | session_exists(#conn{ prefix = Prefix }, SessionID) -> 28 | boss_cache:get(Prefix, SessionID) =/= undefined. 29 | 30 | create_session(#conn{ prefix = Prefix }, SessionID, InitialData) -> 31 | boss_cache:set(Prefix, SessionID, InitialData, 0). 32 | 33 | lookup_session(#conn{ prefix = Prefix}, SessionID) -> 34 | case boss_cache:get(Prefix, SessionID) of 35 | undefined -> []; 36 | Val -> Val 37 | end. 38 | 39 | lookup_session_value(#conn{ prefix = Prefix }, SessionID, Key) -> 40 | case boss_cache:get(Prefix, SessionID) of 41 | undefined -> undefined; 42 | PropList -> proplists:get_value(Key, PropList) 43 | end. 44 | 45 | set_session_value(#conn{ prefix = Prefix }, SessionID, Key, Value) -> 46 | case boss_cache:get(Prefix, SessionID) of 47 | undefined -> 48 | create_session(#conn{ prefix = Prefix }, SessionID, [{Key, Value}]); 49 | PropList -> 50 | boss_cache:set(Prefix, SessionID, [{Key, Value}|proplists:delete(Key, PropList)], 0) 51 | end. 52 | 53 | delete_session(#conn{ prefix = Prefix }, SessionID) -> 54 | boss_cache:delete(Prefix, SessionID). 55 | 56 | delete_session_value(#conn{ prefix = Prefix }, SessionID, Key) -> 57 | case boss_cache:get(Prefix, SessionID) of 58 | undefined -> ok; 59 | PropList -> 60 | boss_cache:set(Prefix, SessionID, proplists:delete(Key, PropList), 0) 61 | end. 62 | -------------------------------------------------------------------------------- /src/boss/boss_web_controller_cowboy.erl: -------------------------------------------------------------------------------- 1 | -module(boss_web_controller_cowboy). 2 | 3 | -export([dispatch_cowboy/1]). 4 | 5 | %% cowboy dispatch rule for static content 6 | dispatch_cowboy(Applications) -> 7 | AppStaticDispatches = create_cowboy_dispatches(Applications), 8 | 9 | BossDispatch = [{'_', boss_mochicow_handler, [{loop, {boss_mochicow_handler, loop}}]}], 10 | % [{"/", boss_mochicow_handler, []}], 11 | %Dispatch = [{'_', 12 | 13 | Dispatch = [{'_', AppStaticDispatches ++ BossDispatch}], 14 | SSLEnabled = boss_env:get_env(ssl_enable, false), 15 | CowboyListener = get_listener(SSLEnabled), 16 | cowboy:set_env(CowboyListener, dispatch, cowboy_router:compile(Dispatch)). 17 | 18 | -spec(get_listener(boolean()) -> boss_https_listener|boss_http_listener). 19 | get_listener(true) -> boss_https_listener; 20 | get_listener(false) -> boss_http_listener. 21 | 22 | create_cowboy_dispatches(Applications) -> 23 | lists:map(fun create_dispatch/1, Applications). 24 | 25 | -spec(create_dispatch(atom()) -> {[any(),...], cowboy_static, {'priv_dir',atom(),[97 | 99 | 105 | 115 | 116,...],[{_,_,_},...]}}). 26 | create_dispatch(AppName) -> 27 | BaseURL = boss_env:get_env(AppName, base_url, "/"), 28 | StaticPrefix = boss_env:get_env(AppName, static_prefix, "/static"), 29 | Path = case BaseURL of 30 | "/" -> StaticPrefix; 31 | _ -> BaseURL ++ StaticPrefix 32 | end, 33 | Handler = cowboy_static, 34 | Etag = [], %%[{etag, false}], %% [{etag, EtagModule, EtagFunction}] 35 | MimeTypes = [{mimetypes, cow_mimetypes, all}], %% [{mimetypes, mimetypes, path_to_mimes}] 36 | Extra = Etag ++ MimeTypes, 37 | Opts = {priv_dir, 38 | AppName, 39 | "static", 40 | Extra 41 | }, 42 | {Path ++ "/[...]", Handler, Opts}. 43 | 44 | 45 | -------------------------------------------------------------------------------- /test/boss_web_controller_handle_request_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_web_controller_handle_request_test). 2 | 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | 6 | 7 | perms([]) -> [[]]; 8 | perms(L) -> [[H|T] || H <- L, T <- perms(L--[H])]. 9 | 10 | f1(_) -> 11 | {ok, test}. 12 | f2(_) -> 13 | {ok, test2}. 14 | f3(_) -> 15 | {error, test3}. 16 | 17 | fold_operations_test() -> 18 | ErrorList = [fun f1/1, fun f2/1, fun f3/1], 19 | OKList = [fun f1/1, fun f2/1], 20 | ?assertEqual({error, test3}, boss_web_controller_handle_request:fold_operations(app,ErrorList)), 21 | ?assertEqual({ok, test2}, boss_web_controller_handle_request:fold_operations(app,OKList)), 22 | ErrLists = perms(ErrorList), 23 | lists:foreach(fun(List) -> 24 | ?assertEqual({error, test3}, 25 | boss_web_controller_handle_request:fold_operations(app,List)) 26 | end, ErrLists), 27 | lists:foreach(fun(List) -> 28 | ?assertMatch({ok, _}, 29 | boss_web_controller_handle_request:fold_operations(app,List)) 30 | end,perms(OKList)). 31 | 32 | make_controlle_names_test() -> 33 | ?assertEqual(["foo","bar","baz"], 34 | boss_web_controller_handle_request:make_controller_names([foo,bar,baz])). 35 | 36 | 37 | make_etag_test() -> 38 | Sha1 = boss_web_controller_handle_request:make_etag(boss, "", "empty"), 39 | ?assertEqual("2jmj7l5rSw0yVb/vlWAYkK/YBwk=", Sha1). 40 | 41 | dev_headers_test() -> 42 | Module = {simple_bridge_response_wrapper,mochiweb_response_bridge, 43 | {{mochicow_request,port,'GET',"/static/test.txt", 44 | {1,1}, 45 | {3, 46 | {"user-agent", 47 | {"user-agent","curl/7.32.0"}, 48 | {"host", 49 | {"host","erlang-ci.com:8001"}, 50 | {"accept",{"accept","*/*"},nil,nil}, 51 | nil}, 52 | nil}}, 53 | <<>>}, 54 | "/home/zkessin/Documents/erlang_ci/priv/static"}, 55 | {response,200,[{header,"Cache-Control","no-cache"}],[],[]}}, 56 | ?assertEqual(Module, boss_web_controller_handle_request:dev_headers(Module, production)), 57 | {_,_,_,Response} = boss_web_controller_handle_request:dev_headers(Module, development), 58 | ?assertMatch({response, 200, [{header, "Cache-Control","no-cache"}],_,_}, Response). 59 | 60 | 61 | -------------------------------------------------------------------------------- /skel/init.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | # 3 | # Chicago Boss Init System 4 | # 5 | # @author: Jose Luis Gordo Romero 6 | # @author: Rune Juhl Jacobsen 7 | # 8 | # ------------------------------------------------------------------- 9 | # The shell commands are automatically generated by the boss rebar 10 | # plugin/driver, all configuration params and paths are in boss.config 11 | # ------------------------------------------------------------------- 12 | 13 | do_start () { 14 | if test $(echo "$1"|grep "^exec erl"> /dev/null;echo $?) -eq 0 15 | then 16 | eval "$1" 17 | else 18 | echo "$1" 19 | fi 20 | } 21 | 22 | 23 | cd `dirname $0` 24 | 25 | case "${1:-''}" in 26 | 'start') 27 | # Start Boss in production mode 28 | echo "starting boss in production mode..." 29 | START=$(./rebar boss c=start_cmd|grep -v "==>") 30 | do_start "$START" 31 | ;; 32 | 33 | 'start-dev') 34 | # Start Boss in development mode 35 | START_DEV=$(./rebar boss c=start_dev_cmd|grep -v "==>") 36 | do_start "$START_DEV" 37 | ;; 38 | 39 | 'stop') 40 | # Stop Boss daemon 41 | echo "stopping boss..." 42 | STOP=$(./rebar boss c=stop_cmd|grep -v "==>") 43 | # After hours of shell quoting problems with the erl command, 44 | # eval with the command quoted works!!! 45 | do_start "$STOP" 46 | ;; 47 | 48 | 'reload') 49 | # Boss hot code reload <-- only the actual node, not the entire cluster 50 | echo "Hot code reload, (WARN: Only this node)" 51 | RELOAD=$(./rebar boss c=reload_cmd|grep -v "==>") 52 | do_start "$RELOAD" 53 | ;; 54 | 55 | 'restart') 56 | # Boss complete restart 57 | echo "Restarting (stop-start) boss..." 58 | $0 stop 59 | $0 start 60 | ;; 61 | 62 | 'attach') 63 | # Boss attach running system's console 64 | echo "Connecting production system..." 65 | ATTACH=$(./rebar boss c=attach_cmd|grep -v "==>") 66 | do_start "$ATTACH" 67 | ;; 68 | 69 | *) 70 | echo "Chicago Boss Boot System" 71 | echo "Usage: $SELF start|start-dev|stop|reload|restart|attach" 72 | exit 1 73 | ;; 74 | esac 75 | -------------------------------------------------------------------------------- /test/boss_controller_compiler_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_controller_compiler_test). 2 | -include_lib("proper/include/proper.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("../src/boss/boss_web.hrl"). 5 | -define(TMODULE, boss_controller_compiler). 6 | -compile(export_all). 7 | 8 | spec_test_() -> 9 | begin 10 | Tests = [ 11 | fun prop_add_routes_to_forms/0, 12 | 13 | {add_export_to_forms, 1}, 14 | {extract_routes_from_clauses, 2}, 15 | {function_for_routes, 1}, 16 | {route_from_token_ast, 1}, 17 | {map_syntax_tuples, 1}, 18 | {map_tokens, 1} 19 | ], 20 | [case Test of 21 | {Funct, Arity} -> 22 | 23 | ?_assert(proper:check_spec({?TMODULE, Funct, Arity}, 24 | [{to_file, user}, 25])); 25 | 26 | F when is_function(F,0) -> 27 | ?_assert(proper:quickcheck(F(), 28 | [{to_file, user}])) 29 | end||Test <-Tests] 30 | end. 31 | 32 | 33 | %-type route_form() :: boss_controller_compiler:route_form(). 34 | prop_add_routes_to_forms() -> 35 | ?FORALL(ExportAttrs , 36 | list(boss_controller_compiler:export_attr1()), 37 | ?IMPLIES(len_in_range(ExportAttrs,1,20), 38 | begin 39 | Result = boss_controller_compiler:add_routes_to_forms(ExportAttrs 40 | ++ [{eof, 1}]), 41 | is_list(Result) 42 | end)). 43 | 44 | 45 | prop_add_routes_to_forms_function() -> 46 | ?FORALL(FctTups, 47 | list(route_form()), 48 | ?IMPLIES(len_in_range(FctTups, 1,5), 49 | ?TIMEOUT(300, 50 | begin 51 | Result = boss_controller_compiler:add_routes_to_forms(FctTups,[],[]), 52 | is_list(Result) 53 | end))). 54 | 55 | 56 | len_in_range(List, Min, Max) -> 57 | Len = length(List), 58 | Len =< Max andalso Len >= Min. 59 | -------------------------------------------------------------------------------- /skel.template: -------------------------------------------------------------------------------- 1 | %-*-Erlang-*- 2 | {variables, [{appid, "bossapp"}, 3 | {port, 8001}, 4 | {src, "../ChicagoBoss"}, 5 | {dest, "{{appid}}"}]}. 6 | {dir, "{{dest}}"}. 7 | {dir, "{{dest}}/log"}. 8 | {dir, "{{dest}}/priv/lang"}. 9 | {dir, "{{dest}}/priv/migrations"}. 10 | {dir, "{{dest}}/ebin"}. 11 | {dir, "{{dest}}/include"}. 12 | {dir, "{{dest}}/src/lib"}. 13 | {dir, "{{dest}}/src/view"}. 14 | {dir, "{{dest}}/src/view/lib"}. 15 | {dir, "{{dest}}/src/view/lib/tag_html"}. 16 | {dir, "{{dest}}/src/view/lib/tag_modules"}. 17 | {dir, "{{dest}}/src/view/lib/filter_modules"}. 18 | {dir, "{{dest}}/src/model"}. 19 | {dir, "{{dest}}/src/controller"}. 20 | {dir, "{{dest}}/src/websocket"}. 21 | {dir, "{{dest}}/src/mail/view"}. 22 | {dir, "{{dest}}/src/test/eunit"}. 23 | {dir, "{{dest}}/src/test/functional"}. 24 | {template, "skel/rebar.config", "{{dest}}/rebar.config"}. 25 | {template, "skel/start-server.bat", "{{dest}}/start-server.bat"}. 26 | {template, "skel/src/project.app.src", "{{dest}}/src/{{appid}}.app.src"}. 27 | {template, "skel/src/view/lib/tag_modules/custom_tags.erl", "{{dest}}/src/view/lib/tag_modules/{{appid}}_custom_tags.erl"}. 28 | {template, "skel/src/view/lib/filter_modules/custom_filters.erl", "{{dest}}/src/view/lib/filter_modules/{{appid}}_custom_filters.erl"}. 29 | {template, "skel/boss.config", "{{dest}}/boss.config"}. 30 | {template, "skel/src/mail/outgoing_mail_controller.erl", "{{dest}}/src/mail/{{appid}}_outgoing_mail_controller.erl"}. 31 | {template, "skel/src/mail/incoming_mail_controller.erl", "{{dest}}/src/mail/{{appid}}_incoming_mail_controller.erl"}. 32 | {template, "skel/priv/init/news.erl", "{{dest}}/priv/init/{{appid}}_01_news.erl"}. 33 | {file, "skel/priv/static/chicago-boss.png", "{{dest}}/priv/static/chicago-boss.png"}. 34 | {file, "skel/priv/static/favicon.ico", "{{dest}}/priv/static/favicon.ico"}. 35 | {file, "skel/priv/boss.routes", "{{dest}}/priv/{{appid}}.routes"}. 36 | {file, "skel/priv/rebar/boss_plugin.erl", "{{dest}}/priv/rebar/boss_plugin.erl"}. 37 | {file, "skel/src/view/lib/README", "{{dest}}/src/view/lib/README"}. 38 | {file, "skel/init.sh", "{{dest}}/init.sh"}. 39 | {file, "skel/init-dev.sh", "{{dest}}/init-dev.sh"}. 40 | {file, "skel/rebar", "{{dest}}/rebar"}. 41 | {file, "skel/rebar.cmd", "{{dest}}/rebar.cmd"}. 42 | {chmod, 8#755, "{{dest}}/init.sh"}. 43 | {chmod, 8#755, "{{dest}}/init-dev.sh"}. 44 | {chmod, 8#755, "{{dest}}/rebar"}. 45 | -------------------------------------------------------------------------------- /test/boss_web_controller_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_web_controller_test). 2 | -include_lib("proper/include/proper.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | -include("../src/boss/boss_web.hrl"). 5 | -export([action/2, wants_session/3]). 6 | 7 | action(exit,_) -> 8 | exit(test); 9 | action(return, _) -> 10 | ok. 11 | 12 | wants_session(_Application, R, _Modules) -> 13 | R. 14 | 15 | 16 | call_controller_action_test() -> 17 | R = boss_web_controller:call_controller_action(?MODULE, exit,[]), 18 | ?assertEqual( {output, "Process Error see console.log for details\n"}, R), 19 | ?assertEqual( ok, boss_web_controller:call_controller_action(?MODULE, return,[])). 20 | 21 | make_action_session_id_test() -> 22 | R = make_ref(), 23 | ?assertEqual(R, boss_web_controller:make_action_session_id({},{}, {}, R, {})), 24 | ?assertEqual(undefined, boss_web_controller:make_action_session_id(false, 25 | #boss_app_info{}, 26 | {}, 27 | undefined, 28 | ?MODULE)). 29 | 30 | receive_controller_response_no_exit_test() -> 31 | Ref = make_ref(), 32 | self() ! {msg, Ref, ok}, 33 | ?assertEqual(ok, boss_web_controller:receive_controller_response(Ref)). 34 | 35 | receive_controller_response_normal_exit_test() -> 36 | Ref = make_ref(), 37 | self() ! {'EXIT', {}, normal}, 38 | self() ! {msg, Ref, ok}, 39 | ?assertEqual(ok, boss_web_controller:receive_controller_response(Ref)). 40 | 41 | receive_controller_response_crash_exit_test() -> 42 | Ref = make_ref(), 43 | self() ! {'EXIT', Ref, ok}, 44 | ?assertMatch({output,_}, boss_web_controller:receive_controller_response(Ref)). 45 | 46 | handle_call_application_info_test() -> 47 | ?assert(proper:quickcheck(prop_handle_call_application_info(), 48 | [{to_file,user}])), 49 | ok. 50 | 51 | 52 | prop_handle_call_application_info() -> 53 | ?FORALL({State0, Application, AppInfo}, 54 | {#state{}, atom(), #boss_app_info{}}, 55 | begin 56 | State = State0#state{applications = [AppInfo#boss_app_info{application = Application}]}, 57 | 58 | {reply, AppInfo1, State} = 59 | boss_web_controller:handle_call({application_info, Application}, self(), State), 60 | is_record(AppInfo1, boss_app_info) 61 | end). 62 | -------------------------------------------------------------------------------- /doc-src/api-session.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

BossSession is a multiadapter session management. The following adapters are supported (specified in boss.config):

4 |
    5 |
  • cache - Store sessions in whatever cache system is configured. Requires cache_enable to be set to true 6 |
  • mnesia - Mnesia. Requires the node to have a name.
  • 7 |
  • mock - In-memory sessions which uses ETS.
  • 8 |
9 | 10 |

BossSession is automatically started, so, by default an ETS based session support is provided.

11 | 12 |

The current session ID is available to controllers as the second module parameter. To disable sessions, set "session_enable" to "false" in your boss.config file.

13 | 14 |

BossFlash is a utility on top of boss_session, you can store messages between requests, automatically populated to the view as {{ "{{ boss_flash }}" }} and deleted from session after used.

15 | 16 |

boss_session

17 | 18 | {% for function in session_functions %} 19 | {% if function.description_long %} 20 |
21 |
22 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 23 |
24 |

{{ function.description_long }}

25 |
26 | {% endif %} 27 | {% endfor %} 28 | 29 | 30 |

boss_flash

31 | 32 |

Add flash messages in the controller like:

33 | 34 |
35 | boss_flash:add(SessionID, notice, "Flash Title", "Flash Message") 36 |
37 | 38 |

Use the boss_flash var in the view, should render "notice - Flash Title - Flash Message":

39 |
40 | {{ "{% for flash in boss_flash %}" }} 41 | {{ "{{ flash.method }} - {{ flash.title }} - {{ flash.message }}" }} 42 | {{ "{% endfor %}" }} 43 |
44 | 45 |

Functions

46 | 47 | {% for function in flash_functions %} 48 | {% if function.description_long %} 49 |
50 |
51 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 52 |
53 |

{{ function.description_long }}

54 |
55 | {% endif %} 56 | {% endfor %} 57 | 58 | {% endblock %} 59 | -------------------------------------------------------------------------------- /src/boss/boss_translator_controller.erl: -------------------------------------------------------------------------------- 1 | -module(boss_translator_controller). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 8 | 9 | -record(state, {strings, application}). 10 | 11 | start_link() -> 12 | start_link([]). 13 | 14 | start_link(Args) -> 15 | gen_server:start_link(?MODULE, Args, []). 16 | 17 | init(Options) -> 18 | BossApp = proplists:get_value(application, Options), 19 | StringDictionaryList = lists:map(fun(Lang) -> 20 | {Lang, dict:from_list(boss_lang:extract_po_strings(BossApp, Lang))} 21 | end, boss_files:language_list(BossApp)), 22 | {ok, #state{ 23 | strings = dict:from_list(StringDictionaryList), 24 | application = BossApp }}. 25 | 26 | handle_call({lookup, Key, Locale}, _From, State) -> 27 | Return = case dict:find(Locale, State#state.strings) of 28 | {ok, Dict} -> lookup_key(Key, Dict); 29 | _ -> undefined 30 | end, 31 | {reply, Return, State}; 32 | 33 | handle_call({is_loaded, Locale}, _From, State) -> 34 | {reply, dict:is_key(Locale, State#state.strings), State}; 35 | 36 | handle_call({reload, Locale}, _From, State) -> 37 | StringDict = dict:from_list(boss_lang:extract_po_strings(State#state.application, Locale)), 38 | NewState = State#state{ 39 | strings = dict:store(Locale, StringDict, State#state.strings) 40 | }, 41 | {reply, ok, NewState}; 42 | handle_call(reload_all, From, State) -> 43 | NewState = lists:foldr(fun(X, StateAcc) -> 44 | {reply, ok, State1} = handle_call({reload, X}, From, StateAcc), 45 | State1 46 | end, State, boss_files:language_list(State#state.application)), 47 | {reply, ok, NewState}. 48 | 49 | 50 | handle_cast(_Request, State) -> 51 | {noreply, State}. 52 | 53 | terminate(_Reason, _State) -> 54 | ok. 55 | 56 | code_change(_OldVsn, State, _Extra) -> 57 | {ok, State}. 58 | 59 | handle_info(_Info, State) -> 60 | {noreply, State}. 61 | 62 | lookup_key(Key, Dict) when is_binary(Key) -> 63 | case dict:find(binary_to_list(Key), Dict) of 64 | {ok, Trans} -> list_to_binary(Trans); 65 | error -> undefined 66 | end; 67 | lookup_key(Key, Dict) -> 68 | case dict:find(Key, Dict) of 69 | {ok, Trans} -> Trans; 70 | error -> undefined 71 | end. 72 | -------------------------------------------------------------------------------- /src/boss/boss_mochicow_handler.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mochicow_handler). 2 | -behaviour(cowboy_websocket_handler). 3 | 4 | -export([init/3, loop/1, terminate/2]). 5 | -export([websocket_init/3, websocket_handle/3, 6 | websocket_info/3, websocket_terminate/3]). 7 | 8 | init({_Any, http}, Req, _Opts) -> 9 | case cowboy_req:header(<<"upgrade">>, Req) of 10 | {undefined, _Req2} -> {upgrade, protocol, mochicow_upgrade}; 11 | {<<"websocket">>, _Req2} -> {upgrade, protocol, cowboy_websocket}; 12 | {<<"Websocket">>, _Req2} -> {upgrade, protocol, cowboy_websocket}; 13 | {<<"WebSocket">>, _Req2} -> {upgrade, protocol, cowboy_websocket} 14 | end. 15 | 16 | -record(state, {websocket_id, session_id, service_url}). 17 | 18 | loop(Req) -> 19 | boss_web_controller_handle_request:handle_request(Req, 20 | mochiweb_request_bridge, 21 | mochiweb_response_bridge). 22 | 23 | terminate(_Req, _State) -> 24 | ok. 25 | 26 | %% TODO check this function 27 | websocket_init(_Any, Req, _Opts) -> 28 | SessionKey = boss_env:get_env(session_key, "_boss_session"), 29 | {ServiceUrl, _Req1} = cowboy_req:path(Req), 30 | {SessionId, _Req2} = cowboy_req:cookie(list_to_binary(SessionKey), Req), 31 | WebsocketId = self(), 32 | State= #state{websocket_id=WebsocketId, 33 | session_id=SessionId, 34 | service_url=ServiceUrl}, 35 | boss_websocket_router:join(ServiceUrl, WebsocketId, Req, SessionId), 36 | case boss_env:get_env(websocket_timeout, undefined) of 37 | undefined -> 38 | {ok, Req, State, hibernate}; 39 | Timeout -> 40 | {ok, Req, State, Timeout, hibernate} 41 | end. 42 | 43 | websocket_handle({text, Msg}, Req, State) -> 44 | #state{websocket_id=WebsocketId, 45 | session_id=SessionId, 46 | service_url=ServiceUrl } = State, 47 | boss_websocket_router:incoming(ServiceUrl, WebsocketId, Req, SessionId, Msg), 48 | {ok, Req, State, hibernate}; 49 | 50 | websocket_handle(_Any, Req, State) -> 51 | {ok, Req, State}. 52 | 53 | websocket_info({text, Msg}, Req, State) -> 54 | {reply, {text, Msg}, Req, State}; 55 | 56 | websocket_info(_Info, Req, State) -> 57 | {ok, Req, State, hibernate}. 58 | 59 | websocket_terminate(Reason, Req, State) -> 60 | #state{websocket_id=WebsocketId, 61 | session_id=SessionId, 62 | service_url=ServiceUrl } = State, 63 | boss_websocket_router:close(Reason, ServiceUrl, WebsocketId, Req, SessionId), 64 | ok. 65 | -------------------------------------------------------------------------------- /src/boss/filters/boss_cache_vars_filter.erl: -------------------------------------------------------------------------------- 1 | -module(boss_cache_vars_filter). 2 | -export([config_key/0, config_default_value/0]). 3 | -export([before_filter/2, middle_filter/3]). 4 | 5 | -define(VARIABLES_CACHE_PREFIX, "boss_web_controller_variables"). 6 | -define(VARIABLES_CACHE_DEFAULT_TTL, 60). 7 | 8 | config_key() -> cache. 9 | config_default_value() -> none. 10 | 11 | before_filter({vars, _}, RequestContext) -> 12 | EffectiveRequestMethod = case proplists:get_value(method, RequestContext) of 13 | 'HEAD' -> 'GET'; 14 | Method -> Method 15 | end, 16 | 17 | case (boss_env:get_env(cache_enable, false) andalso 18 | EffectiveRequestMethod =:= 'GET') of 19 | true -> 20 | Language = proplists:get_value(language, RequestContext, auto), 21 | ControllerModule = proplists:get_value(controller_module, RequestContext), 22 | Action = proplists:get_value(action, RequestContext), 23 | Tokens = proplists:get_value(tokens, RequestContext, []), 24 | 25 | CacheKey = {ControllerModule, Action, Tokens, Language}, 26 | 27 | case boss_cache:get(?VARIABLES_CACHE_PREFIX, CacheKey) of 28 | undefined -> 29 | {ok, RequestContext ++ [{cache_vars, true}, {cache_key, CacheKey}]}; 30 | CachedActionResult -> 31 | CachedActionResult 32 | end; 33 | false -> 34 | {ok, RequestContext} 35 | end; 36 | before_filter(_, RequestContext) -> 37 | {ok, RequestContext}. 38 | 39 | middle_filter({render, _, _} = ActionResult, {vars, CacheOptions}, RequestContext) -> 40 | case proplists:get_value(cache_vars, RequestContext, false) of 41 | true -> 42 | CacheKey = proplists:get_value(cache_key, RequestContext), 43 | CacheTTL = proplists:get_value(seconds, CacheOptions, ?VARIABLES_CACHE_DEFAULT_TTL), 44 | case proplists:get_value(watch, CacheOptions) of 45 | undefined -> ok; 46 | CacheWatchString -> 47 | boss_news:set_watch({?VARIABLES_CACHE_PREFIX, CacheKey}, CacheWatchString, 48 | fun ?MODULE:handle_news_for_cache/3, {?VARIABLES_CACHE_PREFIX, CacheKey}, 49 | CacheTTL) 50 | end, 51 | boss_cache:set(?VARIABLES_CACHE_PREFIX, CacheKey, ActionResult, CacheTTL); 52 | false -> 53 | ok 54 | end, 55 | ActionResult; 56 | middle_filter(Other, _, _) -> 57 | Other. 58 | -------------------------------------------------------------------------------- /src/boss/boss_json.erl: -------------------------------------------------------------------------------- 1 | -module(boss_json). 2 | -export([encode/2]). 3 | 4 | -ifdef(TEST). 5 | -compile(export_all). 6 | -endif. 7 | 8 | -spec encode(_,_) -> any(). 9 | -spec json_data1([{_,_}],_,[{_,_}]) -> {'struct',[{_,_}]}. 10 | 11 | encode([First|_] = Data, ModelList) -> 12 | case boss_model_manager:is_model_instance(First, ModelList) of 13 | true -> 14 | mochijson2:encode(lists:map(fun boss_model_manager:to_json/1, Data)); 15 | false -> 16 | mochijson2:encode(json_data1(Data, ModelList, [])) 17 | end; 18 | 19 | encode([],_) -> 20 | ""; 21 | 22 | encode(Data, ModelList) -> 23 | case boss_model_manager:is_model_instance (Data, ModelList) of 24 | true -> 25 | mochijson2:encode(boss_model_manager:to_json(Data)); 26 | false -> 27 | mochijson2:encode(json_data1(Data, ModelList, [])) 28 | end. 29 | 30 | 31 | json_data1([], _, Acc) -> 32 | {struct, lists:reverse(Acc)}; 33 | 34 | json_data1([{VariableName, [First|_] = Variable}|Rest], ModelList, Acc) when is_integer(First) -> 35 | json_data1(Rest, ModelList, [{VariableName, list_to_binary(Variable)}|Acc]); 36 | 37 | json_data1([{VariableName, [First|_] = Variable}|Rest], ModelList, Acc) when is_tuple(First) -> 38 | case boss_model_manager:is_model_instance (First, ModelList) of 39 | true -> 40 | json_data1(Rest, ModelList, [{VariableName, lists:map(fun boss_model_manager:to_json/1, Variable)}|Acc]); 41 | false -> 42 | json_data1(Rest, ModelList, [{VariableName, json_data1(Variable, ModelList, [])}|Acc]) 43 | end; 44 | 45 | json_data1([{VariableName, [[{_, _}|_]|_] = Variable}|Rest], ModelList, Acc) -> 46 | json_data1(Rest, ModelList, [{VariableName, lists:map(fun(Item) -> 47 | json_data1(Item, ModelList, []) 48 | end, Variable)}|Acc]); 49 | 50 | json_data1([{VariableName, {A, B, C} = Val}|Rest], ModelList, Acc) when is_integer(A), is_integer(B), is_integer(C) -> 51 | json_data1(Rest, ModelList, [{VariableName, list_to_binary(erlydtl_filters:date(calendar:now_to_datetime(Val), "F d, Y H:i:s"))}|Acc]); 52 | json_data1([{VariableName, Variable}|Rest], ModelList, Acc) -> 53 | case boss_model_manager:is_model_instance (Variable, ModelList) of 54 | true -> 55 | json_data1(Rest, ModelList, [{VariableName, boss_model_manager:to_json(Variable)}|Acc]); 56 | false -> 57 | json_data1(Rest, ModelList, [{VariableName, Variable}|Acc]) 58 | end. 59 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | 2 | PREFIX:=../ 3 | DEST:=$(PREFIX)$(PROJECT) 4 | ERL=erl 5 | REBAR=./rebar 6 | SESSION_CONFIG_DIR=priv/test_session_config 7 | 8 | .PHONY: deps get-deps 9 | 10 | all: 11 | @$(REBAR) get-deps 12 | @$(REBAR) compile 13 | 14 | boss: 15 | @$(REBAR) compile skip_deps=true 16 | 17 | clean: 18 | @$(REBAR) clean 19 | 20 | edoc: 21 | $(ERL) -pa ebin -pa deps/*/ebin -run boss_doc run -noshell -s init stop 22 | #$(ERL) -pa ebin -noshell -eval "boss_doc:run()" -s init stop 23 | 24 | app: 25 | @(if ! echo "$(PROJECT)" | grep -qE '^[a-z]+[a-zA-Z0-9_@]*$$'; then echo "Project name should be a valid Erlang atom."; exit 1; fi) 26 | @$(REBAR) create template=skel dest=$(DEST) src=$(PWD) appid=$(PROJECT) skip_deps=true 27 | @mkdir -p $(DEST)/deps 28 | @cp -Rn $(PWD) $(DEST)/deps/boss 29 | @mv -n $(DEST)/deps/boss/deps/* $(DEST)/deps/ 30 | 31 | get-deps: 32 | @$(REBAR) get-deps 33 | 34 | deps: 35 | @$(REBAR) compile 36 | 37 | test: 38 | @$(REBAR) skip_deps=true eunit 39 | 40 | test_session_cache: 41 | $(ERL) -pa ebin -run boss_session_test start -config $(SESSION_CONFIG_DIR)/cache -noshell 42 | 43 | test_session_mnesia: 44 | $(ERL) -pa ebin -run boss_session_test start -config $(SESSION_CONFIG_DIR)/mnesia -noshell 45 | 46 | test_session_mock: 47 | $(ERL) -pa ebin -run boss_session_test start -config $(SESSION_CONFIG_DIR)/mock -noshell 48 | 49 | rebarize: 50 | @mv $(APPDIR)/*.app.src $(APPDIR)/src 51 | @mkdir $(APPDIR)/priv/rebar 52 | @cp skel/priv/rebar/boss_plugin.erl $(APPDIR)/priv/rebar/boss_plugin.erl 53 | @cp skel/init.sh $(APPDIR) 54 | @chmod +x $(APPDIR)/init.sh 55 | @cp skel/init-dev.sh $(APPDIR) 56 | @chmod +x $(APPDIR)/init-dev.sh 57 | @cp skel/rebar $(APPDIR) 58 | @chmod +x $(APPDIR)/rebar 59 | @cp skel/rebar.config $(APPDIR) 60 | @mkdir $(APPDIR)/src/test/functional 61 | @find $(APPDIR)/src/test -maxdepth 1 -name "*.erl" -exec mv {} $(APPDIR)/src/test/functional \; 62 | @mkdir $(APPDIR)/src/test/eunit 63 | @echo $(APPDIR) rebar-boss-ified 64 | @echo WARNING: your boss.config have not been changed, you need to set: 65 | @echo - in boss app section: 66 | @echo ---- {path, \"$(PWD)\"} 67 | @echo ---- {vm_cookie, \"my_secret_cookie\"} % Optional, defaults to "abc123" 68 | @echo - for each app defined: 69 | @echo ---- {path, \"../path/to/app\"} 70 | @echo INFO: you can safely remove the Makefile and start* files from your app dir 71 | @echo INFO: after the boss.config change, you can run: 72 | @echo cd $(APPDIR) 73 | @echo ./rebar boss \# Shows all boss-rebar commands 74 | @echo ./init.sh \# Shows the new boot system commands 75 | 76 | -------------------------------------------------------------------------------- /src/boss/model_manager_adapters/boss_model_manager_boss_db.erl: -------------------------------------------------------------------------------- 1 | %% 2 | %% $Id: $ 3 | %% 4 | %% Module: boss_model -- description 5 | %% Created: 01-MAY-2012 16:32 6 | %% Author: tmr 7 | %% 8 | 9 | -module(boss_model_manager_boss_db). 10 | -behaviour(boss_model_manager_adapter). 11 | -export([ 12 | start/0, 13 | stop/0, 14 | compile/2, 15 | edoc_module/2, 16 | is_model_instance/2, 17 | dummy_instance/1, 18 | to_json/1 %, from_json/1 19 | ]). 20 | 21 | start() -> 22 | DBOptions = lists:foldl(fun(OptName, Acc) -> 23 | case application:get_env(OptName) of 24 | {ok, Val} -> [{OptName, Val}|Acc]; 25 | _ -> Acc 26 | end 27 | end, [], [db_port, db_host, db_username, db_password, db_database, 28 | db_replication_set, db_read_mode, db_write_mode, 29 | db_write_host, db_write_host_port, db_read_capacity, 30 | db_write_capacity, db_model_read_capacity, db_model_write_capacity]), 31 | 32 | DBAdapter = boss_env:get_env(db_adapter, mock), 33 | DBShards = boss_env:get_env(db_shards, []), 34 | CacheEnable = boss_env:get_env(cache_enable, false), 35 | IsMasterNode = boss_env:is_master_node(), 36 | CachePrefix = boss_env:get_env(cache_prefix, db), 37 | DBCacheEnable = boss_env:get_env(db_cache_enable, false) andalso CacheEnable, 38 | DBOptions1 = [{adapter, DBAdapter}, {cache_enable, DBCacheEnable}, {cache_prefix, CachePrefix}, 39 | {shards, DBShards}, {is_master_node, IsMasterNode}|DBOptions], 40 | 41 | boss_db:start(DBOptions1). 42 | 43 | stop() -> 44 | boss_db:stop(). 45 | 46 | compile(ModulePath, CompilerOptions) -> 47 | boss_record_compiler:compile(ModulePath, CompilerOptions). 48 | 49 | edoc_module(ModulePath, Options) -> 50 | boss_record_compiler:edoc_module(ModulePath, Options). 51 | 52 | is_model_instance(Object, AvailableModels) -> 53 | boss_record_lib:is_boss_record(Object, AvailableModels). 54 | 55 | dummy_instance(Model) -> 56 | boss_record_lib:dummy_record(Model). 57 | 58 | to_json(Object) -> 59 | Data = lists:map (fun 60 | ({Attr, Val}) when is_list (Val) -> 61 | {Attr, list_to_binary (Val)}; 62 | ({Attr, {_,_,_} = Val}) -> 63 | {Attr, erlydtl_filters:date (calendar:now_to_datetime (Val), "F d, Y H:i:s")}; 64 | ({Attr, {{_, _, _}, {_, _, _}} = Val}) -> 65 | {Attr, list_to_binary (erlydtl_filters:date (Val, "F d, Y H:i:s"))}; 66 | (Other) -> 67 | Other 68 | end, Object:attributes()), 69 | {struct, Data}. 70 | 71 | %% vim: fdm=syntax:fdn=3:tw=74:ts=2:syn=erlang 72 | -------------------------------------------------------------------------------- /src/boss/boss_erlydtl_tags.erl: -------------------------------------------------------------------------------- 1 | -module(boss_erlydtl_tags). 2 | -export([url/2]). 3 | 4 | url(Variables, Options) -> 5 | ListVars = lists:map(fun 6 | ({K, V}) when is_binary(V) -> {K, binary_to_list(V)}; 7 | ({K, V}) -> {K, V} 8 | end, Variables), 9 | ThisApp = proplists:get_value(application, Options), 10 | LinkedApp = proplists:get_value(application, ListVars, ThisApp), 11 | LinkedAppAtom = list_to_atom(lists:concat([LinkedApp])), 12 | ControllerList = boss_files:web_controller_list(LinkedApp), 13 | ThisController = proplists:get_value(controller, Options), 14 | LinkedController = proplists:get_value(controller, ListVars, ThisController), 15 | DefaultAction = case proplists:get_value(controller, ListVars) of 16 | undefined -> 17 | proplists:get_value(action, Options); 18 | _ -> 19 | undefined 20 | end, 21 | Action = proplists:get_value(action, ListVars, DefaultAction), 22 | 23 | CleanVars = lists:foldl(fun(Key, Vars) -> 24 | proplists:delete(Key, Vars) 25 | end, ListVars, [application, controller, action]), 26 | 27 | NoUndefinedVars = lists:foldl(fun 28 | ({_, undefined}, Acc) -> 29 | Acc; 30 | (KV, Acc) -> 31 | [KV|Acc] 32 | end, [], CleanVars), 33 | 34 | ProtocolPlusDomain = case ThisApp =:= LinkedApp of 35 | true -> ""; 36 | false -> 37 | case boss_web:domains(LinkedAppAtom) of 38 | all -> ""; 39 | Domains -> 40 | UseSameDomain = case proplists:get_value(host, Options) of 41 | undefined -> false; 42 | DefinedHost -> 43 | [H|_] = re:split(DefinedHost, ":", [{return, list}]), 44 | lists:member(H, Domains) 45 | end, 46 | case UseSameDomain of 47 | true -> ""; 48 | false -> "http://" ++ hd(Domains) 49 | end 50 | end 51 | end, 52 | 53 | RouterPid = proplists:get_value(router_pid, Options), 54 | URL = boss_router:unroute(RouterPid, LinkedAppAtom, ControllerList, LinkedController, Action, NoUndefinedVars), 55 | BaseURL = case proplists:get_value(base_url, Options) of 56 | undefined -> boss_web:base_url(LinkedAppAtom); 57 | "" -> boss_web:base_url(LinkedAppAtom); 58 | ProvidedBaseURL -> 59 | ProvidedBaseURL 60 | end, 61 | ProtocolPlusDomain ++ BaseURL ++ URL. 62 | -------------------------------------------------------------------------------- /rebar.config: -------------------------------------------------------------------------------- 1 | %-*-Erlang-*- 2 | {erl_opts, [debug_info, 3 | {parse_transform, lager_transform}, 4 | {parse_transform, cut}, 5 | {parse_transform, do}, 6 | {parse_transform, import_as} 7 | ]}. 8 | 9 | {template_dir, "."}. 10 | {deps, [ 11 | % maintained by CB team 12 | {boss_db, ".*", {git, "git://github.com/ChicagoBoss/boss_db.git", {tag, "v0.8.10"}}}, 13 | {tinymq, ".*", {git, "git://github.com/ChicagoBoss/tinymq.git", "HEAD"}}, 14 | {tiny_pq, ".*", {git, "git://github.com/ChicagoBoss/tiny_pq.git", {tag, "v0.8.9"}}}, 15 | {mochicow, ".*", {git, "git://github.com/ChicagoBoss/mochicow.git", "r16-compat"}}, 16 | 17 | % maintained by others 18 | {lager, ".*", {git, "git://github.com/basho/lager.git", {tag, "2.0.3"}}}, 19 | {erlando, ".*", {git, "git://github.com/travelping/erlando.git", "HEAD"}}, 20 | {erlydtl, ".*", {git, "git://github.com/erlydtl/erlydtl.git", {tag, "0.9.0"}}}, 21 | {jaderl, ".*", {git, "git://github.com/erlydtl/jaderl.git", "HEAD"}}, 22 | 23 | %% Uncomment the follwing line if you have Erlang R16B and want Elixir support 24 | %{elixir, ".*", {git, "git://github.com/elixir-lang/elixir.git", {tag, "v0.12.0"}}}, 25 | %% Then copy priv/elixir to src/ in this directory, run "mix deps.get", and recompile 26 | %% See README_ELIXIR for more details. 27 | 28 | {lfe, ".*", {git, "git://github.com/rvirding/lfe.git", "fb7c96eb17"}}, 29 | {gen_smtp, ".*", {git, "git://github.com/Vagabond/gen_smtp.git", "fd0426c464"}}, 30 | {mochiweb, ".*", {git, "git://github.com/mochi/mochiweb.git", {tag, "v2.7.0"}}}, 31 | {simple_bridge, ".*", {git, "git://github.com/nitrogen/simple_bridge.git", {tag, "v1.4.0"}}}, 32 | {poolboy, ".*", {git, "git://github.com/devinus/poolboy.git", {tag, "1.0.1"}}}, 33 | {cowboy, ".*", {git, "git://github.com/extend/cowboy.git", {tag, "0.9.0"}}}, 34 | {proper, ".*", {git, "git://github.com/manopapad/proper.git", "HEAD"}} 35 | 36 | 37 | ]}. 38 | {erlydtl_opts, [ 39 | {doc_root, "src/boss"}, 40 | {out_dir, "ebin"}, 41 | {source_ext, ".dtl"}, 42 | {module_ext, ""}, 43 | {compiler_options, [debug_info]}, 44 | report, return 45 | ]}. 46 | {lib_dirs, ["deps/elixir/lib", "deps"]}. 47 | 48 | {cover_enabled, true}. 49 | {plugins, [rebar_ct]}. 50 | -------------------------------------------------------------------------------- /src/boss/filters/boss_cache_page_filter.erl: -------------------------------------------------------------------------------- 1 | -module(boss_cache_page_filter). 2 | -export([config_key/0, config_default_value/0]). 3 | -export([before_filter/2, middle_filter/3, after_filter/3]). 4 | 5 | -define(PAGE_CACHE_PREFIX, "boss_web_controller_page"). 6 | -define(PAGE_CACHE_DEFAULT_TTL, 60). 7 | 8 | config_key() -> cache. 9 | config_default_value() -> none. 10 | 11 | before_filter({page, _}, RequestContext) -> 12 | EffectiveRequestMethod = case proplists:get_value(method, RequestContext) of 13 | 'HEAD' -> 'GET'; 14 | Method -> Method 15 | end, 16 | case (boss_env:get_env(cache_enable, false) andalso EffectiveRequestMethod =:= 'GET') of 17 | true -> 18 | Language = proplists:get_value(language, RequestContext, auto), 19 | ControllerModule = proplists:get_value(controller_module, RequestContext), 20 | Action = proplists:get_value(action, RequestContext), 21 | Tokens = proplists:get_value(tokens, RequestContext, []), 22 | 23 | CacheKey = {ControllerModule, Action, Tokens, Language}, 24 | 25 | case boss_cache:get(?PAGE_CACHE_PREFIX, CacheKey) of 26 | undefined -> 27 | {ok, RequestContext ++ [{cache_page, true}, {cache_key, CacheKey}]}; 28 | CachedRenderedResult -> 29 | {cached_page, CachedRenderedResult} 30 | end; 31 | false -> 32 | {ok, RequestContext} 33 | end; 34 | before_filter(_, RequestContext) -> 35 | {ok, RequestContext}. 36 | 37 | middle_filter({cached_page, CachedRenderedResult}, _CacheInfo, _RequestContext) -> 38 | CachedRenderedResult; 39 | middle_filter(Other, _CacheInfo, _RequestContext) -> 40 | Other. 41 | 42 | after_filter({ok, _, _} = Rendered, {page, CacheOptions}, RequestContext) -> 43 | case proplists:get_value(cache_page, RequestContext, false) of 44 | true -> 45 | CacheKey = proplists:get_value(cache_key, RequestContext), 46 | CacheTTL = proplists:get_value(seconds, CacheOptions, ?PAGE_CACHE_DEFAULT_TTL), 47 | case proplists:get_value(watch, CacheOptions) of 48 | undefined -> ok; 49 | CacheWatchString -> 50 | boss_news:set_watch({?PAGE_CACHE_PREFIX, CacheKey}, CacheWatchString, 51 | fun boss_web_controller:handle_news_for_cache/3, {?PAGE_CACHE_PREFIX, CacheKey}, CacheTTL) 52 | end, 53 | boss_cache:set(?PAGE_CACHE_PREFIX, CacheKey, Rendered, CacheTTL); 54 | false -> 55 | ok 56 | end, 57 | Rendered; 58 | after_filter(Rendered, _, _) -> 59 | Rendered. 60 | -------------------------------------------------------------------------------- /doc-src/api.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chicago Boss: The Official API Reference 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 |
13 |

The Chicago Boss API is mostly stable, but still might change before 1.0.

14 |
15 | 76 |
77 |
78 | {% block api_content %} 79 | {% endblock %} 80 |
81 | 82 | 83 | -------------------------------------------------------------------------------- /src/boss/boss_router.erl: -------------------------------------------------------------------------------- 1 | %% Author: jgordor 2 | %% Created: 01/04/2011 3 | %% Description: Minimalist Router system for Chicago Boss 4 | -module(boss_router). 5 | 6 | %% 7 | %% Exported Functions 8 | %% 9 | -export([start/0, start/1, stop/0]). 10 | -export([reload/1, route/2, unroute/6, handle/2, get_all/1, set_controllers/2]). 11 | 12 | %% 13 | %% API Functions 14 | %% 15 | 16 | start() -> 17 | start([]). 18 | 19 | start(Options) -> 20 | boss_router_sup:start_link(Options). 21 | 22 | stop() -> 23 | ok. 24 | 25 | reload(Pid) -> 26 | gen_server:call(Pid, reload). 27 | 28 | route(Pid, Url) -> 29 | gen_server:call(Pid, {route, Url}). 30 | 31 | unroute(Pid, Application, ControllerList, Controller, undefined, Params) -> 32 | ControllerModule = list_to_atom(boss_files:web_controller(Application, Controller, ControllerList)), 33 | Action = case proplists:get_value(default_action, ControllerModule:module_info(attributes)) of 34 | [DefaultAction] when is_atom(DefaultAction) -> 35 | atom_to_list(DefaultAction); 36 | _ -> 37 | "index" 38 | end, 39 | unroute(Pid, Application, ControllerList, Controller, Action, Params); 40 | unroute(Pid, Application, ControllerList, Controller, Action, Params) -> 41 | case gen_server:call(Pid, {unroute, Controller, Action, Params}) of 42 | undefined -> 43 | % Do the fancy routing in the calling process 44 | % to take some pressure off the router process. 45 | % In the future the router needs workers. 46 | ControllerModule = list_to_atom(boss_files:web_controller(Application, Controller, ControllerList)), 47 | {Tokens, Variables1} = boss_controller_lib:convert_params_to_tokens(Params, ControllerModule, list_to_atom(Action)), 48 | 49 | URL = case Tokens of 50 | [] -> 51 | lists:concat(["/", Controller, "/", Action]); 52 | _ -> 53 | lists:concat(["/", Controller, "/", Action | 54 | lists:foldr(fun(T, Acc) -> ["/", mochiweb_util:quote_plus(T) | Acc] end, [], Tokens)]) 55 | end, 56 | QueryString = mochiweb_util:urlencode(Variables1), 57 | case QueryString of 58 | "" -> 59 | URL; 60 | _ -> 61 | URL ++ "?" ++ QueryString 62 | end; 63 | RoutedURL -> 64 | RoutedURL 65 | end. 66 | 67 | handle(Pid, StatusCode) -> 68 | gen_server:call(Pid, {handle, StatusCode}). 69 | 70 | get_all(Pid) -> 71 | gen_server:call(Pid, get_all). 72 | 73 | set_controllers(Pid, Controllers) -> 74 | gen_server:call(Pid, {set_controllers, Controllers}). 75 | -------------------------------------------------------------------------------- /include/httpd_r12b5.hrl: -------------------------------------------------------------------------------- 1 | %% 2 | %% 1997-2008 3 | %% Ericsson AB, All Rights Reserved 4 | %% 5 | %% 6 | %% The contents of this file are subject to the Erlang Public License, 7 | %% Version 1.1, (the "License"); you may not use this file except in 8 | %% compliance with the License. You should have received a copy of the 9 | %% Erlang Public License along with this software. If not, it can be 10 | %% retrieved online at http://www.erlang.org/. 11 | %% 12 | %% Software distributed under the License is distributed on an "AS IS" 13 | %% basis, WITHOUT WARRANTY OF ANY KIND, either express or implied. See 14 | %% the License for the specific language governing rights and limitations 15 | %% under the License. 16 | %% 17 | %% The Initial Developer of the Original Code is Ericsson AB. 18 | %% 19 | %% 20 | 21 | -include_lib("kernel/include/file.hrl"). 22 | 23 | -ifndef(SERVER_SOFTWARE). 24 | -define(SERVER_SOFTWARE,"inets/develop"). % Define in Makefile! 25 | -endif. 26 | -define(SERVER_PROTOCOL,"HTTP/1.1"). 27 | -define(DEFAULT_MODS, [mod_alias, mod_auth, mod_esi, mod_actions, mod_cgi, 28 | mod_dir, mod_get, mod_head, mod_log, mod_disk_log]). 29 | -define(SOCKET_CHUNK_SIZE,8192). 30 | -define(SOCKET_MAX_POLL,25). 31 | -define(FILE_CHUNK_SIZE,64*1024). 32 | -define(GATEWAY_INTERFACE,"CGI/1.1"). 33 | -define(NICE(Reason),lists:flatten(atom_to_list(?MODULE)++": "++Reason)). 34 | -define(DEFAULT_CONTEXT, 35 | [{errmsg,"[an error occurred while processing this directive]"}, 36 | {timefmt,"%A, %d-%b-%y %T %Z"}, 37 | {sizefmt,"abbrev"}]). 38 | 39 | 40 | -ifdef(inets_error). 41 | -define(ERROR(Format, Args), io:format("E(~p:~p:~p) : "++Format++"~n", 42 | [self(),?MODULE,?LINE]++Args)). 43 | -else. 44 | -define(ERROR(F,A),[]). 45 | -endif. 46 | 47 | -ifdef(inets_log). 48 | -define(LOG(Format, Args), io:format("L(~p:~p:~p) : "++Format++"~n", 49 | [self(),?MODULE,?LINE]++Args)). 50 | -else. 51 | -define(LOG(F,A),[]). 52 | -endif. 53 | 54 | -ifdef(inets_debug). 55 | -define(DEBUG(Format, Args), io:format("D(~p:~p:~p) : "++Format++"~n", 56 | [self(),?MODULE,?LINE]++Args)). 57 | -else. 58 | -define(DEBUG(F,A),[]). 59 | -endif. 60 | 61 | -ifdef(inets_cdebug). 62 | -define(CDEBUG(Format, Args), io:format("C(~p:~p:~p) : "++Format++"~n", 63 | [self(),?MODULE,?LINE]++Args)). 64 | -else. 65 | -define(CDEBUG(F,A),[]). 66 | -endif. 67 | 68 | 69 | -record(init_data,{peername,resolve}). 70 | -record(mod,{init_data, 71 | data=[], 72 | socket_type=ip_comm, 73 | socket, 74 | config_db, 75 | method, 76 | absolute_uri=[], 77 | request_uri, 78 | http_version, 79 | request_line, 80 | parsed_header=[], 81 | entity_body, 82 | connection}). 83 | -------------------------------------------------------------------------------- /src/boss/boss_session.erl: -------------------------------------------------------------------------------- 1 | %% @doc Chicago Boss session handler abstraction 2 | 3 | -module(boss_session). 4 | 5 | -export([start/0, start/1, stop/0]). 6 | -export([get_session_key/0, get_session_exp_time/0]). 7 | -export([new_session/1, get_session_data/1, get_session_data/2, set_session_data/3]). 8 | -export([remove_session_data/2, delete_session/1]). 9 | 10 | -define(POOLNAME, boss_session_pool). 11 | 12 | start() -> 13 | SessionOptions = make_session_options(), 14 | SessionDriver = boss_env:get_env(session_adapter, mock), 15 | Adapter = list_to_atom(lists:concat(["boss_session_adapter_", SessionDriver])), 16 | Adapter:init([]), 17 | SessionOptions1 = [{adapter, Adapter}|SessionOptions], 18 | start(SessionOptions1). 19 | 20 | make_session_options() -> 21 | lists:foldl(fun(OptName, Acc) -> 22 | case application:get_env(OptName) of 23 | {ok, Val} -> [{OptName, Val}|Acc]; 24 | _ -> Acc 25 | end 26 | end, [], [session_key, session_exp_time]). 27 | 28 | start(Options) -> 29 | boss_session_sup:start_link(Options). 30 | 31 | stop() -> 32 | ok. 33 | 34 | get_session_key() -> 35 | boss_env:get_env(session_key, "_boss_session"). 36 | 37 | get_session_exp_time() -> 38 | boss_env:get_env(session_exp_time, 1440). 39 | 40 | %% @spec new_session(Cookie::string()) -> string | {error, Reason} 41 | %% @doc Starts new session with the specified `Cookie'. 42 | new_session(Cookie) -> 43 | boss_pool:call(?POOLNAME, {new_session, Cookie}). 44 | 45 | %% @spec get_session_data(SessionID) -> list | {error, Reason} 46 | %% @doc Get session data for the `SessionID'. 47 | get_session_data(SessionID) -> 48 | boss_pool:call(?POOLNAME, {get_session_data, SessionID}). 49 | 50 | %% @spec get_session_data(SessionID, Key) -> list | {error, Reason} 51 | %% @doc Get session data for the `SessionID' for a given `Key'. 52 | get_session_data(SessionID, Key) -> 53 | boss_pool:call(?POOLNAME, {get_session_data, SessionID, Key}). 54 | 55 | %% @spec set_session_data(SessionID, Key, Value) -> ok | {error, Reason} 56 | %% @doc Set session data for the `SessionID'. 57 | set_session_data(SessionID, Key, Value) -> 58 | boss_pool:call(?POOLNAME, {set_session_data, SessionID, Key, Value}). 59 | 60 | %% @spec delete_session(SessionID) -> ok | {error, Reason} 61 | %% @doc Delete session for given `SessionID'. 62 | delete_session(SessionID) -> 63 | boss_pool:call(?POOLNAME, {delete_session, SessionID}). 64 | 65 | %% @spec remove_session_data(SessionID, Key) -> ok | {error, Reason} 66 | %% @doc Remove the Key from session data for the `SessionID'. 67 | remove_session_data(SessionID, Key) -> 68 | boss_pool:call(?POOLNAME, {remove_session_data, SessionID, Key}). 69 | -------------------------------------------------------------------------------- /src/boss_test/boss_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_test). 2 | -export([process_assertions_and_continuations/3, process_assertions_and_continuations/6]). 3 | 4 | process_assertions_and_continuations(Assertions, Continuations, ParsedResponse) -> 5 | process_assertions_and_continuations(Assertions, Continuations, ParsedResponse, 6 | fun() -> ok end, fun() -> ok end, fun() -> "" end). 7 | 8 | process_assertions_and_continuations(Assertions, Continuations, ParsedResponse, PushFun, PopFun, DumpFun) -> 9 | Depth = case get(boss_test_depth) of 10 | undefined -> 0; 11 | D -> D 12 | end, 13 | {NumSuccesses, FailureMessages} = process_assertions(Assertions, ParsedResponse), 14 | case length(FailureMessages) of 15 | 0 -> 16 | io:format("~3B passed~n", [NumSuccesses]), 17 | {NewS, NewF} = process_continuations(Continuations, ParsedResponse, PushFun, PopFun, Depth), 18 | {NumSuccesses + NewS, FailureMessages ++ NewF}; 19 | N -> 20 | io:format("~c[01;31m~3B failed~c[00m~n", [16#1B, N, 16#1B]), 21 | lists:map(fun(Msg) -> 22 | io:format("~s* ~c[01m~p~c[00m~n", 23 | [lists:duplicate(Depth, $\ ), 24 | 16#1B, Msg, 16#1B]) 25 | end, FailureMessages), 26 | io:format("Last response: ~p~n~n", [ParsedResponse]), 27 | io:format("Database: ~p~n~n", [DumpFun()]), 28 | {NumSuccesses, FailureMessages} 29 | end. 30 | 31 | process_assertions(Assertions, ParsedResponse) -> 32 | lists:foldl(fun 33 | (AssertionFun, {N, Acc}) when is_function(AssertionFun) -> 34 | case AssertionFun(ParsedResponse) of 35 | {true, _Msg} -> 36 | {N+1, Acc}; 37 | {false, Msg} -> 38 | {N, [Msg|Acc]} 39 | end 40 | end, {0, []}, Assertions). 41 | 42 | process_continuations(Continuations, Response, PushFun, PopFun, Depth) -> 43 | process_continuations(Continuations, Response, PushFun, PopFun, Depth, {0, []}). 44 | 45 | process_continuations([], _, _, _, _, {NumSuccesses, FailureMessages}) -> 46 | {NumSuccesses, lists:reverse(FailureMessages)}; 47 | process_continuations([Name, Fun | Rest], Response, PushFun, PopFun, Depth, {NumSuccesses, FailureMessages}) 48 | when is_list(Name) and is_function(Fun) -> 49 | io:format("~-60s", [lists:duplicate(Depth, $\ ) ++ Name]), 50 | PushFun(), 51 | put(boss_test_depth, Depth + 1), 52 | {TheseSuccesses, TheseFailureMessages} = Fun(Response), 53 | put(boss_test_depth, Depth), 54 | PopFun(), 55 | process_continuations(Rest, Response, PushFun, PopFun, Depth, {NumSuccesses + TheseSuccesses, TheseFailureMessages ++ FailureMessages}). 56 | -------------------------------------------------------------------------------- /src/boss/boss_service_sup.erl: -------------------------------------------------------------------------------- 1 | %%%------------------------------------------------------------------- 2 | %%% @author mihawk 3 | %%% @copyright (C) 2012, mihawk 4 | %%% @doc 5 | %%% 6 | %%% @end 7 | %%% Created : 18 Jul 2012 by mihawk 8 | %%%------------------------------------------------------------------- 9 | -module(boss_service_sup). 10 | 11 | -behaviour(supervisor). 12 | 13 | %% API 14 | -export([start_link/0, start_services/2]). 15 | 16 | %% Supervisor callbacks 17 | -export([init/1]). 18 | 19 | -define(SERVER, ?MODULE). 20 | 21 | %%%=================================================================== 22 | %%% API functions 23 | %%%=================================================================== 24 | 25 | %%-------------------------------------------------------------------- 26 | %% @doc 27 | %% Starts the supervisor 28 | %% 29 | %% @spec start_link() -> {ok, Pid} | ignore | {error, Error} 30 | %% @end 31 | %%-------------------------------------------------------------------- 32 | start_link() -> 33 | supervisor:start_link({global, ?SERVER}, ?MODULE, []). 34 | 35 | %%%=================================================================== 36 | %%% Supervisor callbacks 37 | %%%=================================================================== 38 | 39 | %%-------------------------------------------------------------------- 40 | %% @private 41 | %% @doc 42 | %% Whenever a supervisor is started using supervisor:start_link/[2,3], 43 | %% this function is called by the new process to find out about 44 | %% restart strategy, maximum restart frequency and child 45 | %% specifications. 46 | %% 47 | %% @spec init(Args) -> {ok, {SupFlags, [ChildSpec]}} | 48 | %% ignore | 49 | %% {error, Reason} 50 | %% @end 51 | %%-------------------------------------------------------------------- 52 | 53 | start_services(SupPid, boss_websocket_router) -> 54 | {ok, BossWebSocketRouterPid} = 55 | supervisor:start_child(SupPid, 56 | {boss_websocket_router, {boss_websocket_router, start_link, []}, 57 | permanent, 5000, worker, [boss_websocket_router]}), 58 | {ok, BossWebSocketRouterPid}; 59 | 60 | 61 | start_services(SupPid, Services) -> 62 | lists:foldl( 63 | fun([], Acc) -> Acc ; 64 | ({ServiceUrl, Service}, Acc) -> 65 | {ok, ServicePid} = 66 | supervisor:start_child(SupPid, 67 | {Service, {boss_service_worker, start_link, [Service, ServiceUrl]}, 68 | permanent, 5000, worker, [Service]}), 69 | Acc ++ [{ok, ServicePid}] 70 | end, 71 | [], 72 | Services ), 73 | {ok, SupPid}. 74 | 75 | init([]) -> 76 | {ok, {{one_for_all, 10, 10}, []}}. 77 | 78 | %%%=================================================================== 79 | %%% Internal functions 80 | %%%=================================================================== 81 | -------------------------------------------------------------------------------- /src/boss/template_adapters/boss_template_adapter_eex.erl: -------------------------------------------------------------------------------- 1 | -module(boss_template_adapter_eex). 2 | -compile(export_all). 3 | 4 | file_extensions() -> ["eex"]. 5 | 6 | translatable_strings(_Module) -> []. 7 | 8 | source(_Module) -> "". 9 | 10 | dependencies(_Module) -> []. 11 | 12 | render(Module, Variables, _Options) -> 13 | {ok, Module:render(Variables)}. 14 | 15 | compile_file(ViewPath, Module, Options) -> 16 | OutDir = proplists:get_value(out_dir, Options), 17 | CompilerOptions = proplists:get_value(compiler_options, Options, []), 18 | EExAst = 'Elixir.EEx':compile_file(ViewPath), 19 | {ErlAst, _} = elixir:translate_forms([EExAst], [{ assigns, [] }], [{delegate_locals_to, ?MODULE}]), 20 | 21 | Render0FunctionAst = erl_syntax:function(erl_syntax:atom(render), 22 | [erl_syntax:clause([erl_syntax:variable("Variables")], none, [ 23 | {match, 0, {var, 0, assigns}, {var, 0, 'Variables'}} | ErlAst])]), 24 | 25 | ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [erl_syntax:atom(Module)]), 26 | 27 | ExportAst = erl_syntax:attribute(erl_syntax:atom(export), 28 | [erl_syntax:list([erl_syntax:arity_qualifier(erl_syntax:atom(render), 29 | erl_syntax:integer(1))])]), 30 | 31 | Forms = [erl_syntax:revert(X) || X <- [ModuleAst, ExportAst, Render0FunctionAst]], 32 | 33 | case compile_forms_and_reload(ViewPath, Forms, CompilerOptions) of 34 | {ok, Module, Bin, _Warnings} -> 35 | case OutDir of 36 | undefined -> {ok, Module}; 37 | _ -> 38 | BeamFile = filename:join([OutDir, atom_to_list(Module) ++ ".beam"]), 39 | case file:write_file(BeamFile, Bin) of 40 | ok -> {ok, Module}; 41 | {error, Reason} -> 42 | {error, lists:flatten( 43 | io_lib:format("Beam generation of '~s' failed: ~p", 44 | [BeamFile, file:format_error(Reason)]))} 45 | end 46 | end; 47 | Err -> 48 | Err 49 | end. 50 | 51 | compile_forms_and_reload(File, Forms, CompilerOptions) -> 52 | case compile:forms(Forms, CompilerOptions) of 53 | {ok, Module1, Bin} -> 54 | load_code(Module1, Bin, []); 55 | {ok, Module1, Bin, Warnings} -> 56 | load_code(Module1, Bin, Warnings); 57 | error -> 58 | {error, lists:concat(["compilation failed: ", File])}; 59 | OtherError -> 60 | OtherError 61 | end. 62 | 63 | load_code(Module, Bin, Warnings) -> 64 | code:purge(Module), 65 | case code:load_binary(Module, atom_to_list(Module) ++ ".erl", Bin) of 66 | {module, _} -> {ok, Module, Bin, Warnings}; 67 | _ -> {error, lists:concat(["code reload failed: ", Module])} 68 | end. 69 | -------------------------------------------------------------------------------- /src/boss/template_adapters/boss_template_adapter_erlydtl.erl: -------------------------------------------------------------------------------- 1 | -module(boss_template_adapter_erlydtl). 2 | -compile(export_all). 3 | 4 | -spec file_extensions() -> [[100 | 104 | 106 | 108 | 109 | 115 | 116 | 120,...],...]. 5 | -spec translatable_strings(atom() | tuple()) -> any(). 6 | -spec source(atom() | tuple()) -> any(). 7 | -spec dependencies(atom() | tuple()) -> any(). 8 | -spec render(atom() | tuple(),_,_) -> {ok, iolist()}|{error, _}. 9 | -spec compile_file(atom() | binary() | [atom() | [any()] | char()],_,[any()]) -> any(). 10 | -spec compile(atom() | binary() | [atom() | [any()] | char()],_,_,_,_,_,_,_,[any()],[any()],[any()],_) -> any(). 11 | file_extensions() -> ["dtl", "html", "txt", "js"]. 12 | 13 | translatable_strings(Module) -> 14 | Module:translatable_strings(). 15 | 16 | source(Module) -> 17 | Module:source(). 18 | 19 | dependencies(Module) -> 20 | Module:dependencies(). 21 | 22 | 23 | render(Module, Variables, RenderOptions) -> 24 | Module:render(Variables, RenderOptions). 25 | 26 | compile_file(ViewPath, Module, Options) -> 27 | HelperDirModule = proplists:get_value(helper_module, Options), 28 | TranslatorPid = proplists:get_value(translator_pid, Options), 29 | OutDir = proplists:get_value(out_dir, Options), 30 | CompilerOptions = proplists:get_value(compiler_options, Options, []), 31 | Locales = proplists:get_value(locales, Options, []), 32 | DocRoot = proplists:get_value(doc_root, Options, "."), 33 | TagHelpers = proplists:get_value(tag_helpers, Options, []), 34 | FilterHelpers = proplists:get_value(filter_helpers, Options, []), 35 | ExtraTagHelpers = boss_env:get_env(template_tag_modules, []), 36 | ExtraFilterHelpers = boss_env:get_env(template_filter_modules, []), 37 | Res = compile(ViewPath, Module, HelperDirModule, TranslatorPid, OutDir, 38 | CompilerOptions, Locales, DocRoot, TagHelpers, FilterHelpers, 39 | ExtraTagHelpers, ExtraFilterHelpers), 40 | case Res of 41 | ok -> 42 | {ok, Module}; 43 | Err -> Err 44 | end. 45 | 46 | 47 | compile(ViewPath, Module, HelperDirModule, TranslatorPid, OutDir, 48 | CompilerOptions, Locales, DocRoot, TagHelpers, FilterHelpers, 49 | ExtraTagHelpers, ExtraFilterHelpers) -> 50 | CompileParams = [{doc_root, DocRoot}, 51 | {custom_tags_modules, TagHelpers ++ ExtraTagHelpers ++ [boss_erlydtl_tags, HelperDirModule]}, 52 | {custom_filters_modules, FilterHelpers ++ ExtraFilterHelpers}, 53 | {compiler_options, CompilerOptions}, 54 | {out_dir, OutDir}, 55 | {blocktrans_fun, 56 | fun(BlockString, Locale) -> 57 | case boss_translator:lookup(TranslatorPid, BlockString, Locale) of 58 | undefined -> default; 59 | Body -> list_to_binary(Body) 60 | end 61 | end}, 62 | {blocktrans_locales, Locales}], 63 | erlydtl:compile(ViewPath, 64 | Module, 65 | CompileParams). 66 | -------------------------------------------------------------------------------- /src/boss/boss_session_controller.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_controller). 2 | 3 | -behaviour(gen_server). 4 | 5 | -export([start_link/0, start_link/1]). 6 | 7 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 8 | 9 | -record(state, { 10 | adapter, 11 | connection 12 | }). 13 | 14 | start_link() -> 15 | start_link([]). 16 | 17 | start_link(Args) -> 18 | gen_server:start_link(?MODULE, Args, []). 19 | 20 | init(Options) -> 21 | Adapter = proplists:get_value(adapter, Options, boss_session_adapter_mock), 22 | {A1, A2, A3} = now(), 23 | random:seed(A1,A2,A3), 24 | {ok, Conn} = Adapter:start(Options), 25 | {ok, #state{adapter = Adapter, connection = Conn }}. 26 | 27 | handle_call({new_session, Cookie}, _From, State) -> 28 | Adapter = State#state.adapter, 29 | Conn = State#state.connection, 30 | NewSessionID = case Adapter:session_exists(Conn, Cookie) of 31 | true -> 32 | Cookie; 33 | false -> 34 | SessionID = generate_session_id(), 35 | Adapter:create_session(Conn, SessionID, []), 36 | SessionID 37 | end, 38 | {reply, NewSessionID, State}; 39 | 40 | handle_call({get_session_data, Sid}, _From, State) -> 41 | Adapter = State#state.adapter, 42 | Conn = State#state.connection, 43 | {reply, Adapter:lookup_session(Conn, Sid), State}; 44 | 45 | handle_call({get_session_data, Sid, Key}, _From, State) -> 46 | Adapter = State#state.adapter, 47 | Conn = State#state.connection, 48 | {reply, Adapter:lookup_session_value(Conn, Sid, Key), State}; 49 | 50 | handle_call({set_session_data, Sid, Key, Value}, _From, State) -> 51 | Adapter = State#state.adapter, 52 | Conn = State#state.connection, 53 | {reply, Adapter:set_session_value(Conn, Sid, Key, Value), State}; 54 | 55 | handle_call({delete_session, SessionID}, _From, State) -> 56 | Adapter = State#state.adapter, 57 | Conn = State#state.connection, 58 | {reply, Adapter:delete_session(Conn, SessionID), State}; 59 | 60 | handle_call({remove_session_data, Sid, Key}, _From, State) -> 61 | Adapter = State#state.adapter, 62 | Conn = State#state.connection, 63 | {reply, Adapter:delete_session_value(Conn, Sid, Key), State}. 64 | 65 | handle_cast(_Request, State) -> 66 | {noreply, State}. 67 | 68 | terminate(_Reason, State) -> 69 | Adapter = State#state.adapter, 70 | Conn = State#state.connection, 71 | Adapter:stop(Conn). 72 | 73 | code_change(_OldVsn, State, _Extra) -> 74 | {ok, State}. 75 | 76 | handle_info(_Info, State) -> 77 | {noreply, State}. 78 | 79 | generate_session_id() -> 80 | Data = crypto:rand_bytes(2048), 81 | Sha_list = binary_to_list(crypto:hash(sha,Data)), 82 | lists:flatten(list_to_hex(Sha_list)). 83 | %% Convert Integer from the SHA to Hex 84 | list_to_hex(L)-> 85 | lists:map(fun(X) -> int_to_hex(X) end, L). 86 | 87 | int_to_hex(N) when N < 256 -> 88 | [hex(N div 16), hex(N rem 16)]. 89 | 90 | hex(N) when N < 10 -> 91 | $0+N; 92 | hex(N) when N >= 10, N < 16 -> 93 | $a + (N-10). 94 | -------------------------------------------------------------------------------- /src/boss/boss_mail_driver_mock.erl: -------------------------------------------------------------------------------- 1 | -module(boss_mail_driver_mock). 2 | -export([start/0, stop/1, deliver/5, read/2, push/0, pop/0]). 3 | 4 | start() -> 5 | register(boss_mock_inbox, spawn(fun() -> loop([]) end)), 6 | {ok, undefined}. 7 | 8 | stop(_) -> 9 | ok. 10 | 11 | loop([]) -> 12 | loop([dict:new()]); 13 | loop([InboxDict|OldState] = State) -> 14 | receive 15 | {From, {read, ToAddress, Subject}} -> 16 | Inbox = case dict:find(ToAddress, InboxDict) of 17 | {ok, Val} -> Val; 18 | _ -> [] 19 | end, 20 | case first_email_with_subject(Subject, Inbox) of 21 | undefined -> 22 | From ! {boss_mock_inbox, undefined}, 23 | loop(State); 24 | Email -> 25 | NewInbox = lists:delete(Email, Inbox), 26 | From ! {boss_mock_inbox, Email}, 27 | loop([dict:store(ToAddress, NewInbox, InboxDict)|OldState]) 28 | end; 29 | {From, {deliver, _FromAddress, ToAddress, Email}} -> 30 | Inbox = case dict:find(ToAddress, InboxDict) of 31 | {ok, Val} -> Val; 32 | _ -> [] 33 | end, 34 | DecodableEmail = iolist_to_binary(Email), 35 | ParsedEmail = mimemail:decode(DecodableEmail), 36 | From ! {boss_mock_inbox, ok}, 37 | loop([dict:store(ToAddress, [ParsedEmail|Inbox], InboxDict)|OldState]); 38 | {From, push} -> 39 | From ! {boss_mock_inbox, ok}, 40 | loop([InboxDict|State]); 41 | {From, pop} -> 42 | From ! {boss_mock_inbox, ok}, 43 | loop(OldState) 44 | end. 45 | 46 | deliver(_, FromAddress, ToAddress, BodyFun, _ResultFun) -> 47 | % TODO use ResultFun 48 | boss_mock_inbox ! {self(), {deliver, FromAddress, ToAddress, BodyFun()}}, 49 | receive 50 | {boss_mock_inbox, ok} -> 51 | ok 52 | end. 53 | 54 | read(ToAddress, Subject) -> 55 | boss_mock_inbox ! {self(), {read, ToAddress, Subject}}, 56 | receive 57 | {boss_mock_inbox, Email} -> 58 | Email 59 | end. 60 | 61 | push() -> 62 | case lists:member(boss_mock_inbox, erlang:registered()) of 63 | true -> 64 | boss_mock_inbox ! {self(), push}, 65 | receive {boss_mock_inbox, ok} -> ok end; 66 | false -> 67 | ok 68 | end. 69 | 70 | pop() -> 71 | case lists:member(boss_mock_inbox, erlang:registered()) of 72 | true -> 73 | boss_mock_inbox ! {self(), pop}, 74 | receive {boss_mock_inbox, ok} -> ok end; 75 | false -> 76 | ok 77 | end. 78 | 79 | % internal 80 | 81 | first_email_with_subject(Subject, Inbox) when is_list(Subject) -> 82 | first_email_with_subject(list_to_binary(Subject), Inbox); 83 | first_email_with_subject(_Subject, []) -> 84 | undefined; 85 | first_email_with_subject(Subject, [{_, _, Headers, _, _} = Email|Rest]) -> 86 | case proplists:get_value(<<"Subject">>, Headers) of 87 | Subject -> Email; 88 | _ -> first_email_with_subject(Subject, Rest) 89 | end. 90 | -------------------------------------------------------------------------------- /test/boss_load_test.erl: -------------------------------------------------------------------------------- 1 | -module(boss_load_test). 2 | -include_lib("proper/include/proper.hrl"). 3 | -include_lib("eunit/include/eunit.hrl"). 4 | 5 | 6 | 7 | load_view_inner_test() -> 8 | Inner = boss_load:load_views_inner(test, ".", self()), 9 | ?assert(is_function(Inner, 2)), 10 | [DtlModule] = Inner("../test/good.dtl", []), 11 | ?assertEqual(test_test_good_dtl, DtlModule), 12 | Exports = DtlModule:module_info(exports), 13 | ?assertEqual([0,1,2], proplists:get_all_values(render, Exports)). 14 | 15 | load_view_inner_bad_test() -> 16 | Inner = boss_load:load_views_inner(test, ".", self()), 17 | ?assert(is_function(Inner, 2)), 18 | ?assertEqual([],Inner("../test/bad.dtl", [])). 19 | 20 | 21 | load_view_inner_no_file_test() -> 22 | Inner = boss_load:load_views_inner(test, ".", self()), 23 | ?assert(is_function(Inner, 2)), 24 | ?assertEqual([test], Inner("../test/no_file.dtl", [test])). 25 | 26 | 27 | 28 | module_is_loaded_test() -> 29 | ?assert(boss_load:module_is_loaded('boss_load')), 30 | ?assertNot(boss_load:module_is_loaded('boss_load1')), 31 | ?assert(proper:check_spec({boss_load, module_is_loaded, 1}, 32 | 33 | [{to_file, user}])), 34 | ok. 35 | 36 | 37 | make_computed_vsn_test() -> 38 | ?assertEqual("ok", boss_load:make_computed_vsn("echo ok")), 39 | ?assert(proper:quickcheck(prop_make_computed_vsn(), 40 | [{to_file, user}])), 41 | ok. 42 | 43 | 44 | prop_make_computed_vsn() -> 45 | ?FORALL(X, 46 | any(), 47 | X =:= boss_load:make_computed_vsn({unknown, X})). 48 | 49 | 50 | make_all_modules_test() -> 51 | ?assertEqual([], boss_load:make_all_modules(test, "/tmp", [])), 52 | ?assert(proper:quickcheck(prop_make_all_modules(), 53 | [{to_file, user}])), 54 | ?assert(proper:quickcheck(prop_make_all_modules_error(), 55 | [{to_file, user}])), 56 | 57 | ok. 58 | 59 | -type op_key() :: test_modules|lib_modules|websocket_modules|mail_modules|controller_modules| 60 | model_modules| view_lib_tags_modules|view_lib_helper_modules|view_modules. 61 | -type op_list_el():: {op_key(), [atom()]}. 62 | prop_make_all_modules()-> 63 | ?FORALL( 64 | OpKeys, 65 | [op_list_el()], 66 | begin 67 | Application = test, 68 | OutDir = "/tmp", 69 | Ops = [{Op, fun(IApp,IOutDir) -> 70 | IApp = Application, 71 | IOutDir = OutDir, 72 | {ok, Modules} 73 | end}|| {Op, Modules} <- OpKeys], 74 | 75 | Result = boss_load:make_all_modules(Application, OutDir, Ops), 76 | 77 | Result -- OpKeys =:= [] 78 | end). 79 | 80 | prop_make_all_modules_error()-> 81 | ?FORALL( 82 | OpKeys, 83 | [op_list_el()], 84 | begin 85 | Application = test, 86 | OutDir = "/tmp", 87 | Ops = [{Op, fun(IApp,IOutDir) -> 88 | IApp = Application, 89 | IOutDir = OutDir, 90 | {error, "test"} 91 | end}|| {Op, _Modules} <- OpKeys], 92 | 93 | Result = boss_load:make_all_modules(Application, OutDir, Ops), 94 | [] =:= lists:concat([Value|| {_, Value} <-Result]) 95 | 96 | end). 97 | 98 | make_ops_list_test() -> 99 | OpsList = boss_load:make_ops_list(self()), 100 | ?assert(lists:all(fun({Op, Fun}) -> 101 | is_atom(Op) andalso is_function(Fun,2) 102 | end, OpsList)), 103 | ok. 104 | -------------------------------------------------------------------------------- /skel/priv/init/news.erl: -------------------------------------------------------------------------------- 1 | -module({{appid}}_01_news). 2 | 3 | -export([init/0, stop/1]). 4 | 5 | % This script is first executed at server startup and should 6 | % return a list of WatchIDs that should be cancelled in the stop 7 | % function below (stop is executed if the script is ever reloaded). 8 | init() -> 9 | {ok, []}. 10 | 11 | stop(ListOfWatchIDs) -> 12 | lists:map(fun boss_news:cancel_watch/1, ListOfWatchIDs). 13 | 14 | %%%%%%%%%%% Ideas 15 | % boss_news:watch("user-42.*", 16 | % fun 17 | % (updated, {Donald, 'location', OldLocation, NewLocation}) -> 18 | % ; 19 | % (updated, {Donald, 'email_address', OldEmail, NewEmail}) 20 | % end), 21 | % 22 | % boss_news:watch("user-*.status", 23 | % fun(updated, {User, 'status', OldStatus, NewStatus}) -> 24 | % Followers = User:followers(), 25 | % lists:map(fun(Follower) -> 26 | % Follower:notify_status_update(User, NewStatus) 27 | % end, Followers) 28 | % end), 29 | % 30 | % boss_news:watch("users", 31 | % fun 32 | % (created, NewUser) -> 33 | % boss_mail:send(?WEBSITE_EMAIL_ADDRESS, 34 | % ?ADMINISTRATOR_EMAIL_ADDRESS, 35 | % "New account!", 36 | % "~p just created an account!~n", 37 | % [NewUser:name()]); 38 | % (deleted, OldUser) -> 39 | % ok 40 | % end), 41 | % 42 | % boss_news:watch("forum_replies", 43 | % fun 44 | % (created, Reply) -> 45 | % OrignalPost = Reply:original_post(), 46 | % OriginalAuthor = OriginalPost:author(), 47 | % case OriginalAuthor:is_online() of 48 | % true -> 49 | % boss_mq:push(OriginalAuthor:comet_channel(), <<"Someone replied!">>); 50 | % false -> 51 | % case OriginalAuthor:likes_email() of 52 | % true -> 53 | % boss_mail:send("website@blahblahblah", 54 | % OriginalAuthor:email_address(), 55 | % "Someone replied!" 56 | % "~p has replied to your post on ~p~n", 57 | % [(Reply:author()):name(), OriginalPost:title()]); 58 | % false -> 59 | % ok 60 | % end 61 | % end; 62 | % (_, _) -> ok 63 | % end), 64 | % 65 | % boss_news:watch("forum_categories", 66 | % fun 67 | % (created, NewCategory) -> 68 | % boss_mail:send(?WEBSITE_EMAIL_ADDRESS, 69 | % ?ADMINISTRATOR_EMAIL_ADDRESS, 70 | % "New category: "++NewCategory:name(), 71 | % "~p has created a new forum category called \"~p\"~n", 72 | % [(NewCategory:created_by()):name(), NewCategory:name()]); 73 | % (_, _) -> ok 74 | % end), 75 | % 76 | % boss_news:watch("forum_category-*.is_deleted", 77 | % fun 78 | % (updated, {ForumCategory, 'is_deleted', false, true}) -> 79 | % ; 80 | % (updated, {ForumCategory, 'is_deleted', true, false}) -> 81 | % end). 82 | 83 | % Invoking the API directly: 84 | %boss_news:deleted("person-42", OldAttrs), 85 | %boss_news:updated("person-42", OldAttrs, NewAttrs), 86 | %boss_news:created("person-42", NewAttrs) 87 | 88 | % Invoking the API via HTTP (with the admin application installed): 89 | % POST /admin/news_api/deleted/person-42 90 | % old[status] = something 91 | 92 | % POST /admin/news_api/updated/person-42 93 | % old[status] = blah 94 | % new[status] = barf 95 | 96 | % POST /admin/news_api/created/person-42 97 | % new[status] = something 98 | -------------------------------------------------------------------------------- /src/boss/boss_web_controller_util.erl: -------------------------------------------------------------------------------- 1 | -module(boss_web_controller_util). 2 | 3 | -export([make_boss_app_info/10]). 4 | 5 | -export([unpack_application_env/1]). 6 | 7 | -export([find_application_for_path/3]). 8 | 9 | -export([execution_mode/1]). 10 | 11 | -include("boss_web.hrl"). 12 | 13 | -spec execution_mode(types:application()) -> types:execution_mode(). 14 | -spec find_application_for_path('undefined' | binary() | maybe_improper_list(binary() | maybe_improper_list(any(),binary() | []) | char(),binary() | []),_,[any()]) -> any(). 15 | -spec make_boss_app_info(types:application(),_,_,_,_,_,_,_,_,_) -> #boss_app_info{}. 16 | -spec unpack_application_env(types:application()) -> {_,_,_,_,_,_,_,_,_,_}. 17 | 18 | make_boss_app_info(AppName, BaseURL, StaticPrefix, DocPrefix, 19 | DomainList, ModelList, ViewList, ControllerList, 20 | RouterSupPid, TranslatorSupPid) -> 21 | InitData = boss_web_controller:run_init_scripts(AppName), 22 | #boss_app_info{ 23 | application = AppName, 24 | init_data = InitData, 25 | router_sup_pid = RouterSupPid, 26 | translator_sup_pid = TranslatorSupPid, 27 | base_url = if BaseURL =:= "/" -> ""; true -> BaseURL end, 28 | static_prefix = StaticPrefix, 29 | doc_prefix = DocPrefix, 30 | domains = DomainList, 31 | model_modules = ModelList, 32 | view_modules = ViewList, 33 | controller_modules = ControllerList 34 | }. 35 | 36 | unpack_application_env( AppName) -> 37 | BaseURL = boss_env:get_env(AppName, base_url, "/"), 38 | StaticPrefix = boss_env:get_env(AppName, static_prefix, "/static"), 39 | DocPrefix = boss_env:get_env(AppName, doc_prefix, "/doc"), 40 | DomainList = boss_env:get_env(AppName, domains, all), 41 | ModelList = boss_files:model_list(AppName), 42 | ViewList = boss_files:view_module_list(AppName), 43 | IsMasterNode = boss_env:is_master_node(), 44 | ControllerList = boss_files:web_controller_list(AppName), 45 | {ok, RouterSupPid} = boss_router:start([{application, AppName}, 46 | {controllers, ControllerList}]), 47 | {ok, TranslatorSupPid} = boss_translator:start([{application, AppName}]), 48 | {TranslatorSupPid, BaseURL, IsMasterNode, StaticPrefix, DocPrefix, 49 | DomainList, ModelList, ViewList, ControllerList, RouterSupPid}. 50 | 51 | find_application_for_path(Host, Path, Applications) -> 52 | UseHost = case Host of 53 | undefined -> undefined; 54 | _ -> hd(re:split(Host, ":", [{return, list}])) 55 | end, 56 | find_application_for_path(UseHost, Path, undefined, Applications, -1). 57 | 58 | find_application_for_path(_Host, _Path, Default, [], _MatchScore) -> 59 | Default; 60 | find_application_for_path(Host, Path, Default, [App|Rest], MatchScore) -> 61 | DomainScore = case Host of 62 | undefined -> 0; 63 | _ -> 64 | case boss_web:domains(App) of 65 | all -> 0; 66 | Domains -> 67 | case lists:member(Host, Domains) of 68 | true -> 1; 69 | false -> -1 70 | end 71 | end 72 | end, 73 | BaseURL = boss_web:base_url(App), 74 | PathScore = length(BaseURL), 75 | {UseApp, UseScore} = case (DomainScore >= 0) andalso (1000 * DomainScore + PathScore > MatchScore) andalso lists:prefix(BaseURL, Path) of 76 | true -> {App, DomainScore * 1000 + PathScore}; 77 | false -> {Default, MatchScore} 78 | end, 79 | find_application_for_path(Host, Path, UseApp, Rest, UseScore). 80 | 81 | 82 | 83 | execution_mode(App) -> 84 | case boss_env:is_developing_app(App) of 85 | true -> development; 86 | false -> production 87 | end. 88 | -------------------------------------------------------------------------------- /src/boss/session_adapters/boss_session_adapter_mnesia.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_adapter_mnesia). 2 | -behaviour(boss_session_adapter). 3 | -export([start/0, start/1, stop/1, init/1]). 4 | -export([session_exists/2, create_session/3, lookup_session/2]). 5 | -export([lookup_session_value/3, set_session_value/4, delete_session/2, delete_session_value/3]). 6 | 7 | -define(TABLE, boss_session). 8 | -define(TIMEOUT, 1000). 9 | 10 | -record(boss_session, {sid,data,ttl}). 11 | 12 | start() -> 13 | start([]). 14 | 15 | start(_Options) -> 16 | {ok, undefined}. 17 | 18 | stop(_) -> 19 | ok. 20 | 21 | init(_) -> 22 | error_logger:info_msg("Starting distributed session mnesia storage~n"), 23 | ok = ensure_schema(), % Expects Mnesia to be stopped 24 | mnesia:start(), 25 | %%Checks for table, after some time tries to recreate it 26 | case mnesia:wait_for_tables([?TABLE], ?TIMEOUT) of 27 | ok -> 28 | error_logger:info_msg("mnesia session table ok~n"), 29 | noop; 30 | {timeout,[?TABLE]} -> 31 | create_session_storage() 32 | end, 33 | 34 | {ok, undefined}. 35 | 36 | session_exists(_, SessionID) -> 37 | {atomic, Result} = mnesia:transaction(fun() -> mnesia:read({?TABLE, SessionID}) end), 38 | Result =/= []. 39 | 40 | create_session(_, SessionID, Data) -> 41 | Session = #boss_session{sid=SessionID,data=Data,ttl=0}, 42 | {atomic, ok} = mnesia:transaction(fun() -> mnesia:write(Session) end), 43 | ok. 44 | 45 | lookup_session(_, SessionID) -> 46 | recover_data(SessionID). 47 | 48 | lookup_session_value(_, SessionID, Key) -> 49 | Data = recover_data(SessionID), 50 | proplists:get_value(Key, Data). 51 | 52 | set_session_value(_, Sid, Key, Value) -> 53 | Data = recover_data(Sid), 54 | Data1 = case proplists:is_defined(Key,Data) of 55 | true -> 56 | Rest = proplists:delete(Key,Data), 57 | [{Key,Value}|Rest]; 58 | false -> 59 | [{Key,Value}|Data] 60 | end, 61 | 62 | Update = fun() -> NewSession = #boss_session{sid=Sid,data=Data1,ttl=0}, mnesia:write(NewSession) end, 63 | {atomic,ok} = mnesia:transaction(Update), 64 | ok. 65 | 66 | delete_session(_, Sid) -> 67 | {atomic, ok} = mnesia:transaction(fun() -> mnesia:delete({?TABLE, Sid}) end), 68 | ok. 69 | 70 | delete_session_value(_, Sid, Key) -> 71 | Data = recover_data(Sid), 72 | case proplists:is_defined(Key, Data) of 73 | true -> 74 | Data1 = proplists:delete(Key, Data), 75 | Update = fun() -> NewSession = #boss_session{sid=Sid,data=Data1,ttl=0}, mnesia:write(NewSession) end, 76 | {atomic,ok} = mnesia:transaction(Update), 77 | ok; 78 | false -> 79 | ok 80 | end. 81 | 82 | %%-------------------------------------------------------------------- 83 | %%% Internal functions 84 | %%-------------------------------------------------------------------- 85 | ensure_schema() -> 86 | Nodes = mnesia_nodes(), 87 | case mnesia:create_schema(Nodes) of 88 | ok -> ok; 89 | {error, {_, {already_exists, _}}} -> ok; 90 | Error -> Error 91 | end. 92 | 93 | create_session_storage()-> 94 | Nodes = mnesia_nodes(), 95 | error_logger:info_msg("Creating mnesia table for nodes ~p~n", [Nodes]), 96 | case mnesia:create_table(?TABLE,[{disc_copies, Nodes}, {attributes, record_info(fields, boss_session)}]) of 97 | {aborted, Reason} -> error_logger:error_msg("Error creating mnesia table for sessions: ~p~n", [Reason]); 98 | {atomic, ok} -> ok 99 | end. 100 | 101 | recover_data(Sid)-> 102 | Recover = fun() -> mnesia:read({?TABLE, Sid}) end, 103 | case mnesia:transaction(Recover) of 104 | {atomic, [S]} -> S#boss_session.data; 105 | {atomic, []} -> [] 106 | end. 107 | 108 | mnesia_nodes()-> 109 | case application:get_env(session_mnesia_nodes) of 110 | {ok, Val} -> Val; 111 | _ -> [node()] 112 | end. 113 | -------------------------------------------------------------------------------- /doc-src/api-view.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

Chicago Boss liked Django's templating language so much, he decided to steal it. Template files go in your project's src/view/ folder (in subdirectories that correspond to the controller name), and will have access to the variables you pass from your Controller. The template file associated with the function foo_controller:bar will be src/view/foo/bar.html. (Template filenames can also use .txt, .dtl and .js file extensions.)

4 | 5 |

Note: if you use the the extends tag, the file path should be relative to your project's src/view/ directory.

6 | 7 |

Chicago Boss has support for the most common features of the Django Template Language, so many existing Django templates should work out of the box. Your templates will be compiled down to Erlang BEAM code using ErlyDTL to give you the fastest Erlang templates this side of Stockholm.

8 | 9 |

100% of Django filters are supported. The only unsupported tag is csrf_token.

10 | 11 |

Custom tags

12 |

If you use repeated logic or template snippets, you can put helper templates into your project's src/view/lib/tag_html directory. The variables appearing in helper templates must be supplied by the caller. For example, if you have a file called "src/view/lib/tag_html/my_custom_tag.html" which uses a variable called "foo", you can invoke it from other templates like:

13 |
14 | {{ "{% " }}my_custom_tag foo="bar"{{ " %}" }} 15 |
16 |

String literals or variables may be passed as arguments to custom tags.

17 |

Another option for factoring out repeated logic is to use ErlyDTL's "include" tag. The difference between using an "include" tag and custom tags is that "include" will have access to all variables currently in the caller's scope, whereas custom tags only have access to the variables explicitly passed in. (In addition, custom tags are compiled only once for the entire project, but included files are compiled for each template that includes it.)

18 |

Helper modules for custom tags and filters

19 |

For more complicated processing, you can create helper modules. Tag helper modules reside in src/view/lib/tag_modules, and export functions which take in a proplist of variables and return rendered HTML. Filter helper modules reside in src/view/lib/filter_modules, and export functions which take in a binary string and return a filter iolist. See those directories in your project for examples.

20 |

Implicit variables

21 |

The following variables are automatically passed to your views without you doing any work:

22 |
    23 |
  • _base_url - The value of the config setting "base_url" or blank if not set
  • 24 |
  • _before - The result of the authorization filter, if present
  • 25 |
  • _lang - The content language of the rendered view
  • 26 |
  • _session - Key/value pairs from the current session, if it exists
  • 27 |
28 | 29 |

Experimental Template Languages

30 |

Two other template languages are supported on an experimental basis: Jade (provided by jaderl) and embedded Elixir.

31 |

Jade

32 |

To use Jade, simply create templates with extension ".jade".

33 |

Embedded Elixir

34 |

To use embedded Elixir, create templates with extension ".eex", and prefix your variables with @.

35 |

Example Elixir:

36 |
37 | A message for you: <%= @message %>
38 |
39 | <%# This is a comment %>
40 |
41 | <%= Enum.map @puppies, fn(puppy) -> %>
42 | Name: <%= puppy.name %>
43 | <% end %> 44 |
45 |

For more information on embedded Elixir, see the EEx docs.

46 | {% endblock %} 47 | -------------------------------------------------------------------------------- /src/boss/boss_mq.erl: -------------------------------------------------------------------------------- 1 | %% @doc Chicago Boss messaging abstraction 2 | 3 | -module(boss_mq). 4 | 5 | -export([start/0, start/1, stop/0]). 6 | 7 | -ifdef(TEST). 8 | -compile(export_all). 9 | -endif. 10 | 11 | -export([ 12 | pull/1, 13 | pull/2, 14 | pull/3, 15 | poll/1, 16 | poll/2, 17 | push/2, 18 | now/1]). 19 | -type channel() ::string(). 20 | -type mq_return() :: {ok, integer(), [_]}|{error,string()}. 21 | 22 | -spec start() -> any(). 23 | start() -> 24 | MQOptions = make_queue_options(), 25 | MQAdapter = get_mq_adapater(), 26 | MQOptions1 = [{adapter, list_to_atom("boss_mq_adapter_"++atom_to_list(MQAdapter))}|MQOptions], 27 | start(MQOptions1). 28 | 29 | -spec start(_) -> any(). 30 | start(Options) -> 31 | boss_mq_sup:start_link(Options). 32 | 33 | -spec stop() -> 'ok'. 34 | stop() -> 35 | ok. 36 | 37 | -spec get_mq_adapater() -> atom(). 38 | get_mq_adapater() -> 39 | case application:get_env(mq_adapter) of 40 | {ok, Val} -> Val; 41 | _ -> tinymq 42 | end. 43 | 44 | make_queue_options() -> 45 | lists:foldl(fun(OptName, Acc) -> 46 | case application:get_env(OptName) of 47 | {ok, Val} -> [{OptName, Val}|Acc]; 48 | _ -> Acc 49 | end 50 | end, 51 | [], 52 | [mq_port, mq_host, mq_max_age]). 53 | 54 | 55 | 56 | -spec pull(channel()) -> mq_return(). 57 | %% @doc Pull messages from the specified `Channel'. If none are in the queue, blocks 58 | %% until a message is pushed to the queue. 59 | pull(Channel) -> 60 | pull(Channel, undefined). 61 | 62 | -spec pull(channel(),undefined|non_neg_integer()) -> mq_return(). 63 | %% @doc Pull messages from the specified `Channel' after `Since' (a timestamp returned from a previous `pull'). 64 | %% If no such messages are in the queue, blocks until a message is pushed to the queue. 65 | pull(Channel, Timestamp) -> 66 | pull(Channel, Timestamp, infinity). 67 | 68 | -spec pull(channel(),undefined|integer()|{integer(), integer(), integer()},infinity|non_neg_integer()) -> mq_return(). 69 | %% @doc Pull messages from the specified `Channel' after `Since' (a timestamp returned from a previous `pull'). If no such messages 70 | %% are in the queue, blocks until a message is pushed to the queue, or until `Timeout' seconds have elapsed. 71 | pull(Channel, {MegaSecs, Secs, MicroSecs}, Timeout) -> 72 | pull(Channel, 1000 * 1000 * (1000 * 1000 * MegaSecs + Secs) + MicroSecs, Timeout); 73 | pull(Channel, Timestamp, Timeout) when is_list(Channel) -> 74 | TimeoutMs = convert_to_ms(Timeout), 75 | CallStatus = gen_server:call({global, ?MODULE}, {pull, Channel, Timestamp, self()}), 76 | pull_recieve(TimeoutMs, CallStatus). 77 | 78 | 79 | -spec pull_recieve('infinity' | non_neg_integer(),{ok, _}|any()) -> mq_return(). 80 | pull_recieve(TimeoutMs, {ok, PullTime}) -> 81 | receive 82 | {_From, NewTimestamp, Messages} -> 83 | {ok, NewTimestamp, Messages} 84 | after 85 | TimeoutMs -> 86 | {ok, PullTime, []} 87 | end; 88 | pull_recieve(_, Error) -> Error. 89 | 90 | 91 | -spec convert_to_ms('infinity' | non_neg_integer()) -> 'infinity' | non_neg_integer(). 92 | convert_to_ms(infinity) -> infinity; 93 | convert_to_ms(Timeout) -> 94 | Timeout * 1000. 95 | 96 | 97 | -spec poll(channel()) -> mq_return(). 98 | %% @doc Like `pull/1', but returns immediately if no messages are in the queue. 99 | poll(Channel) -> 100 | poll(Channel, last). 101 | 102 | -spec poll(channel(),last|integer()) -> mq_return(). 103 | %% @doc Like `pull/2', but returns immediately if no matching messages are in the queue. 104 | poll(Channel, Timestamp) when is_list(Channel) -> 105 | gen_server:call({global, ?MODULE}, {poll, Channel, Timestamp}). 106 | 107 | 108 | -spec push(channel(),_) -> {ok, non_neg_integer()}. 109 | %% @doc Pushes a message to the specified `Channel'. 110 | push(Channel, Message) when is_list(Channel) -> 111 | gen_server:call({global, ?MODULE}, {push, Channel, Message}). 112 | 113 | -spec now(channel()) -> non_neg_integer(). 114 | %% @doc Retrieves the current time for the server managing `Channel'. 115 | now(Channel) when is_list(Channel) -> 116 | gen_server:call({global, ?MODULE}, {now, Channel}). 117 | 118 | -------------------------------------------------------------------------------- /include/yaws_api.hrl: -------------------------------------------------------------------------------- 1 | %%%---------------------------------------------------------------------- 2 | %%% File : yaws_api.hrl 3 | %%% Author : Claes Wikstrom 4 | %%% Purpose : 5 | %%% Created : 24 Jan 2002 by Claes Wikstrom 6 | %%%---------------------------------------------------------------------- 7 | 8 | -author('klacke@hyber.org'). 9 | 10 | -record(arg, { 11 | clisock, %% the socket leading to the peer client 12 | client_ip_port, %% {ClientIp, ClientPort} tuple 13 | headers, %% headers 14 | req, %% request 15 | clidata, %% The client data (as a binary in POST requests) 16 | server_path, %% The normalized server path 17 | %% (pre-querystring part of URI) 18 | querydata, %% For URIs of the form ...?querydata 19 | %% equiv of cgi QUERY_STRING 20 | appmoddata, %% (deprecated - use pathinfo instead) the remainder 21 | %% of the path leading up to the query 22 | docroot, %% Physical base location of data for this request 23 | docroot_mount, %% virtual directory e.g /myapp/ that the docroot 24 | %% refers to. 25 | fullpath, %% full deep path to yaws file 26 | cont, %% Continuation for chunked multipart uploads 27 | state, %% State for use by users of the out/1 callback 28 | pid, %% pid of the yaws worker process 29 | opaque, %% useful to pass static data 30 | appmod_prepath, %% (deprecated - use prepath instead) path in front 31 | %%of: 32 | prepath, %% Path prior to 'dynamic' segment of URI. 33 | %% ie http://some.host///d/e 34 | %% where is an appmod mount point, 35 | %% or .yaws,.php,.cgi,.fcgi etc script file. 36 | pathinfo %% Set to '/d/e' when calling c.yaws for the request 37 | %% http://some.host/a/b/c.yaws/d/e 38 | %% equiv of cgi PATH_INFO 39 | }). 40 | 41 | 42 | -record(http_request, {method, 43 | path, 44 | version}). 45 | 46 | -record(http_response, {version, 47 | status, 48 | phrase}). 49 | 50 | -record(headers, { 51 | connection, 52 | accept, 53 | host, 54 | if_modified_since, 55 | if_match, 56 | if_none_match, 57 | if_range, 58 | if_unmodified_since, 59 | range, 60 | referer, 61 | user_agent, 62 | accept_ranges, 63 | cookie = [], 64 | keep_alive, 65 | location, 66 | content_length, 67 | content_type, 68 | content_encoding, 69 | authorization, 70 | transfer_encoding, 71 | other = [] %% misc other headers 72 | }). 73 | 74 | 75 | 76 | 77 | -record(url, 78 | {scheme, %% undefined means not set 79 | host, %% undefined means not set 80 | port, %% undefined means not set 81 | path = [], 82 | querypart = []}). 83 | 84 | 85 | -record(setcookie,{ 86 | key, 87 | value, 88 | quoted, 89 | comment, 90 | comment_url, 91 | discard, 92 | domain, 93 | max_age, 94 | expires, 95 | path, 96 | port, 97 | secure, 98 | version}). 99 | 100 | 101 | -record(redir_self, { 102 | host, %% string() - our own host 103 | scheme, %% http | https 104 | scheme_str, %% "https://" | "http://" 105 | port, %% integer() - our own port 106 | port_str %% "" | ":" - the optional port part 107 | %% to append to the url 108 | }). 109 | 110 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /doc-src/boss.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: Arial; 3 | text-align: left; 4 | background-color: #E1DFDF; 5 | margin: 0; 6 | } 7 | div#api-content { 8 | max-width: 1100px; 9 | margin: 0 auto; 10 | background-color: white; 11 | padding: 10pt; 12 | } 13 | header { 14 | background-color: #333333; 15 | color: white; 16 | } 17 | header a { 18 | transition: all 0.15s linear 0s; 19 | font-weight: 300; 20 | } 21 | 22 | h1, h2, h3 { 23 | color: black; 24 | line-height: 120%; 25 | margin-top: 1.3em; 26 | } 27 | 28 | h1 { 29 | font-size: 36pt; 30 | } 31 | 32 | h2 { 33 | font-size: 32pt; 34 | } 35 | 36 | h3 { 37 | font-size: 26pt; 38 | } 39 | 40 | div.equation { 41 | color: white; 42 | font-size: 26px; padding: 20px; 43 | } 44 | div.equation span { 45 | padding: 6px 10px; 46 | color: white; 47 | } 48 | span.chicago { 49 | font-style: italic; 50 | font-size: 72px; 51 | } 52 | span.boss { 53 | color: black; 54 | font-size: 98px; 55 | font-weight: bold; 56 | } 57 | span.bullet { 58 | font-size: 36px; 59 | } 60 | div.quote { 61 | font-style: italic; 62 | } 63 | pre.code { 64 | padding: 10px; 65 | } 66 | div.code { 67 | padding-top: 4px; 68 | } 69 | 70 | div.code, pre.code, td.code { 71 | font-family: "Courier New", monospace; 72 | color: #333; 73 | font-weight: bold; 74 | font-size: 14pt; 75 | } 76 | span.atom, { 77 | color: #D48528; 78 | } 79 | span.attr { 80 | color: #D48528; 81 | } 82 | span.braces { 83 | color: #D48528; 84 | font-weight: bold; 85 | } 86 | span.bracket { 87 | color: #D48528; 88 | } 89 | span.comment { 90 | color: #D48528; 91 | } 92 | 93 | span.function { 94 | color: #D48528; 95 | } 96 | 97 | span.keyword { 98 | color: #D48528; 99 | } 100 | span.name { 101 | color: #D48528; 102 | } 103 | span.string { 104 | color: #D48528; 105 | } 106 | span.typevar { 107 | color: #D48528; 108 | } 109 | span.var { 110 | color: #D48528; 111 | } 112 | 113 | div.nav { 114 | clear: both; 115 | color: white; 116 | font-size: 20px; 117 | margin-bottom: 20px; 118 | } 119 | div.nav a { 120 | color: #000; 121 | font-size: 20px; margin-top: 40px; margin-bottom: 20px; 122 | } 123 | div.subnav { 124 | text-align: center; 125 | font-size: 14px; 126 | padding: 16px; 127 | } 128 | 129 | a:link, a:visited { 130 | color: gray; 131 | } 132 | 133 | a:hover { 134 | color: #999; 135 | } 136 | 137 | a:active { 138 | color: #000; 139 | } 140 | 141 | div.subnav a:link, div.subnav a:visited { 142 | color: #D48528; 143 | } 144 | 145 | div.subnav a:hover { 146 | color: #ddd; 147 | } 148 | 149 | div.subnav a:active { 150 | color: #fff; 151 | } 152 | 153 | 154 | 155 | 156 | 157 | p.question { 158 | margin: 0px; 159 | font-style: italic; 160 | font-weight: bold; 161 | } 162 | div.main_header { 163 | width: 900px; margin-left: auto; margin-right: auto; 164 | } 165 | div.main_logo { 166 | text-align: left; 167 | padding: 40px 40px 40px 30px; 168 | color: white; 169 | margin-right: auto; 170 | width: 280px; 171 | float: left; 172 | } 173 | div.main_checklist { 174 | color: black; 175 | font-size: 30px; 176 | text-align: left; 177 | float: left; 178 | margin-top: 40px; 179 | line-height: 36px; 180 | } 181 | div.secondary_header { 182 | width: 790px; 183 | height: 50px; 184 | margin-left: auto; 185 | margin-right: auto; 186 | text-align: left; 187 | color: white; 188 | } 189 | div.secondary_logo { 190 | float: left; 191 | font-size: 24px; 192 | } 193 | div.secondary_logo strong { 194 | color: black; 195 | } 196 | div.secondary_quote { 197 | float: right; 198 | } 199 | div.content { 200 | text-align: left; 201 | background-color: white; 202 | position: absolute; 203 | margin-left: -425px; 204 | left: 50%; 205 | padding: 30px 30px; 206 | width: 790px; 207 | font-size: 20px; padding-bottom: 300px; 208 | } 209 | code { 210 | font-weight: bold; 211 | color: #333; 212 | } 213 | div.example { 214 | border-bottom: #ddd solid 1px; 215 | padding: 10pt; 216 | } 217 | div.row2 { 218 | background-color: rgb(240, 240, 240); 219 | } 220 | 221 | td.code { 222 | 223 | } 224 | .nobr { 225 | white-space: pre; 226 | } 227 | 228 | #link-home { 229 | margin-top: 0px; 230 | padding: 10px; 231 | } -------------------------------------------------------------------------------- /priv/elixir/boss/web_controller.ex: -------------------------------------------------------------------------------- 1 | defmodule Boss.WebController do 2 | 3 | defmacro __using__(_) do 4 | quote do 5 | Module.register_attribute __MODULE__, :__routes__, persist: false, accumulate: true 6 | @before_compile unquote(__MODULE__) 7 | import unquote(__MODULE__) 8 | end 9 | end 10 | 11 | defmacro get(action, tokens, block) do 12 | handle(:GET, action, tokens, block) 13 | end 14 | 15 | defmacro get(action, tokens, info, block) do 16 | handle(:GET, action, tokens, block, info) 17 | end 18 | 19 | defmacro post(action, tokens, block) do 20 | handle(:POST, action, tokens, block) 21 | end 22 | 23 | defmacro post(action, tokens, info, block) do 24 | handle(:POST, action, tokens, block, info) 25 | end 26 | 27 | defmacro put(action, tokens, block) do 28 | handle(:PUT, action, tokens, block) 29 | end 30 | 31 | defmacro put(action, tokens, info, block) do 32 | handle(:PUT, action, tokens, block, info) 33 | end 34 | 35 | defmacro delete(action, tokens, block) do 36 | handle(:DELETE, action, tokens, block) 37 | end 38 | 39 | defmacro delete(action, tokens, info, block) do 40 | handle(:DELETE, action, tokens, block, info) 41 | end 42 | 43 | defmacro before_(action, block) do 44 | quote do 45 | def before_(var!(req), var!(session_id), unquote(action)), unquote(block) 46 | end 47 | end 48 | 49 | defmacro before_(action, method, tokens, block) do 50 | quote do 51 | def before_(var!(req), var!(session_id), unquote(action), unquote(method), unquote(tokens)), unquote(block) 52 | end 53 | end 54 | 55 | defmacro cache_(action, tokens, block) do 56 | quote do 57 | def cache_(var!(req), var!(session_id), unquote(action), unquote(tokens)), unquote(block) 58 | end 59 | end 60 | 61 | defmacro cache_(action, tokens, info, block) do 62 | quote do 63 | def cache_(var!(req), var!(session_id), unquote(action), unquote(tokens), unquote(info)), unquote(block) 64 | end 65 | end 66 | 67 | defmacro lang_(action, block) do 68 | quote do 69 | def lang_(var!(req), var!(session_id), unquote(action)), unquote(block) 70 | end 71 | end 72 | 73 | defmacro lang_(action, info, block) do 74 | quote do 75 | def lang_(var!(req), var!(session_id), unquote(action), unquote(info)), unquote(block) 76 | end 77 | end 78 | 79 | defmacro after_(action, result, block) do 80 | quote do 81 | def after_(var!(req), var!(session_id), unquote(action), unquote(result)), unquote(block) 82 | end 83 | end 84 | 85 | defmacro after_(action, result, info, block) do 86 | quote do 87 | def after_(var!(req), var!(session_id), unquote(action), unquote(result), unquote(info)), unquote(block) 88 | end 89 | end 90 | 91 | defmacro req() do 92 | quote do 93 | :erlang.get("BOSS_INTERNAL_REQUEST_OBJECT") 94 | end 95 | end 96 | 97 | defmacro session_id() do 98 | quote do 99 | :erlang.get("BOSS_INTERNAL_SESSION_ID") 100 | end 101 | end 102 | 103 | defp handle(method, action, tokens, block) do 104 | action = to_action(action) 105 | route = {action, to_route_tokens(tokens)} 106 | quote do 107 | @__routes__ unquote(route) 108 | def unquote(action)(var!(req), var!(session_id), unquote(method), unquote(tokens)), unquote(block) 109 | end 110 | end 111 | 112 | defp handle(method, action, tokens, block, info) do 113 | action = to_action(action) 114 | route = {action, to_route_tokens(tokens)} 115 | quote do 116 | @__routes__ unquote(route) 117 | def unquote(action)(var!(req), var!(session_id), unquote(method), unquote(tokens), unquote(info)), unquote(block) 118 | end 119 | end 120 | 121 | defp to_action({atom, _, _}), do: atom 122 | defp to_action(action), do: action 123 | 124 | defp to_route_tokens({_, _, nil}), do: [] 125 | defp to_route_tokens(tokens) do 126 | lc token inlist tokens, do: to_route_token(token) 127 | end 128 | 129 | defp to_route_token(token) do 130 | case token do 131 | {:|, _, [token, _]} -> to_route_token(token) 132 | {name, _, _} -> name 133 | value -> value 134 | end 135 | end 136 | 137 | defmacro __before_compile__(_) do 138 | quote do 139 | def _routes(_), do: @__routes__ 140 | # simulate a new && instance functions 141 | def new(req), do: {__MODULE__, req} 142 | def instance(req), do: {__MODULE__, req} 143 | end 144 | end 145 | 146 | end 147 | -------------------------------------------------------------------------------- /doc-src/api-test.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

Jump to: boss_web_test 4 |   boss_assert

5 |

Chicago Boss ships with a unique functional test framework, where tests are structured as trees of continuations. All assertions are performed in callbacks, and additional tests are also performed in callbacks. For more information on the design of Boss's functional tests, see “Functional Tests As A Tree Of Continuations”.

6 | 7 |

To create a test suite, create a module in the src/test/functional/ directory. Your test module should export a start/0 function, which should invoke a function from the boss_web_test module. This function will in turn invoke functions in the boss_assert module (to run assertions) and the boss_web_test module (to run further tests). A simple example of a test module with included start/0 function would be:

8 | 9 |
10 | -module(my_app_test).
11 | 
12 | -export([start/0]).
13 | 
14 | start() ->
15 |     boss_web_test:get_request("/", [],
16 |         [fun boss_assert:http_ok/1], []).
17 | 
18 | 19 |

The above test issues a GET request to the root URL, asserts that the response is 200 OK, and quits.

20 | 21 |

When you are ready to run your test suite, type "./rebar boss c=test_functional" in your project directory.

22 | 23 | 24 |

boss_web_test

25 |

Functions in the boss_web_test issue HTTP requests to your web application (or check for emails sent by it). All functions in the boss_web_test module take the same two final arguments.

26 | 27 |

Second to last, Assertions is a list of funs that must return a tuple {Passed, ErrorMessage}, where: 28 |

    29 |
  • Passed - a boolean indicating whether the test passed
  • 30 |
  • ErrorMessage - an error message to be displayed if the test failed
  • 31 |
32 |

Each fun in Assertions takes a single argument, which is the response object of the current test. The response object will usually by an HTTP response, but in boss_web_test:read_email/4, it's an email.

33 | 34 |

Most assertions will take the form of calls to boss_assert, which is documented below.

35 | 36 |

The last argument to any boss_web_test function is called Continuations. Continuations is a list of additional tests — funs that take the HTTP response value as their only argument, and use it to invoke additional tests from the boss_web_test module. Funs in Continuations should be preceded by a string label, e.g.:

37 | 38 |
39 | boss_web_test:get_request("/", [], [],
40 |    [ "Click the register link", fun(Res) -> ... end,
41 |      "Click the copyright link", fun(Res) -> ... end, 
42 |       ... ]).
43 | 
44 |

The key thing to understand about Continuations is that they are performed in parallel. Any database manipulations that occur in one continuation cannot affect sibling continuations.

45 | 46 |

Functions available in the boss_web_test module include:

47 | 48 | {% for function in test_functions %} 49 | {% if function.description_long %} 50 |
51 |
52 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 53 |
54 |

{{ function.description_long }}

55 |
56 | {% endif %} 57 | {% endfor %} 58 | 59 | 60 |

boss_assert

61 | 62 |

The Assertions list in a boss_web_test invocation will usually refer to functions in the boss_assert module. Available functions include:

63 | 64 | {% for function in assert_functions %} 65 | {% if function.description_long %} 66 |
67 |
68 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 69 |
70 |

{{ function.description_long }}

71 |
72 | {% endif %} 73 | {% endfor %} 74 | 75 | {% endblock %} 76 | -------------------------------------------------------------------------------- /doc-src/api-websocket.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

WebSockets are an HTML5 technology used for two-way messaging from inside a web page. As of version 0.8.0, Chicago Boss provides infrastructure for defining one or more WebSocket controllers. Note that WebSockets are only supported with Cowboy, so put {server, cowboy} in your boss.config before attempting anything on this page.

4 |

Server code

5 |

The server WebSocket API is based around Erlang message-passing; to send a message to a particular client, simply send it a message like:

6 |
7 | WebSocket ! {text, <<"some text">>} 8 |
9 |

To handle incoming messages from clients, you'll need to create WebSocket controllers in your project's src/websocket directory. Each controller module should have a name of the form <app name>_<service name>_websocket.erl and be a parameterized module with parameters for Req and SessionId, e.g.:

10 | 11 |
12 | -module(myapp_myservice_websocket, [Req, SessionId]). 13 |
14 | 15 |

The module should implement the boss_service_handler behavior. The boss_service_handler behavior consists of the following six functions:

16 |
17 | init() -> {ok, InitialState} 18 |
19 |

Initialize the service.

20 | 21 |
22 | handle_join(ServiceURL::string(), WebSocket::pid(), State) ->
23 |   {noreply, NewState} |
24 |   {noreply, NewState, Timeout} |
25 |   {stop, Reason, NewState 26 |
27 |

Handle a client joining a service.

28 | 29 |
30 | handle_close(Reason::terminate_reason(), ServiceURL::string(), WebSocket::pid(), State) ->
31 |   {noreply, NewState} |
32 |   {noreply, NewState, Timeout} |
33 |   {stop, Reason, NewState} 34 |

35 |   -type terminate_reason() :: {normal, shutdown}
36 |   | {normal, timeout}
37 |   | {error, closed}
38 |   | {remote, closed}
39 |   | {remote, cowboy_websocket:close_code(), binary()}
40 |   | {error, badencoding}
41 |   | {error, badframe}
42 |   | {error, atom()}.
43 |
44 | 45 | 46 |

Handle a client leaving a service.

47 | 48 |
49 | handle_incoming(ServiceURL::string(), WebSocket::pid(), Message, State) ->
50 |   {noreply, NewState} |
51 |   {noreply, NewState, Timeout} |
52 |   {stop, Reason, NewState} 53 |
54 |

Handle an incoming message from a client.

55 | 56 |
57 | handle_broadcast(Message::term(), State) ->
58 |   {noreply, NewState} |
59 |   {noreply, NewState, Timeout} |
60 |   {stop, Reason, NewState} 61 |
62 |

Handle an outgoing broadcast message from some Erlang process to 63 | the connected web sockets. It is your responsibility to keep track of 64 | the web sockets (via handle_join and handle_close) and send the 65 | outgoing messages as you see fit.

66 | 67 |
68 | handle_info(Info, State) ->
69 |   {noreply, NewState} |
70 |   {noreply, NewState, Timeout} |
71 |   {stop, Reason, NewState} 72 |
73 | 74 |

Handle an informational message sent to the underlying gen_server process.

75 | 76 |
77 | terminate(Reason, State) -> ok 78 |
79 |

Perform any cleanup before shutting down the service.

80 | 81 |

Client code

82 | 83 |

WebSocket URLs are automatically generated from the WebSocket controllers in your project's src/websocket directory. For example, if you have myapp_foobar_websocket.erl, then to create a new WebSocket on the client:

84 |
85 | <script type="text/javascript">
86 |   wsc = new WebSocket("ws://localhost:8001/websocket/foobar", "some_service_name");
87 | </script> 88 |
89 |

The WebSocket URLs respect the base_url config parameter. For example, if the base_url is set to "/chat", then you would use this code instead:

90 |
91 | <script type="text/javascript">
92 |   wsc = new WebSocket("ws://localhost:8001/chat/websocket/foobar", "some_service_name");
93 | </script> 94 |
95 | 96 |

For a reference on the WebSocket client programming, see Mozilla's WebSockets page.

97 | 98 | {% endblock %} 99 | -------------------------------------------------------------------------------- /doc-src/api-news.html: -------------------------------------------------------------------------------- 1 | {% extends "api.html" %} 2 | {% block api_content %} 3 |

BossNews: Events for your data model

4 |

The BossNews API lets you cleanly separate notification logic from your models and controllers. You can specify model attributes and collections to watch, and have a callback function invoked when that part of the model changes. BossNews might be used to send email updates about changes occuring on a website, or it might be used in conjunction with BossMQ to provide users with real-time events (for example, to fire an alert when someone replies to a user's forum post).

5 |

The boss_news module

6 |

The boss_news module provides functions for creating and managing event listeners, called watches. Watches take a TopicString, which is a comma-separated list of topics to watch, and then execute the provided function when a topic changes. The following table summarizes valid topics and the events that the callback function will receive:

7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 |
TopicEventEventInfo
RecordId.Attributeupdated{Record, Attribute::atom(), OldValue, NewValue}
RecordId.*updated{Record, Attribute::atom(), OldValue, NewValue}
RecordIddeletedDeletedRecord
Type-*.Attributeupdated{Record, Attribute::atom(), OldValue, NewValue}
Type-*.*updated{Record, Attribute::atom(), OldValue, NewValue}
CollectioncreatedNewRecord
CollectiondeletedDeletedRecord
18 | 19 |

Examples of valid topics are:

20 |
    21 |
  • "user-42.status"
  • 22 |
  • "user-*.status"
  • 23 |
  • "user-42.*
  • 24 |
  • "user-*.*
  • 25 |
  • "users"
  • 26 |
27 |

The return value of the callback function can be used to cancel (or extend) the watch that invoked the function. This feature can be used to ensure that a callback is only performed once. Meaningful return values are:

28 |
    29 |
  • {ok, cancel_watch} - Cancel the invoking watch.
  • 30 |
  • {ok, extend_watch} - Extend the invoking watch.
  • 31 |
32 |

(Other return values are ignored.)

33 |

Functions for managing watches are:

34 | 35 | {% for function in functions %} 36 | {% if function.description_long %} 37 |
38 |
39 | {{ function.function }}{% if function.typespec %}{{ function.typespec }}{% endif %} 40 |
41 |

{{ function.description_long }}

42 |
43 | {% endif %} 44 | {% endfor %} 45 | 46 | 47 |

The *_news.erl startup script

48 |

Listeners should be set up in the priv/init/<appname>_news.erl file in your project directory. The module should have one exported function, init/0, which is executed at server startup. This function is a convenient place to set up listeners before any web requests are received, but listeners can be set up anywhere in the application.

49 | 50 |

The script's init/0 function must return {ok, ListOfWatchIDs}. If the script is reloaded during the lifetime of the application (for example, via the admin application), then this list of watches will be cancelled before the script is executed again.

51 | 52 |

HTTP API for supporting other frameworks

53 |

If the admin application is installed, other applications can post data update notifications to a Chicago Boss server via a simple HTTP API:

54 | 55 |
56 |     POST /admin/news_api/created/<record-id>
57 |     POST /admin/news_api/updated/<record-id>
58 |     POST /admin/news_api/deleted/<record-id>
59 | 
60 | 61 |

The body of the POST request should include model attributes in the form new[<attr>]=<val> and old[<attr>]=<val>. The created API takes only "new" attributes. The deleted API takes only "old" attributes. The updated API takes both "new" attributes and their corresponding "old" attributes.

62 |

When invoked via the HTTP API, all values are passed to the listeners as strings.

63 | {% endblock %} 64 | -------------------------------------------------------------------------------- /src/boss/boss_session_mock_controller.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_mock_controller). 2 | -behaviour(gen_server). 3 | 4 | -export([start_link/0, start_link/1]). 5 | -export([init/1, handle_call/3, handle_cast/2, handle_info/2, terminate/2, code_change/3]). 6 | 7 | -record(boss_session, {sid, data}). 8 | 9 | -record(state, { 10 | table, 11 | session_dict = dict:new(), 12 | ttl_tree = gb_trees:empty(), 13 | exp_time 14 | }). 15 | 16 | start_link() -> 17 | start_link([]). 18 | 19 | start_link(Args) -> 20 | gen_server:start_link({global, boss_session_mock}, ?MODULE, Args, []). 21 | 22 | init(Options) -> 23 | TableId = ets:new(?MODULE,[set,named_table,{keypos, 2}]), 24 | {ok, #state{ table = TableId, exp_time = proplists:get_value(session_exp_time, Options, 1440) }}. 25 | 26 | handle_call({session_exists, SessionID}, _From, State) -> 27 | Exists = dict:is_key(SessionID, State#state.session_dict), 28 | NewState = case Exists of 29 | true -> 30 | NowSeconds = now_seconds(), 31 | Val = dict:fetch(SessionID, State#state.session_dict), 32 | State#state{ 33 | ttl_tree = tiny_pq:move_value(Val, NowSeconds + State#state.exp_time, SessionID, State#state.ttl_tree), 34 | session_dict = dict:store(SessionID, 35 | NowSeconds + State#state.exp_time, 36 | State#state.session_dict)}; 37 | false -> 38 | State 39 | end, 40 | {reply, Exists, prune_expired_sessions(NewState, now_seconds())}; 41 | handle_call({create_session, SessionID, Data}, _From, State) -> 42 | NowSeconds = now_seconds(), 43 | ets:insert(State#state.table, #boss_session{sid=SessionID, data=Data}), 44 | NewState = State#state{ 45 | ttl_tree = tiny_pq:insert_value(NowSeconds + State#state.exp_time, SessionID, State#state.ttl_tree), 46 | session_dict = dict:store(SessionID, NowSeconds + State#state.exp_time, State#state.session_dict) 47 | }, 48 | {reply, ok, NewState}; 49 | handle_call({lookup_session, SessionID}, _From, State) -> 50 | Data = case ets:lookup(State#state.table, SessionID) of 51 | [S] -> S#boss_session.data; 52 | [] -> [] 53 | end, 54 | {reply, Data, State}; 55 | handle_call({lookup_session_value, SessionID, Key}, _From, State) -> 56 | Data = case ets:lookup(State#state.table, SessionID) of 57 | [S] -> S#boss_session.data; 58 | [] -> [] 59 | end, 60 | {reply, proplists:get_value(Key, Data), State}; 61 | handle_call({set_session_value, SessionID, Key, Value}, _From, State) -> 62 | Data = case ets:lookup(State#state.table, SessionID) of 63 | [S] -> S#boss_session.data; 64 | [] -> [] 65 | end, 66 | Data1 = case proplists:is_defined(Key,Data) of 67 | true -> 68 | Rest = proplists:delete(Key,Data), 69 | [{Key,Value}|Rest]; 70 | false -> 71 | [{Key,Value}|Data] 72 | end, 73 | ets:insert(State#state.table, #boss_session{sid=SessionID,data=Data1}), 74 | {reply, ok, State}; 75 | handle_call({delete_session, SessionID}, _From, State) -> 76 | ets:delete(State#state.table, SessionID), 77 | NewState = case dict:find(SessionID, State#state.session_dict) of 78 | {ok, Val} -> 79 | State#state{ 80 | ttl_tree = tiny_pq:delete_value(Val, SessionID, State#state.ttl_tree), 81 | session_dict = dict:erase(SessionID, State#state.session_dict) 82 | }; 83 | _ -> 84 | State 85 | end, 86 | {reply, ok, NewState}; 87 | handle_call({delete_session_value, SessionID, Key}, _From, State) -> 88 | case ets:lookup(?MODULE,SessionID) of 89 | [S] -> 90 | Data = S#boss_session.data, 91 | case proplists:is_defined(Key,Data) of 92 | true -> 93 | Data1 = proplists:delete(Key,Data), 94 | ets:insert(State#state.table, #boss_session{ sid=SessionID, data=Data1 }); 95 | false -> 96 | ok 97 | end; 98 | [] -> 99 | ok 100 | end, 101 | {reply, ok, State}. 102 | 103 | handle_cast(_, State) -> 104 | {noreply, State}. 105 | 106 | terminate(_Reason, _State) -> 107 | ok. 108 | 109 | code_change(_OldVsn, State, _Extra) -> 110 | {ok, State}. 111 | 112 | handle_info(_Info, State) -> 113 | {noreply, State}. 114 | 115 | prune_expired_sessions(#state{ ttl_tree = Tree, session_dict = Dict, table = TableId } = State, NowSeconds) -> 116 | {NewDict, NewTree} = tiny_pq:prune_collect_old(fun(SessionID, DictAcc) -> 117 | ets:delete(TableId, SessionID), 118 | dict:erase(SessionID, DictAcc) 119 | end, Dict, Tree, NowSeconds), 120 | State#state{ ttl_tree = NewTree, session_dict = NewDict }. 121 | 122 | now_seconds() -> 123 | {A, B, _} = erlang:now(), 124 | A * 1000 * 1000 + B. 125 | 126 | -------------------------------------------------------------------------------- /README_ELIXIR.md: -------------------------------------------------------------------------------- 1 | Using Elixir with Chicago Boss 2 | == 3 | 4 | Elixir is a nifty new language for the Erlang VM which you can use in parts of 5 | your project instead of regular Erlang if you prefer. 6 | 7 | Supported features: 8 | 9 | * Elixir web controllers 10 | * Ecto models 11 | * Embedded Elixir views 12 | * Elixir library files in src/lib/ (but no inter-file macros!) 13 | 14 | Requirements: 15 | 16 | * Erlang R16B or later 17 | * A love of danger 18 | 19 | Learn more about Elixir at http://www.elixir-lang.org/ 20 | 21 | Setup 22 | -- 23 | 24 | To enable Elixir, you need to do the following: 25 | 26 | 1. Uncomment {elixir, ...} in the "deps" section of rebar.config in the 27 | ChicagoBoss directory 28 | 29 | 2. Copy priv/elixir to the src/ directory in ChicagoBoss 30 | 31 | 3. Download Elixir and friends: 32 | 33 | ./rebar get-deps 34 | 35 | 4. First pass compile: 36 | 37 | ./rebar compile 38 | 39 | 5. Download Elixir dependencies: 40 | 41 | PATH=./deps/elixir/bin:.:$PATH mix deps.get 42 | 43 | 6. Workaround for poolboy error: 44 | 45 | cp rebar deps/poolboy 46 | 47 | 7. Retry Elixir dependencies: 48 | 49 | PATH=./deps/elixir/bin:.:$PATH mix deps.get 50 | 51 | 8. Make a new project: 52 | 53 | make app PROJECT=my_application 54 | 55 | 56 | Controller API 57 | -- 58 | 59 | Put your Elixir controllers into src/controller/ as before. The source files 60 | should NOT be prefixed with the application name. Example: 61 | 62 | src/controller/puppy_controller.ex 63 | 64 | Open up the controller and write a basic module, like: 65 | 66 | defmodule MyApplication.PuppyController do 67 | use Boss.WebController 68 | 69 | def index(:GET, []) do 70 | {:output, "Hello, world!"} 71 | end 72 | 73 | def about(:GET, []) do 74 | {:output, "Hello, world!"} 75 | end 76 | 77 | end 78 | 79 | To test it out, start the server and visit: 80 | 81 | http://localhost:8001/puppy/index 82 | 83 | The ChicagoBoss API is essentially the same as the Erlang one. 84 | 85 | There are two variables implicitly available to your handlers: "req" (the 86 | SimpleBridge request object) and "session_id" (the session identifier, if 87 | present). 88 | 89 | Other macros available include "before_", "cache_", "after_", and "lang_", 90 | which take the same arguments as their Erlang counterparts. 91 | 92 | Tokens are passed in as Elixir strings (binaries), not Erlang strings. 93 | 94 | Feel free to mix and match Elixir and Erlang controllers in the same project, 95 | but watch out for name collisions. 96 | 97 | You can access the regular Chicago Boss Erlang API using atoms: 98 | 99 | get :index, [] do 100 | puppies = :boss_db.find(:puppy, [{:name, :equals, "Fido"}]) 101 | {:ok, [{:puppies, puppies}]} 102 | end 103 | 104 | 105 | View API 106 | -- 107 | 108 | Chicago Boss also support EEx files (Embedded Elixir). These should go in 109 | src/view// and have an extension ".eex". Example: 110 | 111 | src/view/puppy/index.eex 112 | 113 | You can open it up and write a basic template like: 114 | 115 | Puppy name: <%= @puppy.name %> 116 | 117 | Note that top-level variables should be prefixed with @. 118 | 119 | To populate this template, you'll want something in your controller like: 120 | 121 | def index(:GET, []) do 122 | {:ok, [{:puppy, [{:name, "Fido"}]}]} 123 | end 124 | 125 | To write a loop, use Enum.map: 126 | 127 | <%= Enum.map @puppies, fn(pup) -> %> 128 | Pupply name: <%= pup.name %>
129 | <% end %> 130 | 131 | This is a comment: 132 | 133 | <%# Comment %> 134 | 135 | Of course, you're free to continue using Django templates with extension ".dtl" 136 | if you prefer a dedicated template language. 137 | 138 | 139 | Model API 140 | -- 141 | 142 | Ecto is database wrapper and query system written in Elixir. You can use it 143 | instead of BossDB for a pure-Elixir data modeling experience. Read more about 144 | Ecto here: 145 | 146 | https://github.com/elixir-lang/ecto 147 | 148 | To enable Ecto in your project, set the `model_manager' config key to `ecto': 149 | 150 | [{boss, [ 151 | ... 152 | {model_manager, ecto}, 153 | ... 154 | ] 155 | }] 156 | 157 | Model files go in src/model as usual. A simple model file might look like: 158 | 159 | defmodule MyApp.Greeting do 160 | use Ecto.Model 161 | 162 | queryable "greeting" do 163 | field :greeting_text, :string 164 | end 165 | end 166 | 167 | To interact with the database, use `Boss.Repo', e.g.: 168 | 169 | greeting = MyApp.Greeting.new(greeting_text: "Hello, world!") 170 | 171 | saved_greeting = Boss.Repo.create(greeting) 172 | 173 | all_greetings = Boss.Repo.all(MyApp.Greeting) 174 | 175 | You can display query results seamlessly from ErlyDTL templates. The template 176 | engine will automatically call "get" and "to_list" on associations as needed. 177 | 178 | Enjoy! 179 | -------------------------------------------------------------------------------- /src/boss/boss_session_test_app.erl: -------------------------------------------------------------------------------- 1 | -module(boss_session_test_app). 2 | 3 | -behaviour(application). 4 | -export([start/2, stop/1]). 5 | 6 | 7 | -spec(start(_,_) -> no_return()). 8 | start(_Type, _StartArgs) -> 9 | CacheAdapter = boss_env:get_env(cache_adapter, memcached_bin), 10 | CacheOptions = 11 | case CacheAdapter of 12 | ets -> 13 | MaxSize = boss_env:get_env(ets_maxsize, 32 * 1024 * 1024), 14 | Threshold = boss_env:get_env(ets_threshold, 0.85), 15 | Weight = boss_env:get_env(ets_weight, 30), 16 | [{adapter, ets}, {ets_maxsize, MaxSize}, 17 | {ets_threshold, Threshold}, {ets_weight, Weight}]; 18 | _ -> 19 | [{adapter, CacheAdapter}, 20 | {cache_servers, boss_env:get_env(cache_servers, [{"127.0.0.1", 11211, 1}])}] 21 | end, 22 | case application:get_env(session_adapter) of 23 | {ok, mnesia} -> 24 | mnesia:stop(), 25 | mnesia:create_schema([node()]); 26 | {ok, cache} -> 27 | boss_cache:start(CacheOptions); 28 | _ -> ok 29 | end, 30 | boss_session:start(), 31 | run_tests(), 32 | erlang:halt(). 33 | 34 | stop(_State) -> 35 | ok. 36 | 37 | run_tests() -> 38 | io:format("~-60s", ["Inserting keys in session"]), 39 | Key1 = "ricky", 40 | Key1Value = "jarl", 41 | Key2 = "cleaf", 42 | Key2Value = "pascual", 43 | SessionID = "Foobar", 44 | do( 45 | fun() -> 46 | boss_session:set_session_data(SessionID, Key1, Key1Value), 47 | boss_session:set_session_data(SessionID, Key2, Key2Value) 48 | end, 49 | [ 50 | fun(_) -> 51 | {boss_session:get_session_data(SessionID, Key1) =:= Key1Value, 52 | "Key1 value not stored correctly in session"} 53 | end, 54 | fun(_) -> 55 | {boss_session:get_session_data(SessionID, Key2) =:= Key2Value, 56 | "Key2 value not stored correctly in session"} 57 | end 58 | ], 59 | [ "Get all data from session", 60 | fun(_) -> 61 | do( 62 | fun() -> 63 | Result = boss_session:get_session_data(SessionID), 64 | Result 65 | end, 66 | [ 67 | fun(Result) -> 68 | {length(Result) == 2, 69 | "Get session data not returning all values"} 70 | end, 71 | fun(Result) -> 72 | {proplists:get_value(Key1, Result) =:= Key1Value, 73 | "Get session value for key1 not correctly"} 74 | end, 75 | fun(Result) -> 76 | {proplists:get_value(Key2, Result) =:= Key2Value, 77 | "Get session value for key2 not correctly"} 78 | end 79 | ], []) 80 | end, 81 | 82 | "Get keys from session", 83 | fun(_) -> 84 | do( 85 | fun() -> 86 | Key1Result = boss_session:get_session_data(SessionID, Key1), 87 | Key2Result = boss_session:get_session_data(SessionID, Key2), 88 | {Key1Result, Key2Result} 89 | end, 90 | [ 91 | fun({Key1Result, _Key2Result}) -> 92 | {Key1Result =:= Key1Value, 93 | "Get session value for key1 - direct - not correct"} 94 | end, 95 | fun({_Key1Result, Key2Result}) -> 96 | {Key2Result =:= Key2Value, 97 | "Get session value for key2 - direct - not correct"} 98 | end 99 | ], []) 100 | end, 101 | 102 | "Removing keys", 103 | fun(_) -> 104 | do( 105 | fun() -> 106 | boss_session:remove_session_data(SessionID, Key1), 107 | Key1Result = boss_session:get_session_data(SessionID, Key1), 108 | Key2Result = boss_session:get_session_data(SessionID, Key2), 109 | {Key1Result, Key2Result} 110 | end, 111 | [ 112 | fun({Key1Result, _Key2Result}) -> 113 | {Key1Result =:= undefined, 114 | "Key1 not removed correctly"} 115 | end, 116 | fun({_Key1Result, Key2Result}) -> 117 | {Key2Result =:= Key2Value, 118 | "Get session value for key2 - after remove key1 - not correct"} 119 | end 120 | ], []) 121 | end, 122 | 123 | "Removing all session", 124 | fun(_) -> 125 | do( 126 | fun() -> 127 | boss_session:delete_session(SessionID), 128 | AllResult = boss_session:get_session_data(SessionID), 129 | Key1Result = boss_session:get_session_data(SessionID, Key1), 130 | Key2Result = boss_session:get_session_data(SessionID, Key2), 131 | {AllResult, Key1Result, Key2Result} 132 | end, 133 | [ 134 | fun({AllResult, _Key1Result, _Key2Result}) -> 135 | {AllResult =:= [], 136 | "Session not deleted"} 137 | end, 138 | fun({_AllResult, Key1Result, _Key2Result}) -> 139 | {Key1Result =:= undefined, 140 | "Key1 not removed correctly after session delete"} 141 | end, 142 | fun({_AllResult, _Key1Result, Key2Result}) -> 143 | {Key2Result =:= undefined, 144 | "Key2 not removed correctly after session delete"} 145 | end 146 | ], []) 147 | end 148 | 149 | ] 150 | ). 151 | 152 | do(Fun, Assertions, Continuations) -> 153 | boss_test:process_assertions_and_continuations(Assertions, Continuations, Fun()). 154 | 155 | -------------------------------------------------------------------------------- /README_DATABASE.md: -------------------------------------------------------------------------------- 1 | The Database README 2 | =================== 3 | 4 | By default Chicago Boss uses an in-memory database, which is useful for 5 | development and testing but not much else. 6 | 7 | 8 | Using PostgreSQL/MySQL 9 | ---------------------- 10 | 11 | Open up boss.config and set db_adapter to `pgsql` or `mysql`. You should also set: 12 | 13 | - db_host 14 | - db_port 15 | - db_username 16 | - db_password 17 | - db_database 18 | 19 | Set the db_port appropriately. By default, PostgreSQL listens on port 5432. 20 | 21 | Models 22 | - - - 23 | 24 | To use Chicago Boss with a SQL database, you need to create a table for each of 25 | your model files. The table name should be the plural of the model name. Field 26 | names should be the underscored versions of the model attribute names. Use 27 | whatever data types make sense for your application. 28 | 29 | ID fields 30 | - - - - - 31 | 32 | The "id" field should be a serial integer. (However, the generated 33 | BossRecord ID exposed to your application will still have the form "model_name-1".) 34 | 35 | Here are useful starting points for MySQL and PostgreSQL respectively: 36 | 37 | CREATE TABLE models ( 38 | id MEDIUMINT NOT NULL AUTO_INCREMENT, 39 | ... 40 | PRIMARY KEY (id) 41 | ); 42 | 43 | CREATE TABLE models ( 44 | id SERIAL PRIMARY KEY, 45 | ... 46 | ); 47 | 48 | Counters 49 | - - - - 50 | 51 | To use the counter features, you need a table called "counters" with a "name" 52 | and "value" column, like these: 53 | 54 | CREATE TABLE counters ( 55 | name VARCHAR(255) PRIMARY KEY, 56 | value INTEGER DEFAULT 0 57 | ); 58 | 59 | 60 | Using Tokyo Tyrant 61 | ------------------ 62 | 63 | Tokyo Tyrant is a non-relational database developed by FAL Labs. 64 | 65 | 1. Download and install Tokyo Tyrant from 66 | 67 | 2. Run Tyrant with a table database. Other database types are not supported. 68 | 69 | 3. Open up boss.config and set db_adapter to tyrant 70 | 71 | Set the db_port appropriately. By default, Tokyo Tyrant listens on port 1978. 72 | 73 | 74 | Using MongoDB 75 | ------------------ 76 | 77 | MongoDB is a document-oriented NoSQL database developed by 10gen. 78 | 79 | 1. Download and install MongoDB from 80 | 81 | 2. Start MongoDB with `mongod` 82 | 83 | 3. Open up boss.config and set the db_adapter to mongodb and db_database to the name of your choice. The database will be created automatically, with the name you specified. 84 | 85 | Set the db_port appropriately. By default, MongoDB listens on port 27017. 86 | 87 | 88 | Using Mnesia 89 | ------------ 90 | 91 | Notes: 92 | 93 | 1. The erlang VM needs a name and cookie to run Mnesia. You can set them with 94 | the 'vm_name' and 'vm_cookie' options in boss.config. 95 | 96 | 2. Create a table for each model as well a table called '_ids_' with attributes 97 | for [type, id]. For more details see 98 | 99 | https://github.com/evanmiller/ChicagoBoss/wiki/Automatic-schema-initialization-for-mnesia 100 | 101 | 3. Open up boss.config and set db_adapter to mnesia 102 | 103 | 104 | Using Riak 105 | ------------------------- 106 | 107 | Riak is a NoSQL database developed by Basho Technologies. 108 | 109 | 1. Install Riak as described at http://wiki.basho.com/Installation-and-Setup.html 110 | 111 | 2. Start a Riak node with `sudo riak start` 112 | 113 | 3. Open up boss.config and set db_adapter to riak. 114 | 115 | For query operations to work, you first need to ensure that search is enabled 116 | in your Riak configuration: 117 | 118 | {riak_search, [{enabled, true}]} 119 | 120 | Then run the following command in your Riak installation for each model: 121 | 122 | bin/search-cmd install 123 | 124 | E.g. if you have a "greeting" model the command should be 125 | 126 | bin/search-cmd install greetings 127 | 128 | Database Migrations 129 | ------------------- 130 | 131 | Chicago Boss supports Rails-like database migrations, which work like so: 132 | 133 | > boss_migrate:make(your_app_name, create_some_table). 134 | 135 | This creates a file in priv/migrations/ with a name like 1363463613_create_some_table.erl 136 | 137 | Edit this file in order to do something along these lines: 138 | 139 | {create_some_table, 140 | fun(up) -> 141 | boss_db:execute("create table some_table ( id serial unique, name varchar, 142 | birth_date timestamp, created_at timestamp )"); 143 | (down) -> 144 | boss_db:execute("drop table some_table") 145 | end}. 146 | 147 | To actually run the migrations, you'd do 148 | 149 | > boss_migrate:run(gsd_web). 150 | 151 | You can also redo the last migration - which is useful when you're 152 | developing and perhaps haven't got the table definition quite right: 153 | 154 | > boss_migrate:redo(gsd_web, create_some_table). 155 | 156 | Internally, migrations are tracked in a table called 157 | "schema_migrations" which is automatically created and managed by 158 | boss_db. 159 | --------------------------------------------------------------------------------