Current section

Files

Jump to
hecate_sdk src hecate_plugin_codegen_cmd.erl
Raw

src/hecate_plugin_codegen_cmd.erl

%%% @doc CMD desk templates for code generation.
%%%
%%% Templates for: command, event, handler, API handler, CMD tests.
%%% @end
-module(hecate_plugin_codegen_cmd).
-export([desk/1]).
-import(hecate_plugin_codegen, [fmt/2, s/1, b/1, write_files/1,
pluralize/1, replace_src_with_test/1, subject_from_cmd/1]).
%% ===================================================================
%% desk/1 -- CMD desk entry point
%% ===================================================================
-spec desk(map()) -> {ok, [string()]}.
desk(#{verb := Verb, subject := Subject, past_verb := PastVerb} = Opts) ->
OutDir = maps:get(output_dir, Opts, "src"),
StoreId = maps:get(store_id, Opts, <<"martha_store">>),
AggType = maps:get(aggregate_type, Opts, b(s(Subject) ++ "_aggregate")),
DeskName = s(Verb) ++ "_" ++ s(Subject),
CmdMod = DeskName ++ "_v1",
EvtMod = s(Subject) ++ "_" ++ s(PastVerb) ++ "_v1",
HandlerMod = "maybe_" ++ DeskName,
ApiMod = DeskName ++ "_api",
DeskDir = filename:join(OutDir, DeskName),
TestDir = maps:get(test_dir, Opts, replace_src_with_test(OutDir)),
ApiPath = maps:get(api_path, Opts, "/api/" ++ pluralize(s(Subject)) ++ "/" ++ s(Verb)),
Files = [
{filename:join(DeskDir, CmdMod ++ ".erl"),
tpl_command(CmdMod, DeskName, Subject)},
{filename:join(DeskDir, EvtMod ++ ".erl"),
tpl_event(EvtMod, Subject, PastVerb)},
{filename:join(DeskDir, HandlerMod ++ ".erl"),
tpl_handler(HandlerMod, CmdMod, EvtMod, AggType, StoreId)},
{filename:join(DeskDir, ApiMod ++ ".erl"),
tpl_cmd_api(ApiMod, CmdMod, HandlerMod, ApiPath, Subject)},
{filename:join(TestDir, DeskName ++ "_tests.erl"),
tpl_cmd_tests(DeskName, CmdMod, EvtMod, HandlerMod)}
],
write_files(Files).
%% ===================================================================
%% Templates
%% ===================================================================
tpl_command(CmdMod, DeskName, Subject) ->
IdField = s(Subject) ++ "_id",
fmt("%%% @doc ~s command.\n"
"-module(~s).\n"
"-behaviour(evoq_command).\n"
"\n"
"-export([command_type/0]).\n"
"-export([new/1, from_map/1, validate/1, to_map/1]).\n"
"-export([get_~s/1]).\n"
"-export([generate_id/0]).\n"
"\n"
"-record(~s, {\n"
" ~s :: binary()\n"
" %% TODO: Add command fields\n"
"}).\n"
"\n"
"-spec command_type() -> atom().\n"
"command_type() -> ~s.\n"
"\n"
"-spec new(map()) -> {ok, #~s{}} | {error, term()}.\n"
"new(Params) ->\n"
" Id = maps:get(~s, Params, generate_id()),\n"
" {ok, #~s{~s = Id}}.\n"
"\n"
"-spec validate(#~s{}) -> {ok, #~s{}} | {error, term()}.\n"
"validate(#~s{} = Cmd) -> {ok, Cmd}.\n"
"\n"
"-spec to_map(#~s{}) -> map().\n"
"to_map(#~s{} = Cmd) ->\n"
" #{command_type => ~s, ~s => Cmd#~s.~s}.\n"
"\n"
"-spec from_map(map()) -> {ok, #~s{}} | {error, term()}.\n"
"from_map(Map) ->\n"
" Id = hecate_plugin_api:get_field(~s, Map, generate_id()),\n"
" {ok, #~s{~s = Id}}.\n"
"\n"
"get_~s(#~s{~s = V}) -> V.\n"
"\n"
"generate_id() ->\n"
" Ts = integer_to_binary(erlang:system_time(millisecond)),\n"
" Rand = binary:encode_hex(crypto:strong_rand_bytes(8)),\n"
" <<\"~s-\", Ts/binary, \"-\", Rand/binary>>.\n",
[DeskName, CmdMod,
IdField,
CmdMod, IdField,
CmdMod, CmdMod,
IdField, CmdMod, IdField,
CmdMod, CmdMod, CmdMod,
CmdMod, CmdMod,
CmdMod, IdField, CmdMod, IdField,
CmdMod, IdField, CmdMod, IdField,
IdField, CmdMod, IdField,
s(Subject)]).
tpl_event(EvtMod, Subject, PastVerb) ->
IdField = s(Subject) ++ "_id",
fmt("%%% @doc ~s_~s event.\n"
"-module(~s).\n"
"-behaviour(evoq_event).\n"
"\n"
"-export([event_type/0]).\n"
"-export([new/1, to_map/1, from_map/1]).\n"
"-export([get_~s/1]).\n"
"\n"
"-record(~s, {\n"
" ~s :: binary(),\n"
" ~s_at :: integer()\n"
" %% TODO: Add event fields\n"
"}).\n"
"\n"
"-spec event_type() -> atom().\n"
"event_type() -> ~s.\n"
"\n"
"-spec new(map()) -> #~s{}.\n"
"new(#{~s := Id} = _Params) ->\n"
" #~s{~s = Id, ~s_at = erlang:system_time(millisecond)}.\n"
"\n"
"-spec to_map(#~s{}) -> map().\n"
"to_map(#~s{} = E) ->\n"
" #{event_type => ~s,\n"
" ~s => E#~s.~s,\n"
" ~s_at => E#~s.~s_at}.\n"
"\n"
"-spec from_map(map()) -> {ok, #~s{}} | {error, term()}.\n"
"from_map(Map) ->\n"
" Id = hecate_plugin_api:get_field(~s, Map),\n"
" case Id of\n"
" undefined -> {error, invalid_event};\n"
" _ -> {ok, #~s{\n"
" ~s = Id,\n"
" ~s_at = hecate_plugin_api:get_field(~s_at, Map,\n"
" erlang:system_time(millisecond))\n"
" }}\n"
" end.\n"
"\n"
"get_~s(#~s{~s = V}) -> V.\n",
[s(Subject), s(PastVerb), EvtMod,
IdField,
EvtMod, IdField, s(PastVerb),
EvtMod, EvtMod, IdField, EvtMod, IdField, s(PastVerb),
EvtMod, EvtMod, EvtMod,
IdField, EvtMod, IdField,
s(PastVerb), EvtMod, s(PastVerb),
EvtMod, IdField,
EvtMod, IdField, s(PastVerb), s(PastVerb),
IdField, EvtMod, IdField]).
tpl_handler(HandlerMod, CmdMod, EvtMod, AggType, StoreId) ->
SubjectStr = subject_from_cmd(CmdMod),
fmt("%%% @doc ~s handler.\n"
"-module(~s).\n"
"\n"
"-include_lib(\"evoq/include/evoq.hrl\").\n"
"\n"
"-export([handle/1, handle/2, dispatch/1]).\n"
"\n"
"-spec handle(~s:~s()) ->\n"
" {ok, [~s:~s()]} | {error, term()}.\n"
"handle(Cmd) -> handle(Cmd, undefined).\n"
"\n"
"-spec handle(~s:~s(), term()) ->\n"
" {ok, [~s:~s()]} | {error, term()}.\n"
"handle(Cmd, _State) ->\n"
" Event = ~s:new(#{~s_id => ~s:get_~s_id(Cmd)}),\n"
" {ok, [Event]}.\n"
"\n"
"-spec dispatch(~s:~s()) ->\n"
" {ok, non_neg_integer(), [map()]} | {error, term()}.\n"
"dispatch(Cmd) ->\n"
" Id = ~s:get_~s_id(Cmd),\n"
" EvoqCmd = #evoq_command{\n"
" command_type = ~s:command_type(),\n"
" aggregate_type = ~s,\n"
" aggregate_id = Id,\n"
" payload = ~s:to_map(Cmd),\n"
" metadata = #{timestamp => erlang:system_time(millisecond),\n"
" aggregate_type => ~s}\n"
" },\n"
" evoq_dispatcher:dispatch(EvoqCmd, #{\n"
" store_id => ~s,\n"
" adapter => reckon_evoq_adapter,\n"
" consistency => eventual\n"
" }).\n",
[HandlerMod, HandlerMod,
CmdMod, CmdMod, EvtMod, EvtMod,
CmdMod, CmdMod, EvtMod, EvtMod,
EvtMod, SubjectStr, CmdMod, SubjectStr,
CmdMod, CmdMod,
CmdMod, SubjectStr,
CmdMod, s(AggType), CmdMod, s(AggType),
s(StoreId)]).
tpl_cmd_api(ApiMod, CmdMod, HandlerMod, ApiPath, Subject) ->
IdField = s(Subject) ++ "_id",
fmt("%%% @doc API handler: POST ~s\n"
"-module(~s).\n"
"-export([init/2, routes/0]).\n"
"\n"
"routes() -> [{\"~s\", ?MODULE, []}].\n"
"\n"
"init(Req0, State) ->\n"
" case cowboy_req:method(Req0) of\n"
" <<\"POST\">> -> handle_post(Req0, State);\n"
" _ -> hecate_plugin_api:method_not_allowed(Req0)\n"
" end.\n"
"\n"
"handle_post(Req0, _State) ->\n"
" case hecate_plugin_api:read_json_body(Req0) of\n"
" {ok, Params, Req1} -> do_command(Params, Req1);\n"
" {error, invalid_json, Req1} ->\n"
" hecate_plugin_api:bad_request(<<\"Invalid JSON\">>, Req1)\n"
" end.\n"
"\n"
"do_command(Params, Req) ->\n"
" case ~s:from_map(Params) of\n"
" {ok, Cmd} ->\n"
" case ~s:dispatch(Cmd) of\n"
" {ok, Version, Events} ->\n"
" hecate_plugin_api:json_ok(201, #{\n"
" ~s => ~s:get_~s(Cmd),\n"
" version => Version,\n"
" events => Events\n"
" }, Req);\n"
" {error, Reason} ->\n"
" hecate_plugin_api:bad_request(Reason, Req)\n"
" end;\n"
" {error, Reason} ->\n"
" hecate_plugin_api:bad_request(Reason, Req)\n"
" end.\n",
[ApiPath, ApiMod, ApiPath,
CmdMod, HandlerMod,
IdField, CmdMod, IdField]).
tpl_cmd_tests(DeskName, CmdMod, EvtMod, HandlerMod) ->
SubjectStr = subject_from_cmd(CmdMod),
fmt("-module(~s_tests).\n"
"-include_lib(\"eunit/include/eunit.hrl\").\n"
"\n"
"%% ===================================================================\n"
"%% Command tests\n"
"%% ===================================================================\n"
"\n"
"new_command_test() ->\n"
" {ok, Cmd} = ~s:new(#{}),\n"
" ?assert(is_binary(~s:get_~s_id(Cmd))).\n"
"\n"
"to_map_roundtrip_test() ->\n"
" {ok, Cmd} = ~s:new(#{}),\n"
" Map = ~s:to_map(Cmd),\n"
" {ok, Cmd2} = ~s:from_map(Map),\n"
" ?assertEqual(~s:get_~s_id(Cmd), ~s:get_~s_id(Cmd2)).\n"
"\n"
"validate_test() ->\n"
" {ok, Cmd} = ~s:new(#{}),\n"
" ?assertMatch({ok, _}, ~s:validate(Cmd)).\n"
"\n"
"%% ===================================================================\n"
"%% Handler tests\n"
"%% ===================================================================\n"
"\n"
"handle_returns_event_test() ->\n"
" {ok, Cmd} = ~s:new(#{}),\n"
" {ok, Events} = ~s:handle(Cmd),\n"
" ?assertEqual(1, length(Events)).\n"
"\n"
"event_has_correct_type_test() ->\n"
" {ok, Cmd} = ~s:new(#{}),\n"
" {ok, [Event]} = ~s:handle(Cmd),\n"
" Map = ~s:to_map(Event),\n"
" ?assertEqual(~s, maps:get(event_type, Map)).\n",
[DeskName,
CmdMod, CmdMod, SubjectStr,
CmdMod, CmdMod, CmdMod,
CmdMod, SubjectStr, CmdMod, SubjectStr,
CmdMod, CmdMod,
CmdMod, HandlerMod,
CmdMod, HandlerMod, EvtMod, EvtMod]).