From 8656fe7323edfcb73634c7c1b0448dfa587f5bee Mon Sep 17 00:00:00 2001 From: mihawk Date: Tue, 21 Jan 2020 01:12:11 +0700 Subject: [PATCH 1/2] add scafolding --- include/api.hrl | 3 +- src/mad.erl | 4 +- src/mad_local.erl | 1 + src/mad_mustache.erl | 213 +++++++++++++++++++++++++++ src/mad_scafolding.erl | 323 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 542 insertions(+), 2 deletions(-) create mode 100644 src/mad_mustache.erl create mode 100644 src/mad_scafolding.erl diff --git a/include/api.hrl b/include/api.hrl index 39aaca0..f58d03a 100644 --- a/include/api.hrl +++ b/include/api.hrl @@ -1,6 +1,6 @@ -define(MAD,[compile/1,app/1,get/1,man/1,dia/1,release/1,resolve/1,clean/1, start/1,attach/1,stop/1,sh/1,deps/1,up/1,fetch/1,rsa/1,ecc/1, - static/1,eunit/1,strip/1]). + static/1,eunit/1,strip/1,scafolding/1]). -type return() :: [] | true | false | {ok,any()} | {error,any()}. @@ -24,3 +24,4 @@ -spec static(list(string())) -> return(). -spec eunit(list(string())) -> return(). -spec strip(list(string())) -> return(). +-spec scafolding(list(string())) -> return(). diff --git a/src/mad.erl b/src/mad.erl index e945503..ea1610f 100644 --- a/src/mad.erl +++ b/src/mad.erl @@ -5,6 +5,7 @@ -export([main/1]). main([]) -> halt(help()); +main(["sca"++_|P])-> mad_scafolding:tpl(P); main(Params) -> % filter valid (atoms) from invalid (unparsed lists) commands @@ -76,5 +77,6 @@ help() -> info("MAD Manage Dependencies ~s~n",[?VERSION]), info(" cmd = app [nitro|zero] | deps | clean | compile | strip~n"), info(" | bundle [beam|script] | man | repl~n"), info(" | start | stop | attach | static | get | up [name]~n"), - info(" | [ ca | client | server ]~n"), + info(" | scafolding tpl= appid= [VARS...]~n"), + info(" | [ ca | client | server ]~n"), return(false). diff --git a/src/mad_local.erl b/src/mad_local.erl index 77d0620..653d59d 100644 --- a/src/mad_local.erl +++ b/src/mad_local.erl @@ -22,3 +22,4 @@ up(Params) -> mad_git:up(Params). fetch(Params) -> mad_git:fetch(Params). eunit(Params) -> mad_eunit:main_test(Params). sh(Params) -> mad_repl:sh(Params). +scafolding(Params)-> mad_scafolding:tpl(Params). \ No newline at end of file diff --git a/src/mad_mustache.erl b/src/mad_mustache.erl new file mode 100644 index 0000000..05c9549 --- /dev/null +++ b/src/mad_mustache.erl @@ -0,0 +1,213 @@ +%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +%% THE SOFTWARE. + +%% See the README at http://github.com/mojombo/mustache.erl for additional +%% documentation and usage examples. + +-module(mad_mustache). %% v0.1.0 +-author("Tom Preston-Werner"). +-export([compile/1, compile/2, render/1, render/2, render/3, get/2, get/3, escape/1, start/1]). + +-record(mstate, {mod = undefined, + section_re = undefined, + tag_re = undefined}). + +-define(MUSTACHE_STR, "mad_mustache"). + +compile(Body) when is_list(Body) -> + State = #mstate{}, + CompiledTemplate = pre_compile(Body, State), + % io:format("~p~n~n", [CompiledTemplate]), + % io:format(CompiledTemplate ++ "~n", []), + {ok, Tokens, _} = erl_scan:string(CompiledTemplate), + {ok, [Form]} = erl_parse:parse_exprs(Tokens), + Bindings = erl_eval:new_bindings(), + {value, Fun, _} = erl_eval:expr(Form, Bindings), + Fun; +compile(Mod) -> + TemplatePath = template_path(Mod), + compile(Mod, TemplatePath). + +compile(Mod, File) -> + code:purge(Mod), + {module, _} = code:load_file(Mod), + {ok, TemplateBin} = file:read_file(File), + Template = re:replace(TemplateBin, "\"", "\\\\\"", [global, {return,list}]), + State = #mstate{mod = Mod}, + CompiledTemplate = pre_compile(Template, State), + % io:format("~p~n~n", [CompiledTemplate]), + % io:format(CompiledTemplate ++ "~n", []), + {ok, Tokens, _} = erl_scan:string(CompiledTemplate), + {ok, [Form]} = erl_parse:parse_exprs(Tokens), + Bindings = erl_eval:new_bindings(), + {value, Fun, _} = erl_eval:expr(Form, Bindings), + Fun. + +render(Mod) -> + TemplatePath = template_path(Mod), + render(Mod, TemplatePath). + +render(Body, Ctx) when is_list(Body) -> + TFun = compile(Body), + render(undefined, TFun, Ctx); +render(Mod, File) when is_list(File) -> + render(Mod, File, dict:new()); +render(Mod, CompiledTemplate) -> + render(Mod, CompiledTemplate, dict:new()). + +render(Mod, File, Ctx) when is_list(File) -> + CompiledTemplate = compile(Mod, File), + render(Mod, CompiledTemplate, Ctx); +render(Mod, CompiledTemplate, Ctx) -> + Ctx2 = dict:store('__mod__', Mod, Ctx), + lists:flatten(CompiledTemplate(Ctx2)). + +pre_compile(T, State) -> + SectionRE = "\{\{\#([^\}]*)}}\s*(.+?){{\/\\1\}\}\s*", + {ok, CompiledSectionRE} = re:compile(SectionRE, [dotall]), + TagRE = "\{\{(#|=|!|<|>|\{)?(.+?)\\1?\}\}+", + {ok, CompiledTagRE} = re:compile(TagRE, [dotall]), + State2 = State#mstate{section_re = CompiledSectionRE, tag_re = CompiledTagRE}, + "fun(Ctx) -> " ++ + "CFun = fun(A, B) -> A end, " ++ + compiler(T, State2) ++ " end.". + +compiler(T, State) -> + Res = re:run(T, State#mstate.section_re), + case Res of + {match, [{M0, M1}, {N0, N1}, {C0, C1}]} -> + Front = string:substr(T, 1, M0), + Back = string:substr(T, M0 + M1 + 1), + Name = string:substr(T, N0 + 1, N1), + Content = string:substr(T, C0 + 1, C1), + "[" ++ compile_tags(Front, State) ++ + " | [" ++ compile_section(Name, Content, State) ++ + " | [" ++ compiler(Back, State) ++ "]]]"; + nomatch -> + compile_tags(T, State) + end. + +compile_section(Name, Content, State) -> + Mod = State#mstate.mod, + Result = compiler(Content, State), + "fun() -> " ++ + "case " ++ ?MUSTACHE_STR ++ ":get(" ++ Name ++ ", Ctx, " ++ atom_to_list(Mod) ++ ") of " ++ + "\"true\" -> " ++ + Result ++ "; " ++ + "\"false\" -> " ++ + "[]; " ++ + "List when is_list(List) -> " ++ + "[fun(Ctx) -> " ++ Result ++ " end(dict:merge(CFun, SubCtx, Ctx)) || SubCtx <- List]; " ++ + "Else -> " ++ + "throw({template, io_lib:format(\"Bad context for ~p: ~p\", [" ++ Name ++ ", Else])}) " ++ + "end " ++ + "end()". + +compile_tags(T, State) -> + Res = re:run(T, State#mstate.tag_re), + case Res of + {match, [{M0, M1}, K, {C0, C1}]} -> + Front = string:substr(T, 1, M0), + Back = string:substr(T, M0 + M1 + 1), + Content = string:substr(T, C0 + 1, C1), + Kind = tag_kind(T, K), + Result = compile_tag(Kind, Content, State), + "[\"" ++ Front ++ + "\" | [" ++ Result ++ + " | " ++ compile_tags(Back, State) ++ "]]"; + nomatch -> + "[\"" ++ T ++ "\"]" + end. + +tag_kind(_T, {-1, 0}) -> + none; +tag_kind(T, {K0, K1}) -> + string:substr(T, K0 + 1, K1). + +compile_tag(none, Content, State) -> + Mod = State#mstate.mod, + ?MUSTACHE_STR ++ ":escape(" ++ ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ "))"; +compile_tag("{", Content, State) -> + Mod = State#mstate.mod, + ?MUSTACHE_STR ++ ":get(" ++ Content ++ ", Ctx, " ++ atom_to_list(Mod) ++ ")"; +compile_tag("!", _Content, _State) -> + "[]". + +template_dir(Mod) -> + DefaultDirPath = filename:dirname(code:which(Mod)), + case application:get_env(mustache, templates_dir) of + {ok, DirPath} when is_list(DirPath) -> + case filelib:ensure_dir(DirPath) of + ok -> DirPath; + _ -> DefaultDirPath + end; + _ -> + DefaultDirPath + end. +template_path(Mod) -> + DirPath = template_dir(Mod), + Basename = atom_to_list(Mod), + filename:join(DirPath, Basename ++ ".mustache"). + +get(Key, Ctx) when is_list(Key) -> + {ok, Mod} = dict:find('__mod__', Ctx), + get(list_to_atom(Key), Ctx, Mod); +get(Key, Ctx) -> + {ok, Mod} = dict:find('__mod__', Ctx), + get(Key, Ctx, Mod). + +get(Key, Ctx, Mod) when is_list(Key) -> + get(list_to_atom(Key), Ctx, Mod); +get(Key, Ctx, Mod) -> + case dict:find(Key, Ctx) of + {ok, Val} -> + % io:format("From Ctx {~p, ~p}~n", [Key, Val]), + to_s(Val); + error -> + case erlang:function_exported(Mod, Key, 1) of + true -> + Val = to_s(Mod:Key(Ctx)), + % io:format("From Mod/1 {~p, ~p}~n", [Key, Val]), + Val; + false -> + case erlang:function_exported(Mod, Key, 0) of + true -> + Val = to_s(Mod:Key()), + % io:format("From Mod/0 {~p, ~p}~n", [Key, Val]), + Val; + false -> + [] + end + end + end. + +to_s(Val) when is_integer(Val) -> + integer_to_list(Val); +to_s(Val) when is_float(Val) -> + io_lib:format("~.2f", [Val]); +to_s(Val) when is_atom(Val) -> + atom_to_list(Val); +to_s(Val) -> + Val. + +escape(HTML) -> + escape(HTML, []). + +escape([], Acc) -> + lists:reverse(Acc); +escape(["<" | Rest], Acc) -> + escape(Rest, lists:reverse("<", Acc)); +escape([">" | Rest], Acc) -> + escape(Rest, lists:reverse(">", Acc)); +escape(["&" | Rest], Acc) -> + escape(Rest, lists:reverse("&", Acc)); +escape([X | Rest], Acc) -> + escape(Rest, [X | Acc]). + +%%--------------------------------------------------------------------------- + +start([T]) -> + Out = render(list_to_atom(T)), + io:format(Out ++ "~n", []). diff --git a/src/mad_scafolding.erl b/src/mad_scafolding.erl new file mode 100644 index 0000000..daed24f --- /dev/null +++ b/src/mad_scafolding.erl @@ -0,0 +1,323 @@ +-module(mad_scafolding). +-copyright('Chan Sisowath'). +-compile(export_all). + +tpl(Params) -> + mad_repl:load(), + Apps = ets:tab2list(filesystem), + Vars = decode_params(Params), + case proplists:get_value(tpl, Vars) of + undefined -> + mad:info("please specify a template: scafolding tpl= appid=...~n",[]),true; + TplName -> + AppName = proplists:get_value(appid, Vars), + mad:info("AppId = ~p~n",[AppName]), + mad:info("TplName = ~p~n",[TplName]), + mad:info("Vars = ~p~n",[Vars]), + case proplists:get_value("priv/" ++ TplName ++ ".skel", Apps) of + undefined -> case getskel(TplName) of + {error,F} -> mad:info("unable to load the template ~p.~n",[F]),true; + Bin -> TemplateTerms = consult(Bin), + Vars1 = lists:keyreplace(name, 1, Vars, {appid, AppName}), + create({dir,tpldir()}, TemplateTerms, Vars1) + end; + + Skel -> TemplateTerms = consult(Skel), + Vars1 = lists:keyreplace(name, 1, Vars, {appid, AppName}), + create({mem,Apps}, TemplateTerms, Vars1) + end + end. + +tpldir() -> case os:get_env_var("MAD_TEMPLATE_DIR") of + false -> filename:join([os:get_env_var("HOME"), ".mad", "templates"]); + Dir -> Dir end. + +getskel(TplName) -> + F = filename:join([tpldir(),TplName++".skel"]), + case file:read_file(F) of {error,_} -> {error,F}; {ok,Bin} -> Bin end. + +lib(_) -> ok. + +decode_params(Params) -> + decode_params(Params, []). + +decode_params([], Acc) -> Acc; +decode_params([H|T], Acc) -> + [K, V] = string:tokens(H, "="), + decode_params(T, [{list_to_atom(K), V}|Acc]). + +create(Src, FinalTemplate, VarsCtx) -> + case lists:keyfind(variables, 1, FinalTemplate) of + {variables, Vars} -> + case parse_vars(Vars, dict:new()) of + {error, _Entry} -> + Context0 = undefined; + Context0 -> + ok + end; + false -> + Context0 = dict:new() + end, + %%Variables = lists:keyreplace(appid, 1, dict:to_list(Context0), {appid, Name}), + Name = proplists:get_value(appid, VarsCtx), + Variables = dict:to_list(Context0) ++ VarsCtx, + Context = resolve_variables(Variables, Context0), + Force = "1", + %io:format("Context ~p~n", [[ begin {_, Y} = dict:find(X, Context), {X, Y} end|| X <- dict:fetch_keys(Context)]] ), + execute_template(Src, FinalTemplate, none, Name, Context, Force, []), + {ok,Name}. + +% -------------------------------------------------------------------------------- +% internal +% -------------------------------------------------------------------------------- + +-spec consult(string() | binary()) -> [term()]. +consult(Source) -> + + SourceStr = to_str(Source), + {ok, Tokens, _} = erl_scan:string(SourceStr), + Forms = split_when(fun is_dot/1, Tokens), + ParseFun = fun (Form) -> + case erl_parse:parse_exprs(Form) of + {ok, Expr} -> Expr; + E -> mad:info("Error mad_tpl ~p", [E]), E end + end, + Parsed = lists:map(ParseFun, Forms), + ExprsFun = fun(P) -> + {value, Value, _} = erl_eval:exprs(P, []), + Value + end, + lists:map(ExprsFun, Parsed). + +-spec split_when(fun(), list()) -> list(). +split_when(When, List) -> + split_when(When, List, [[]]). + +split_when(When, [], [[] | Results]) -> + split_when(When, [], Results); +split_when(_When, [], Results) -> + Reversed = lists:map(fun lists:reverse/1, Results), + lists:reverse(Reversed); +split_when(When, [Head | Tail], [Current0 | Rest]) -> + Current = [Head | Current0], + Result = case When(Head) of + true -> + [[], Current | Rest]; + false -> + [Current | Rest] + end, + split_when(When, Tail, Result). + +-spec is_dot(tuple()) -> boolean(). +is_dot({dot, _}) -> true; +is_dot(_) -> false. + +-spec to_str(binary() | list() | atom()) -> string(). +to_str(Arg) when is_binary(Arg) -> + unicode:characters_to_list(Arg); +to_str(Arg) when is_atom(Arg) -> + atom_to_list(Arg); +to_str(Arg) when is_integer(Arg) -> + integer_to_list(Arg); +to_str(Arg) when is_list(Arg) -> + Arg. + +%% +%% Given a list of key value pairs, for each string value attempt to +%% render it using Dict as the context. Storing the result in Dict as Key. +%% +resolve_variables([], Dict) -> + Dict; +resolve_variables([{Key, Value0} | Rest], Dict) when is_list(Value0) -> + Value = render(list_to_binary(Value0), Dict), + resolve_variables(Rest, dict:store(Key, Value, Dict)); +resolve_variables([{Key, {list, Dicts}} | Rest], Dict) when is_list(Dicts) -> + %% just un-tag it so mustache can use it + resolve_variables(Rest, dict:store(Key, Dicts, Dict)); +resolve_variables([_Pair | Rest], Dict) -> + resolve_variables(Rest, Dict). + +%% +%% Render a binary to a string, using erlydtl and the specified context +%% +render(Bin, Context) -> + %% Be sure to escape any double-quotes before rendering... + ReOpts = [global, {return, list}], + Str0 = re:replace(Bin, "\\\\", "\\\\\\", ReOpts), + Str1 = re:replace(Str0, "\"", "\\\\\"", ReOpts), + mad_mustache:render(Str1, Context). + +%% =================================================================== +%% Internal functions +%% =================================================================== + +%% +%% Read the contents of a file from the appropriate source +%% +load_file({dir,Dir}, _, Name) -> + case file:read_file(filename:join(Dir,Name)) of + {_, Bin} -> Bin; + _ -> ok + end; +load_file({mem,Files}, _, Name) -> + case lists:keyfind("priv/" ++ Name, 1, Files) of + {_, Bin} -> Bin; + _ -> ok + end. + +%% +%% Parse/validate variables out from the template definition +%% +parse_vars([], Dict) -> + Dict; +parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) -> + parse_vars(Rest, dict:store(Key, Value, Dict)); +parse_vars([Other | _Rest], _Dict) -> + {error, Other}; +parse_vars(Other, _Dict) -> + {error, Other}. + +maybe_dict({Key, {list, Dicts}}) -> + %% this is a 'list' element; a list of lists representing dicts + {Key, {list, [dict:from_list(D) || D <- Dicts]}}; +maybe_dict(Term) -> + Term. + +write_file(Output, Data, Force) -> + %% determine if the target file already exists + FileExists = filelib:is_regular(Output), + + %% perform the function if we're allowed, + %% otherwise just process the next template + case Force =:= "1" orelse FileExists =:= false of + true -> + ok = filelib:ensure_dir(Output), + case {Force, FileExists} of + {"1", true} -> + mad:info("Writing ~s (forcibly overwriting)~n", + [Output]); + _ -> + mad:info("Writing ~s~n", [Output]) + end, + case file:write_file(Output, Data) of + ok -> + ok; + {error, Reason} -> + mad:info("Failed to write output file ~p: ~p\n", + [Output, Reason]) + end; + false -> + {error, exists} + end. + +execute_template(_Files, [], _TemplateType, _TemplateName, + _Context, _Force, ExistingFiles) -> + case ExistingFiles of + [] -> + ok; + _ -> + Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) || + F <- lists:reverse(ExistingFiles)]), + Help = "To force overwriting, specify -f/--force/force=1" + " on the command line.\n", + mad:info("One or more files already exist on disk and " + "were not generated:~n~s~s", [Msg , Help]) + end; + +execute_template(Files, [{template, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + File = load_file(Files, TemplateType, Input), + OutputName = render(Output, Context), + %io:format("template outputName ~p~n",[OutputName]), + case write_file(OutputName, render(File, Context), Force) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, exists} -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, [Output|ExistingFiles]) + end; +execute_template(Files, [{file, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + File = load_file(Files, TemplateType, Input), + OutputName = render(Output, Context), + case write_file(OutputName, File, Force) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, exists} -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, [Output|ExistingFiles]) + end; +execute_template(Files, [{dir, InputName} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + Name = render(InputName, Context), + case filelib:ensure_dir(filename:join(Name, "dummy")) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{dir, ~s}: ~p\n", [Name, Reason]) + end; +execute_template(Files, [{copy, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + InputName = filename:join(filename:dirname(TemplateName), Input), + try cp_r([InputName ++ "/*"], Output) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles) + catch _:_ -> + io:format("Failed while processing template instruction " + "{copy, ~s, ~s}~n", [Input, Output]) + end; +execute_template(Files, [{chmod, Mod, File} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) + when is_integer(Mod) -> + FileName = render(File, Context), + case file:change_mode(FileName, Mod) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{chmod, ~b, ~s}: ~p~n", [Mod, FileName, Reason]) + end; +execute_template(Files, [{symlink, Existing, New} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + case file:make_symlink(Existing, New) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{symlink, ~s, ~s}: ~p~n", [Existing, New, Reason]) + end; +execute_template(Files, [{variables, _} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); +execute_template(Files, [Other | Rest], TemplateType, TemplateName, + Context, Force, ExistingFiles) -> + io:format("Skipping unknown template instruction: ~p\n", [Other]), + execute_template(Files, Rest, TemplateType, TemplateName, Context, + Force, ExistingFiles). + + +-spec cp_r(list(string()), file:filename()) -> 'ok'. +cp_r([], _Dest) -> + ok; +cp_r(Sources, Dest) -> + case os:type() of + {unix, _} -> + EscSources = [escape_path(Src) || Src <- Sources], + SourceStr = string:join(EscSources, " "), + {done, 0, <<>>} = sh:run(["cp", "-R", SourceStr, Dest]), + ok; + {win32, _} -> + io:format("unsuported os."), + ok + end. + +escape_path(Str) -> + re:replace(Str, "([ ()?])", "\\\\&", [global, {return, list}]). From 517e9c9d962147612523d4c24f2a7215599b93c1 Mon Sep 17 00:00:00 2001 From: mihawk Date: Thu, 23 Jan 2020 09:14:02 +0700 Subject: [PATCH 2/2] typo --- include/api.hrl | 4 +- src/mad.erl | 4 +- src/mad_local.erl | 2 +- src/mad_scaffolding.erl | 323 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 328 insertions(+), 5 deletions(-) create mode 100644 src/mad_scaffolding.erl diff --git a/include/api.hrl b/include/api.hrl index f58d03a..d8426c4 100644 --- a/include/api.hrl +++ b/include/api.hrl @@ -1,6 +1,6 @@ -define(MAD,[compile/1,app/1,get/1,man/1,dia/1,release/1,resolve/1,clean/1, start/1,attach/1,stop/1,sh/1,deps/1,up/1,fetch/1,rsa/1,ecc/1, - static/1,eunit/1,strip/1,scafolding/1]). + static/1,eunit/1,strip/1,scaffolding/1]). -type return() :: [] | true | false | {ok,any()} | {error,any()}. @@ -24,4 +24,4 @@ -spec static(list(string())) -> return(). -spec eunit(list(string())) -> return(). -spec strip(list(string())) -> return(). --spec scafolding(list(string())) -> return(). +-spec scaffolding(list(string())) -> return(). diff --git a/src/mad.erl b/src/mad.erl index ea1610f..04db09e 100644 --- a/src/mad.erl +++ b/src/mad.erl @@ -5,7 +5,7 @@ -export([main/1]). main([]) -> halt(help()); -main(["sca"++_|P])-> mad_scafolding:tpl(P); +main(["sca"++_|P])-> mad_scaffolding:tpl(P); main(Params) -> % filter valid (atoms) from invalid (unparsed lists) commands @@ -77,6 +77,6 @@ help() -> info("MAD Manage Dependencies ~s~n",[?VERSION]), info(" cmd = app [nitro|zero] | deps | clean | compile | strip~n"), info(" | bundle [beam|script] | man | repl~n"), info(" | start | stop | attach | static | get | up [name]~n"), - info(" | scafolding tpl= appid= [VARS...]~n"), + info(" | scaffolding tpl= appid= [VARS...]~n"), info(" | [ ca | client | server ]~n"), return(false). diff --git a/src/mad_local.erl b/src/mad_local.erl index 653d59d..e027afe 100644 --- a/src/mad_local.erl +++ b/src/mad_local.erl @@ -22,4 +22,4 @@ up(Params) -> mad_git:up(Params). fetch(Params) -> mad_git:fetch(Params). eunit(Params) -> mad_eunit:main_test(Params). sh(Params) -> mad_repl:sh(Params). -scafolding(Params)-> mad_scafolding:tpl(Params). \ No newline at end of file +scaffolding(Params)-> mad_scaffolding:tpl(Params). \ No newline at end of file diff --git a/src/mad_scaffolding.erl b/src/mad_scaffolding.erl new file mode 100644 index 0000000..0b981df --- /dev/null +++ b/src/mad_scaffolding.erl @@ -0,0 +1,323 @@ +-module(mad_scaffolding). +-copyright('Chan Sisowath'). +-compile(export_all). + +tpl(Params) -> + mad_repl:load(), + Apps = ets:tab2list(filesystem), + Vars = decode_params(Params), + case proplists:get_value(tpl, Vars) of + undefined -> + mad:info("please specify a template: scaffolding tpl= appid=...~n",[]),true; + TplName -> + AppName = proplists:get_value(appid, Vars), + mad:info("AppId = ~p~n",[AppName]), + mad:info("TplName = ~p~n",[TplName]), + mad:info("Vars = ~p~n",[Vars]), + case proplists:get_value("priv/" ++ TplName ++ ".skel", Apps) of + undefined -> case getskel(TplName) of + {error,F} -> mad:info("unable to load the template ~p.~n",[F]),true; + Bin -> TemplateTerms = consult(Bin), + Vars1 = lists:keyreplace(name, 1, Vars, {appid, AppName}), + create({dir,tpldir()}, TemplateTerms, Vars1) + end; + + Skel -> TemplateTerms = consult(Skel), + Vars1 = lists:keyreplace(name, 1, Vars, {appid, AppName}), + create({mem,Apps}, TemplateTerms, Vars1) + end + end. + +tpldir() -> case os:get_env_var("MAD_TEMPLATE_DIR") of + false -> filename:join([os:get_env_var("HOME"), ".mad", "templates"]); + Dir -> Dir end. + +getskel(TplName) -> + F = filename:join([tpldir(),TplName++".skel"]), + case file:read_file(F) of {error,_} -> {error,F}; {ok,Bin} -> Bin end. + +lib(_) -> ok. + +decode_params(Params) -> + decode_params(Params, []). + +decode_params([], Acc) -> Acc; +decode_params([H|T], Acc) -> + [K, V] = string:tokens(H, "="), + decode_params(T, [{list_to_atom(K), V}|Acc]). + +create(Src, FinalTemplate, VarsCtx) -> + case lists:keyfind(variables, 1, FinalTemplate) of + {variables, Vars} -> + case parse_vars(Vars, dict:new()) of + {error, _Entry} -> + Context0 = undefined; + Context0 -> + ok + end; + false -> + Context0 = dict:new() + end, + %%Variables = lists:keyreplace(appid, 1, dict:to_list(Context0), {appid, Name}), + Name = proplists:get_value(appid, VarsCtx), + Variables = dict:to_list(Context0) ++ VarsCtx, + Context = resolve_variables(Variables, Context0), + Force = "1", + %io:format("Context ~p~n", [[ begin {_, Y} = dict:find(X, Context), {X, Y} end|| X <- dict:fetch_keys(Context)]] ), + execute_template(Src, FinalTemplate, none, Name, Context, Force, []), + {ok,Name}. + +% -------------------------------------------------------------------------------- +% internal +% -------------------------------------------------------------------------------- + +-spec consult(string() | binary()) -> [term()]. +consult(Source) -> + + SourceStr = to_str(Source), + {ok, Tokens, _} = erl_scan:string(SourceStr), + Forms = split_when(fun is_dot/1, Tokens), + ParseFun = fun (Form) -> + case erl_parse:parse_exprs(Form) of + {ok, Expr} -> Expr; + E -> mad:info("Error mad_scaffolding ~p", [E]), E end + end, + Parsed = lists:map(ParseFun, Forms), + ExprsFun = fun(P) -> + {value, Value, _} = erl_eval:exprs(P, []), + Value + end, + lists:map(ExprsFun, Parsed). + +-spec split_when(fun(), list()) -> list(). +split_when(When, List) -> + split_when(When, List, [[]]). + +split_when(When, [], [[] | Results]) -> + split_when(When, [], Results); +split_when(_When, [], Results) -> + Reversed = lists:map(fun lists:reverse/1, Results), + lists:reverse(Reversed); +split_when(When, [Head | Tail], [Current0 | Rest]) -> + Current = [Head | Current0], + Result = case When(Head) of + true -> + [[], Current | Rest]; + false -> + [Current | Rest] + end, + split_when(When, Tail, Result). + +-spec is_dot(tuple()) -> boolean(). +is_dot({dot, _}) -> true; +is_dot(_) -> false. + +-spec to_str(binary() | list() | atom()) -> string(). +to_str(Arg) when is_binary(Arg) -> + unicode:characters_to_list(Arg); +to_str(Arg) when is_atom(Arg) -> + atom_to_list(Arg); +to_str(Arg) when is_integer(Arg) -> + integer_to_list(Arg); +to_str(Arg) when is_list(Arg) -> + Arg. + +%% +%% Given a list of key value pairs, for each string value attempt to +%% render it using Dict as the context. Storing the result in Dict as Key. +%% +resolve_variables([], Dict) -> + Dict; +resolve_variables([{Key, Value0} | Rest], Dict) when is_list(Value0) -> + Value = render(list_to_binary(Value0), Dict), + resolve_variables(Rest, dict:store(Key, Value, Dict)); +resolve_variables([{Key, {list, Dicts}} | Rest], Dict) when is_list(Dicts) -> + %% just un-tag it so mustache can use it + resolve_variables(Rest, dict:store(Key, Dicts, Dict)); +resolve_variables([_Pair | Rest], Dict) -> + resolve_variables(Rest, Dict). + +%% +%% Render a binary to a string, using erlydtl and the specified context +%% +render(Bin, Context) -> + %% Be sure to escape any double-quotes before rendering... + ReOpts = [global, {return, list}], + Str0 = re:replace(Bin, "\\\\", "\\\\\\", ReOpts), + Str1 = re:replace(Str0, "\"", "\\\\\"", ReOpts), + mad_mustache:render(Str1, Context). + +%% =================================================================== +%% Internal functions +%% =================================================================== + +%% +%% Read the contents of a file from the appropriate source +%% +load_file({dir,Dir}, _, Name) -> + case file:read_file(filename:join(Dir,Name)) of + {_, Bin} -> Bin; + _ -> ok + end; +load_file({mem,Files}, _, Name) -> + case lists:keyfind("priv/" ++ Name, 1, Files) of + {_, Bin} -> Bin; + _ -> ok + end. + +%% +%% Parse/validate variables out from the template definition +%% +parse_vars([], Dict) -> + Dict; +parse_vars([{Key, Value} | Rest], Dict) when is_atom(Key) -> + parse_vars(Rest, dict:store(Key, Value, Dict)); +parse_vars([Other | _Rest], _Dict) -> + {error, Other}; +parse_vars(Other, _Dict) -> + {error, Other}. + +maybe_dict({Key, {list, Dicts}}) -> + %% this is a 'list' element; a list of lists representing dicts + {Key, {list, [dict:from_list(D) || D <- Dicts]}}; +maybe_dict(Term) -> + Term. + +write_file(Output, Data, Force) -> + %% determine if the target file already exists + FileExists = filelib:is_regular(Output), + + %% perform the function if we're allowed, + %% otherwise just process the next template + case Force =:= "1" orelse FileExists =:= false of + true -> + ok = filelib:ensure_dir(Output), + case {Force, FileExists} of + {"1", true} -> + mad:info("Writing ~s (forcibly overwriting)~n", + [Output]); + _ -> + mad:info("Writing ~s~n", [Output]) + end, + case file:write_file(Output, Data) of + ok -> + ok; + {error, Reason} -> + mad:info("Failed to write output file ~p: ~p\n", + [Output, Reason]) + end; + false -> + {error, exists} + end. + +execute_template(_Files, [], _TemplateType, _TemplateName, + _Context, _Force, ExistingFiles) -> + case ExistingFiles of + [] -> + ok; + _ -> + Msg = lists:flatten([io_lib:format("\t* ~p~n", [F]) || + F <- lists:reverse(ExistingFiles)]), + Help = "To force overwriting, specify -f/--force/force=1" + " on the command line.\n", + mad:info("One or more files already exist on disk and " + "were not generated:~n~s~s", [Msg , Help]) + end; + +execute_template(Files, [{template, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + File = load_file(Files, TemplateType, Input), + OutputName = render(Output, Context), + %io:format("template outputName ~p~n",[OutputName]), + case write_file(OutputName, render(File, Context), Force) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, exists} -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, [Output|ExistingFiles]) + end; +execute_template(Files, [{file, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + File = load_file(Files, TemplateType, Input), + OutputName = render(Output, Context), + case write_file(OutputName, File, Force) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, exists} -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, [Output|ExistingFiles]) + end; +execute_template(Files, [{dir, InputName} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + Name = render(InputName, Context), + case filelib:ensure_dir(filename:join(Name, "dummy")) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{dir, ~s}: ~p\n", [Name, Reason]) + end; +execute_template(Files, [{copy, Input, Output} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + InputName = filename:join(filename:dirname(TemplateName), Input), + try cp_r([InputName ++ "/*"], Output) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles) + catch _:_ -> + io:format("Failed while processing template instruction " + "{copy, ~s, ~s}~n", [Input, Output]) + end; +execute_template(Files, [{chmod, Mod, File} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) + when is_integer(Mod) -> + FileName = render(File, Context), + case file:change_mode(FileName, Mod) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{chmod, ~b, ~s}: ~p~n", [Mod, FileName, Reason]) + end; +execute_template(Files, [{symlink, Existing, New} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + case file:make_symlink(Existing, New) of + ok -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); + {error, Reason} -> + io:format("Failed while processing template instruction " + "{symlink, ~s, ~s}: ~p~n", [Existing, New, Reason]) + end; +execute_template(Files, [{variables, _} | Rest], TemplateType, + TemplateName, Context, Force, ExistingFiles) -> + execute_template(Files, Rest, TemplateType, TemplateName, + Context, Force, ExistingFiles); +execute_template(Files, [Other | Rest], TemplateType, TemplateName, + Context, Force, ExistingFiles) -> + io:format("Skipping unknown template instruction: ~p\n", [Other]), + execute_template(Files, Rest, TemplateType, TemplateName, Context, + Force, ExistingFiles). + + +-spec cp_r(list(string()), file:filename()) -> 'ok'. +cp_r([], _Dest) -> + ok; +cp_r(Sources, Dest) -> + case os:type() of + {unix, _} -> + EscSources = [escape_path(Src) || Src <- Sources], + SourceStr = string:join(EscSources, " "), + {done, 0, <<>>} = sh:run(["cp", "-R", SourceStr, Dest]), + ok; + {win32, _} -> + io:format("unsuported os."), + ok + end. + +escape_path(Str) -> + re:replace(Str, "([ ()?])", "\\\\&", [global, {return, list}]).