Current section

Files

Jump to
hecate_sdk src hecate_plugin_codegen_plugin.erl
Raw

src/hecate_plugin_codegen_plugin.erl

%%% @doc Plugin and division templates for code generation.
%%%
%%% Templates for: plugin callback, supervisor, app.src, rebar.config,
%%% manifest.json, aggregate, state, state/status headers, department supervisor.
%%% @end
-module(hecate_plugin_codegen_plugin).
-export([tpl_plugin_callback/5, tpl_plugin_sup/1]).
-export([tpl_app_src/2, tpl_rebar_config/1, tpl_manifest/5]).
-export([tpl_aggregate/3, tpl_state/2, tpl_state_hrl/2, tpl_status_hrl/1]).
-export([tpl_dept_sup/1]).
-import(hecate_plugin_codegen, [fmt/2, s/1, sdk_version/0, store_name/1]).
%% ===================================================================
%% Plugin Templates
%% ===================================================================
tpl_plugin_callback(Mod, Name, DisplayName, Version, Tag) ->
SdkVsn = sdk_version(),
fmt("%%% @doc ~s plugin callback module.\n"
"-module(~s).\n"
"-behaviour(hecate_plugin).\n"
"\n"
"-include_lib(\"hecate_sdk/include/hecate_plugin.hrl\").\n"
"\n"
"-export([init/1, routes/0, store_config/0, static_dir/0, manifest/0, flag_maps/0]).\n"
"-export([health/0]).\n"
"\n"
"-define(DOMAIN_APPS, [\n"
" %% TODO: Add domain apps here\n"
"]).\n"
"\n"
"-spec init(map()) -> {ok, map()} | {error, term()}.\n"
"init(#{plugin_name := PluginName, store_id := StoreId, data_dir := DataDir}) ->\n"
" persistent_term:put(~s_config, #{\n"
" plugin_name => PluginName,\n"
" store_id => StoreId,\n"
" data_dir => DataDir\n"
" }),\n"
" persistent_term:put(hecate_current_plugin, PluginName),\n"
" lists:foreach(fun(App) -> application:load(App) end, ?DOMAIN_APPS),\n"
" case ~s_sup:start_link() of\n"
" {ok, Pid} -> {ok, #{sup_pid => Pid}};\n"
" {error, {already_started, Pid}} -> {ok, #{sup_pid => Pid}};\n"
" {error, Reason} -> {error, Reason}\n"
" end.\n"
"\n"
"-spec routes() -> [{string(), module(), term()}].\n"
"routes() ->\n"
" [hecate_plugin_routes:strip_api_prefix(R)\n"
" || R <- hecate_plugin_routes:discover_routes(?DOMAIN_APPS)].\n"
"\n"
"-spec store_config() -> #hecate_store_config{}.\n"
"store_config() ->\n"
" #hecate_store_config{\n"
" store_id = ~s_store,\n"
" dir_name = \"~s\",\n"
" description = \"~s event store\"\n"
" }.\n"
"\n"
"-spec static_dir() -> string() | none.\n"
"static_dir() -> \"priv/static\".\n"
"\n"
"-spec manifest() -> map().\n"
"manifest() ->\n"
" #{\n"
" name => <<\"~s\">>,\n"
" display_name => <<\"~s\">>,\n"
" version => <<\"~s\">>,\n"
" description => <<\"~s plugin\">>,\n"
" tag => <<\"~s\">>,\n"
" min_sdk_version => <<\"~s\">>\n"
" }.\n"
"\n"
"-spec flag_maps() -> #{binary() => evoq_bit_flags:flag_map()}.\n"
"flag_maps() -> #{}.\n"
"\n"
"-spec health() -> ok | degraded | {unhealthy, binary()}.\n"
"health() ->\n"
" case is_pid(whereis(~s_sup)) of\n"
" true -> ok;\n"
" false -> {unhealthy, <<\"supervision tree down\">>}\n"
" end.\n",
[s(DisplayName), s(Mod),
s(Mod), s(Mod),
store_name(Name), store_name(Name), s(DisplayName),
s(Name), s(DisplayName), s(Version), s(DisplayName), s(Tag),
SdkVsn,
s(Mod)]).
tpl_plugin_sup(Mod) ->
SupMod = s(Mod) ++ "_sup",
fmt("%%% @doc Top-level supervisor for ~s plugin.\n"
"-module(~s).\n"
"-behaviour(supervisor).\n"
"\n"
"-export([start_link/0, init/1]).\n"
"\n"
"start_link() ->\n"
" supervisor:start_link({local, ?MODULE}, ?MODULE, []).\n"
"\n"
"init([]) ->\n"
" SupFlags = #{strategy => one_for_one, intensity => 10, period => 60},\n"
" Children = [\n"
" %% TODO: Add department supervisors here\n"
" %% child(my_cmd_app_sup),\n"
" %% child(my_prj_app_sup),\n"
" %% child(my_qry_app_sup)\n"
" ],\n"
" {ok, {SupFlags, Children}}.\n"
"\n"
"child(Mod) ->\n"
" #{id => Mod, start => {Mod, start_link, []},\n"
" restart => permanent, type => supervisor}.\n",
[s(Mod), SupMod]).
%% ===================================================================
%% Division Templates
%% ===================================================================
tpl_aggregate(AggMod, StateMod, StatusHrl) ->
fmt("%%% @doc ~s aggregate.\n"
"-module(~s).\n"
"-behaviour(evoq_aggregate).\n"
"\n"
"-include(\"~s\").\n"
"-include(\"~s\").\n"
"\n"
"-export([state_module/0, init/1, execute/2, apply/2]).\n"
"\n"
"-type state() :: #~s{}.\n"
"\n"
"-spec state_module() -> module().\n"
"state_module() -> ~s.\n"
"\n"
"-spec init(binary()) -> {ok, state()}.\n"
"init(AggregateId) ->\n"
" {ok, ~s:new(AggregateId)}.\n"
"\n"
"%% NOTE: evoq calls execute(State, Payload) - State FIRST!\n"
"-spec execute(state(), map()) -> {ok, [map()]} | {error, term()}.\n"
"execute(#~s{status = 0}, Payload) ->\n"
" %% Fresh aggregate -- only initiate allowed\n"
" case maps:get(command_type, Payload, undefined) of\n"
" %% TODO: Match initiate command\n"
" _ -> {error, not_initiated}\n"
" end;\n"
"execute(#~s{}, _Payload) ->\n"
" %% TODO: Route commands by type\n"
" {error, unknown_command}.\n"
"\n"
"-spec apply(state(), map()) -> state().\n"
"apply(State, Event) ->\n"
" ~s:apply_event(State, Event).\n",
[AggMod, AggMod,
StatusHrl, StateMod ++ ".hrl",
StateMod, StateMod, StateMod,
StateMod, StateMod, StateMod]).
tpl_state(StateMod, Subject) ->
RecName = Subject ++ "_state",
fmt("%%% @doc ~s state module.\n"
"-module(~s).\n"
"-behaviour(evoq_state).\n"
"\n"
"-include(\"~s.hrl\").\n"
"\n"
"-export([new/1, apply_event/2]).\n"
"\n"
"-spec new(binary()) -> #~s{}.\n"
"new(Id) ->\n"
" #~s{id = Id, status = 0}.\n"
"\n"
"-spec apply_event(#~s{}, map()) -> #~s{}.\n"
"apply_event(State, #{event_type := EventType} = Data) ->\n"
" do_apply(EventType, Data, State);\n"
"apply_event(State, #{<<\"event_type\">> := EventType} = Data) ->\n"
" do_apply(EventType, Data, State);\n"
"apply_event(State, _) ->\n"
" State.\n"
"\n"
"do_apply(_EventType, _Data, State) ->\n"
" %% TODO: Handle events and update state\n"
" State.\n",
[StateMod, StateMod, StateMod,
RecName, RecName, RecName, RecName]).
tpl_state_hrl(_StateMod, Subject) ->
RecName = Subject ++ "_state",
Guard = string:uppercase(RecName ++ "_hrl"),
fmt("-ifndef(~s).\n"
"-define(~s, true).\n"
"\n"
"-record(~s, {\n"
" id :: binary(),\n"
" status = 0 :: non_neg_integer()\n"
" %% TODO: Add state fields\n"
"}).\n"
"\n"
"-endif.\n",
[Guard, Guard, RecName]).
tpl_status_hrl(Subject) ->
Prefix = string:uppercase(string:slice(Subject, 0, 2)),
Guard = string:uppercase(Subject ++ "_status_hrl"),
fmt("-ifndef(~s).\n"
"-define(~s, true).\n"
"\n"
"%% Bit flag definitions for ~s status\n"
"-define(~s_INITIATED, 1). %% 2^0\n"
"-define(~s_ARCHIVED, 2). %% 2^1\n"
"%% TODO: Add status flags (powers of 2)\n"
"\n"
"-define(~s_FLAG_MAP, #{\n"
" ?~s_INITIATED => <<\"initiated\">>,\n"
" ?~s_ARCHIVED => <<\"archived\">>\n"
"}).\n"
"\n"
"-endif.\n",
[Guard, Guard, Subject,
Prefix, Prefix, Prefix, Prefix, Prefix]).
%% ===================================================================
%% Shared Templates (app.src, dept_sup, rebar.config, manifest)
%% ===================================================================
tpl_app_src(AppName, Desc) ->
fmt("{application, ~s, [\n"
" {description, \"~s\"},\n"
" {vsn, \"0.1.0\"},\n"
" {registered, []},\n"
" {applications, [kernel, stdlib]},\n"
" {env, []}\n"
"]}.\n",
[s(AppName), Desc]).
tpl_dept_sup(AppName) ->
SupMod = s(AppName) ++ "_sup",
fmt("%%% @doc ~s department supervisor.\n"
"-module(~s).\n"
"-behaviour(supervisor).\n"
"\n"
"-export([start_link/0, init/1]).\n"
"\n"
"start_link() ->\n"
" supervisor:start_link({local, ?MODULE}, ?MODULE, []).\n"
"\n"
"init([]) ->\n"
" {ok, {#{strategy => one_for_one, intensity => 10, period => 60}, []}}.\n",
[s(AppName), SupMod]).
tpl_rebar_config(AppAtom) ->
SdkVsn = sdk_version(),
fmt("{erl_opts, [\n"
" debug_info,\n"
" warnings_as_errors\n"
"]}.\n"
"\n"
"{deps, [\n"
" {hecate_sdk, \"~s\"}\n"
"]}.\n"
"\n"
"{relx, [\n"
" {release, {~s, \"0.1.0\"}, [\n"
" ~s,\n"
" sasl\n"
" ]},\n"
" {mode, dev},\n"
" {dev_mode, true},\n"
" {include_erts, false},\n"
" {extended_start_script, true},\n"
" {sys_config, \"config/sys.config\"},\n"
" {vm_args, \"config/vm.args\"}\n"
"]}.\n"
"\n"
"{profiles, [\n"
" {prod, [{relx, [{mode, prod}, {dev_mode, false}, {include_erts, true}]}]},\n"
" {test, [{deps, [{meck, \"0.9.2\"}]}]}\n"
"]}.\n"
"\n"
"{project_plugins, [rebar3_ex_doc]}.\n",
[SdkVsn, s(AppAtom), s(AppAtom)]).
tpl_manifest(Name, DisplayName, Version, Tag, CallbackMod) ->
SdkVsn = sdk_version(),
fmt("{\n"
" \"name\": \"~s\",\n"
" \"display_name\": \"~s\",\n"
" \"version\": \"~s\",\n"
" \"description\": \"~s plugin\",\n"
" \"tag\": \"~s\",\n"
" \"callback_module\": \"~s\",\n"
" \"plugin_type\": \"in_vm\",\n"
" \"min_sdk_version\": \"~s\"\n"
"}\n",
[s(Name), s(DisplayName), s(Version), s(DisplayName),
s(Tag), s(CallbackMod), SdkVsn]).