Current section
Files
Jump to
Current section
Files
src/grpcbox_plugin_prv.erl
-module(grpcbox_plugin_prv).
-export([init/1, do/1, format_error/1]).
-include_lib("providers/include/providers.hrl").
-define(PROVIDER, gen).
-define(NAMESPACE, grpc).
-define(DEPS, [{default, app_discovery}]).
%% ===================================================================
%% Public API
%% ===================================================================
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create(
[{name, ?PROVIDER}, % The 'user friendly' name of the task
{namespace, ?NAMESPACE},
{module, ?MODULE}, % The module implementation of the task
{bare, true}, % The task can be run by the user, always true
{deps, ?DEPS}, % The list of dependencies
{example, "rebar3 grpc gen"}, % How to use the plugin
{opts, [{protos, $p, "protos", string, "directory of protos to build"},
{force, $f, "force", boolean, "overwrite already generated modules"},
{type, $t, "type", string, "generate 'client', 'server' or 'all'"}]},
{short_desc, "Generates behaviours for grpc services"},
{desc, "Generates behaviours for grpc services"}]),
{ok, rebar_state:add_provider(State, Provider)}.
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
Apps = case rebar_state:current_app(State) of
undefined ->
rebar_state:project_apps(State);
AppInfo ->
[AppInfo]
end,
{Options, _} = rebar_state:command_parsed_args(State),
lists:foreach(fun(AppInfo) -> handle_app(AppInfo, Options, State) end, Apps),
{ok, State}.
-spec format_error(any()) -> iolist().
format_error({compile_errors, Errors}) ->
[[io_lib:format("Error building ~s~n", [File]) |
[io_lib:format(" ~p: ~s", [Line, M:format_error(E)])
|| {Line, M, E} <- Es]]
|| {File, Es} <- Errors];
format_error({gpb, File, Error}) ->
io_lib:format("Error compiling proto file ~s ~s", [filename:basename(File),
gpb_compile:format_error(Error)]);
format_error(Reason) ->
io_lib:format("~p", [Reason]).
handle_app(AppInfo, Options, State) ->
Opts = rebar_app_info:opts(AppInfo),
BeamOutDir = rebar_app_info:ebin_dir(AppInfo),
GrpcOpts = rebar_opts:get(Opts, grpc, []),
GpbOpts = proplists:get_value(gpb_opts, GrpcOpts, []),
BaseDir = rebar_app_info:dir(AppInfo),
GrpcOptOutDir = proplists:get_value(out_dir, GrpcOpts, filename:join(BaseDir, "src")),
GrpcOutDir = filename:join(BaseDir, GrpcOptOutDir),
GpbOutDir = filename:join(BaseDir, proplists:get_value(o, GpbOpts, GrpcOptOutDir)),
ProtosDirs = case proplists:get_all_values(protos, Options) of
[] ->
case proplists:get_value(protos, GrpcOpts, [filename:join("priv", "protos")]) of
[H | _] = Ds when is_list(H) ->
Ds;
D ->
[D]
end;
Ds ->
Ds
end,
ProtoFiles = lists:append([filelib:wildcard(filename:join([BaseDir, D, "*.proto"])) || D <- ProtosDirs]),
Type = case proplists:get_value(type, Options, undefined) of
undefined ->
proplists:get_value(type, GrpcOpts, all);
T when T =:= "all" orelse T =:= "client" ->
T
end,
Templates = templates(Type),
ProtoModules = [compile_pb(Filename, GpbOutDir, BeamOutDir, GpbOpts) || Filename <- ProtoFiles],
[gen_services(Templates, ProtoModule, ProtoBeam, GrpcOutDir, GrpcOpts, State)
|| {ProtoModule, ProtoBeam} <- ProtoModules],
ok.
compile_pb(Filename, OutDir, BeamOutDir, GpbOpts) ->
ModuleName = lists:flatten(
[proplists:get_value(module_name_prefix, GpbOpts, ""),
filename:basename(Filename, ".proto"),
proplists:get_value(module_name_suffix, GpbOpts, "")]),
GeneratedPB = filename:join(OutDir, ModuleName ++ ".erl"),
CompiledPB = filename:join(OutDir, ModuleName ++ ".beam"),
ok = filelib:ensure_dir(GeneratedPB),
case needs_update(Filename, GeneratedPB) of
true ->
rebar_log:log(info, "Writing ~s", [GeneratedPB]),
case gpb_compile:file(Filename, [{rename,{msg_name,snake_case}},
{rename,{msg_fqname,base_name}},
use_packages, maps,
strings_as_binaries, {i, "."},
{report_errors, false},
{o, OutDir} | GpbOpts]) of
ok ->
ok;
{error, Error} ->
erlang:error(?PRV_ERROR({gpb, Filename, Error}))
end;
false ->
ok
end,
case needs_update(GeneratedPB, CompiledPB) of
true ->
GpbIncludeDir = filename:join(code:lib_dir(gpb), "include"),
case compile:file(GeneratedPB, [{outdir, BeamOutDir}, {i, GpbIncludeDir}, return_errors]) of
{ok, _} ->
ok;
{ok, _, Warnings} ->
log_warnings(Warnings),
ok;
{error, Errors, Warnings} ->
log_warnings(Warnings),
throw(?PRV_ERROR({compile_errors, Errors}))
end;
false ->
ok
end,
{module, Module} = code:load_abs(filename:join(BeamOutDir, ModuleName)),
{Module, CompiledPB}.
gen_services(Templates, ProtoModule, ProtoBeam, OutDir, GrpcConfig, State) ->
ServiceDefs = [gen_service_def(S, ProtoModule, GrpcConfig, OutDir)
|| S <- ProtoModule:get_service_names()],
WithTemplates = [{S, TemplateSuffix, TemplateName}
|| S <- ServiceDefs, {TemplateSuffix, TemplateName} <- Templates],
Services = lists:filter(fun(S) -> filter_outdated(S, OutDir, ProtoBeam) end, WithTemplates),
rebar_log:log(debug, "services: ~p", [Services]),
[rebar_templater:new(TemplateName, maps:to_list(Service), true, State)
|| {Service, _, TemplateName} <- Services].
gen_service_def(Service, ProtoModule, GrpcConfig, FullOutDir) ->
ServiceModules = proplists:get_value(service_modules, GrpcConfig, []),
ServicePrefix = proplists:get_value(prefix, GrpcConfig, ""),
ServiceSuffix = proplists:get_value(suffix, GrpcConfig, ""),
{{_, Name}, Methods} = ProtoModule:get_service_def(Service),
ModuleName = proplists:get_value(Name, ServiceModules, list_snake_case(atom_to_list(Name))),
#{out_dir => FullOutDir,
pb_module => atom_to_list(ProtoModule),
unmodified_service_name => atom_to_list(Name),
module_name => ServicePrefix ++ ModuleName ++ ServiceSuffix,
methods => [resolve_method(M, ProtoModule) || M <- Methods]}.
resolve_method(Method, ProtoModule) ->
MessageType = {message_type, ProtoModule:msg_name_to_fqbin(maps:get(input, Method))},
MethodData = lists:flatmap(fun normalize_method_opt/1, maps:to_list(Method)),
[MessageType | MethodData].
filter_outdated({#{module_name := ModuleName}, TemplateSuffix, _}, OutDir, ProtoBeam) ->
ModulePath = filename:join([OutDir, ModuleName ++ "_" ++ TemplateSuffix ++ ".erl"]),
ok = filelib:ensure_dir(ModulePath),
needs_update(ProtoBeam, ModulePath).
templates(S) when is_list(S) ->
templates(list_to_existing_atom(S));
templates(all) ->
[{"client", "grpcbox_service_client"},
{"bhvr", "grpcbox_service_bhvr"}];
templates(client) ->
[{"client", "grpcbox_service_client"}];
templates(server) ->
[{"bhvr", "grpcbox_service_bhvr"}].
normalize_method_opt({opts, _}) ->
[];
normalize_method_opt({name, Name}) ->
StrName = atom_to_list(Name),
[{method, list_snake_case(StrName)},
{unmodified_method, StrName}];
normalize_method_opt({K, V}) when V =:= true; V =:= false ->
[{K, V}];
normalize_method_opt({K, V}) ->
[{K, atom_to_list(V)}].
list_snake_case(NameString) ->
Snaked = lists:foldl(
fun(RE, Snaking) ->
re:replace(Snaking, RE, "\\1_\\2", [{return, list}, global])
end,
NameString,
[%% uppercase followed by lowercase
"(.)([A-Z][a-z]+)",
%% any consecutive digits
"(.)([0-9]+)",
%% uppercase with lowercase
%% or digit before it
"([a-z0-9])([A-Z])"]),
Snaked1 = string:replace(Snaked, ".", "_", all),
Snaked2 = string:replace(Snaked1, "__", "_", all),
string:to_lower(unicode:characters_to_list(Snaked2)).
needs_update(Source, Artifact) ->
filelib:last_modified(Source) > filelib:last_modified(Artifact).
log_warnings(Warnings) ->
[begin
rebar_api:warn("Warning building ~s~n", [File]),
[rebar_api:warn(" ~p: ~s", [Line, M:format_error(E)]) || {Line, M, E} <- Es]
end || {File, Es} <- Warnings].