Skip to content

Commit

Permalink
Add options to generate macros for enums
Browse files Browse the repository at this point in the history
The intention is to be able to catch errors already at compile-time
if the enum symbols would change in a future version of the proto.
  • Loading branch information
tomas-abrahamsson committed Sep 10, 2023
1 parent 31f9368 commit 600516b
Show file tree
Hide file tree
Showing 3 changed files with 172 additions and 11 deletions.
112 changes: 101 additions & 11 deletions src/gpb_compile.erl
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,8 @@
{module_name_prefix, name_part()} |
{module_name_suffix, name_part()} |
{module_name, new_name()} |
{enum_macro_prefix, name_part()} |
{enum_macro_suffix, name_part()} |
%% What to generate and how
boolean_opt(use_packages) |
boolean_opt(descriptor) |
Expand All @@ -160,6 +162,7 @@
boolean_opt(type_defaults_for_omitted_optionals) |
{target_erlang_version, target_erlang_version()} |
boolean_opt(preserve_unknown_fields) |
boolean_opt(gen_enum_macros) |
{erlc_compile_options, string()} |
%% Introspection of the proto definitions
{proto_defs_version, gpb_defs:version()} |
Expand Down Expand Up @@ -399,6 +402,10 @@ file(File) ->
%% {@link name_part()}}</tt>,
%% <tt>{<a href="#option-module_name">module_name</a>
%% {@link new_name()}}</tt>,
%% <tt>{<a href="#option-enum_macro_prefix">enum_macro_prefix</a>,
%% {@link name_part()}}</tt>,
%% <tt>{<a href="#option-enum_macro_suffix">enum_macro_suffix</a>,
%% {@link name_part()}}</tt>,
%% </dd>
%% <dt>What to generate and how</dt>
%% <dd><tt><a href="#option-use_packages">use_packages</a></tt>,
Expand All @@ -418,6 +425,7 @@ file(File) ->
%% {@link target_erlang_version()}}</tt>,
%% <tt><a href="#option-preserve_unknown_fields"
%% >preserve_unknown_fields</a></tt>,
%% <tt><a href="#option-gen_enum_macros">gen_enum_macros</a></tt>,
%% <tt>{<a href="#option-erlc_compile_options">erlc_compile_options</a>,
%% string()}</tt>
%% <br/>
Expand Down Expand Up @@ -833,6 +841,19 @@ file(File) ->
%% Corresponding command line option:
%% <a href="#cmdline-option-modname">-modname</a>.
%%
%% <h4><a id="option-enum_macro_prefix"/>
%% <a id="option-enum_macro_suffix"/>
%% <tt>{enum_macro_prefix, {@link name_part()}}</tt><br/>
%% <tt>{enum_macro_suffix, {@link name_part()}}</tt></h4>
%%
%% The `{enum_macro_prefix,Prefix}' will add `Prefix' (a string or an atom)
%% to the generated enum macros in the hrl file. `{enum_macro_suffix,Suffix}'
%% works correspondingly.
%%
%% Corresponding command line options:
%% <a href="#cmdline-option-enum-macro-prefix">-enum-macro-refix</a> and
%% <a href="#cmdline-option-enum-macro-suffix">-enum-macro-suffix</a>.
%%
%% <!-- ======================================================== -->
%% <h3><a id="optionsection-generate"/>
%% What to generate and how
Expand Down Expand Up @@ -997,6 +1018,33 @@ file(File) ->
%% <a href="#cmdline-option-preserve-unknown-fields"
%% >-preserve-unknown-fields</a>.
%%
%% <h4><a id="option-gen_enum_macros"/>`gen_enum_macros'</h4>
%%
%% The `gen_enum_macros' option causes macros to be emitted on the form
%% indicated by the following example:
%% ```
%% x.proto:
%% syntax="proto3";
%% message Msg {
%% enum Status { NOT_SET = 0; FAILURE = 1; SUCCESS = 2; }
%% Status f = 1;
%% }
%% x.hrl:
%% -define('Msg.Status.NOT_SET', 'NOT_SET').
%% -define('Msg.Status.FAILURE', 'FAILURE').
%% -define('Msg.Status.SUCCESS', 'SUCCESS').
%% '''
%% The intention is to make it possible to catch errors already at compile-time
%% if any enum symbol would get renamed in a future version of the proto file.
%% Note that this option will cause `.hrl' files to be generated, even with
%% the <a href="#option-maps">`maps'</a> option.
%%
%% See also the <a href="#option-enum_macro_prefix">`enum_macro_prefix'</a>
%% and <a href="#option-enum_macro_suffix">`enum_macro_suffix'</a> options.
%%
%% Corresponding command line option:
%% <a href="#cmdline-option-gen-enum-macros">-gen-enum-macros</a>.
%%
%% <h4><a id="option-erlc_compile_options"/>
%% `{erlc_compile_options, string()}'</h4>
%%
Expand Down Expand Up @@ -2149,10 +2197,10 @@ get_output_files(Mod, Opts) ->
NifCcOutDir = get_nif_cc_outdir(Opts),
Erl = filename:join(ErlOutDir, atom_to_list(Mod) ++ ".erl"),
Hrl =
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records ->
case get_gen_hrl_file(Opts) of
true ->
filename:join(HrlOutDir, atom_to_list(Mod) ++ ".hrl");
maps ->
false ->
'$not_generated'
end,
NifCc =
Expand All @@ -2164,6 +2212,11 @@ get_output_files(Mod, Opts) ->
end,
{Erl, Hrl, NifCc}.

get_gen_hrl_file(Opts) ->
Mapping = gpb_lib:get_records_or_maps_by_opts(Opts),
DoEnumMacros = gpb_lib:get_enum_macros_by_opts(Opts),
Mapping == records orelse DoEnumMacros.

get_erl_outdir(Opts) ->
proplists:get_value(o_erl, Opts, get_outdir(Opts)).

Expand Down Expand Up @@ -2624,6 +2677,16 @@ c() ->
%% <dd>Specify the name of the generated module.<br/>
%% Corresponding Erlang-level option:
%% <a href="#option-module_name">module_name</a></dd>
%% <dt><a id="cmdline-option-enum-macro-prefix"/>
%% `-enum-macro-prefix Prefix'</dt>
%% <dd>Prefix each enum-macro with `Prefix'.<br/>
%% Corresponding Erlang-level option:
%% <a href="#option-enum_macro_prefix">enum_macro_prefix</a></dd>
%% <dt><a id="cmdline-option-enum-macro-suffix"/>
%% `-enum-macro-suffix Suffix'</dt>
%% <dd>Suffix each enum-macro with `Suffix'.<br/>
%% Corresponding Erlang-level option:
%% <a href="#option-enum_macro_suffix">enum_macro_suffix</a></dd>
%% </dl>
%%
%% What to generate and how
Expand Down Expand Up @@ -2701,6 +2764,12 @@ c() ->
%% Corresponding Erlang-level option:
%% <a href="#option-preserve_unknown_fields"
%% >preserve_unknown_fields</a></dd>
%% <dt><a id="cmdline-option-gen-enum-macros"/>
%% `-gen-enum-macros'</dt>
%% <dd>Generate macro definitions for enum symbols. Note that this causes
%% a `.hrl' file to be generated even with the `-maps' option.<br/>
%% Corresponding Erlang-level option:
%% <a href="#option-gen_enum_macros">gen_enum_macros</a></dd>
%% <dt><a id="cmdline-option-erlc_compile_options"/>
%% `-erlc_compile_options Options'</dt>
%% <dd>Specifies compilation options, in a comma separated string, to pass
Expand Down Expand Up @@ -3276,6 +3345,10 @@ opt_specs() ->
" Suffix the module name with Suffix.\n"},
{"modname", 'string()', module_name, "Name\n"
" Specify the name of the generated module.\n"},
{"enum-macro-prefix", 'string()', enum_macro_prefix, "Prefix\n"
" Prefix the enum macros with Prefix.\n"},
{"enum-macro-suffix", 'string()', enum_macro_suffix, "Suffix\n"
" Suffix the enum macros with Suffix.\n"},
{{section, "What to generate and how"}},
{"pkgs", undefined, use_packages, "\n"
" Prepend the name of a package to every message it contains.\n"
Expand Down Expand Up @@ -3312,6 +3385,9 @@ opt_specs() ->
" Generate code for Erlang/OTP version N instead of current.\n"},
{"preserve-unknown-fields", undefined, preserve_unknown_fields, "\n"
" Preserve unknown fields.\n"},
{"gen-enum-macros", undefined, gen_enum_macros, "\n"
" Generate macro definitions for enum symbols. Note that this\n"
" causes a .hrl file to be generated even with the -maps option.\n"},
{"erlc_compile_options", 'string()', erlc_compile_options, "String\n"
" Specifies compilation options, in a comma separated string, to\n"
" pass along to the -compile() directive on the generated code.\n"},
Expand Down Expand Up @@ -4466,13 +4542,17 @@ possibly_format_descriptor(Defs, Opts) ->
%% -- hrl -----------------------------------------------------

possibly_format_hrl(Mod, Defs, AnRes, Opts) ->
case gpb_lib:get_records_or_maps_by_opts(Opts) of
records -> format_hrl(Mod, Defs, AnRes, Opts);
maps -> '$not_generated'
case get_gen_hrl_file(Opts) of
true -> format_hrl(Mod, Defs, AnRes, Opts);
false -> '$not_generated'
end.

format_hrl(Mod, Defs, AnRes, Opts1) ->
Opts = [{module, Mod}|Opts1],
format_hrl(Mod, Defs, AnRes, Opts0) ->
Opts = [{module, Mod} | Opts0],
Mapping = gpb_lib:get_records_or_maps_by_opts(Opts),
DoEnumMacros = gpb_lib:get_enum_macros_by_opts(Opts),
EnumPrefix = gpb_lib:get_enum_macro_prefix_by_opts(Opts),
EnumSuffix = gpb_lib:get_enum_macro_suffix_by_opts(Opts),
ModVsn = list_to_atom(atom_to_list(Mod) ++ "_gpb_version"),
gpb_lib:iolist_to_utf8_or_escaped_binary(
[?f("%% Automatically generated, do not edit~n"
Expand All @@ -4484,9 +4564,19 @@ format_hrl(Mod, Defs, AnRes, Opts1) ->
"\n",
?f("-define(~p, \"~s\").~n", [ModVsn, gpb:version_as_string()]),
"\n",
gpb_lib:nl_join(
[gpb_gen_types:format_msg_record(Msg, Fields, AnRes, Opts, Defs)
|| {_,Msg,Fields} <- gpb_lib:msgs_or_groups(Defs)]),
[gpb_lib:nl_join(
[[?f("-define(~p, ~p).~n",
[list_to_atom(
lists:concat([EnumPrefix, EnumName, '.', Sym, EnumSuffix])),
Sym])
|| {Sym, _EValue, _EOpts} <- EnumDef]
|| {{enum, EnumName}, EnumDef} <- Defs])
|| DoEnumMacros],
"\n",
[gpb_lib:nl_join(
[gpb_gen_types:format_msg_record(Msg, Fields, AnRes, Opts, Defs)
|| {_,Msg,Fields} <- gpb_lib:msgs_or_groups(Defs)])
|| Mapping == records],
"\n",
?f("-endif.~n")],
Opts).
Expand Down
12 changes: 12 additions & 0 deletions src/gpb_lib.erl
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,9 @@
-export([get_defs_as_maps_or_records/1]).
-export([get_epb_functions_by_opts/1]).
-export([get_bypass_wrappers_by_opts/1]).
-export([get_enum_macros_by_opts/1]).
-export([get_enum_macro_prefix_by_opts/1]).
-export([get_enum_macro_suffix_by_opts/1]).
-export([is_target_major_version_at_least/2]).
-export([target_has_lists_join/1]).
-export([target_has_variable_key_map_update/1]).
Expand Down Expand Up @@ -703,6 +706,15 @@ get_epb_functions_by_opts(Opts) ->
get_bypass_wrappers_by_opts(Opts) ->
proplists:get_bool(bypass_wrappers, Opts).

get_enum_macros_by_opts(Opts) ->
proplists:get_bool(gen_enum_macros, Opts).

get_enum_macro_prefix_by_opts(Opts) ->
proplists:get_value(enum_macro_prefix, Opts, "").

get_enum_macro_suffix_by_opts(Opts) ->
proplists:get_value(enum_macro_suffix, Opts, "").

is_target_major_version_at_least(VsnMin, Opts) ->
case proplists:get_value(target_erlang_version, Opts, current) of
current ->
Expand Down
59 changes: 59 additions & 0 deletions test/gpb_compile_tests.erl
Original file line number Diff line number Diff line change
Expand Up @@ -2643,6 +2643,15 @@ list_io_with_nif_options_includes_nif_cc_output_test() ->
do_list_io_defs(FileSystem, [nif]),
ok.

list_io_with_gen_enum_macros_and_maps_test() ->
FileSystem = [{"/main.proto", ["message M { optional uint32 f = 1; }"]}],
[{erl_output, "/main.erl"},
{hrl_output, "/main.hrl"},
{sources, ["/main.proto"]},
{missing, []}] =
do_list_io_defs(FileSystem, [maps, gen_enum_macros]),
ok.

generates_makefile_deps_to_stdout_test() ->
MainProto = lf_lines(["import 'a.proto';",
"message M { required uint32 f = 1; }\n"]),
Expand Down Expand Up @@ -3407,6 +3416,47 @@ defaults_for_proto3_fields_test() ->
{m, undefined, undefined, undefined, [], undefined, []} = P2M:new_m_msg(),
unload_code(P2M).

enum_macros_test() ->
Proto1 = ["package foo.bar;
enum A { NO = 0; YES = 1; }
enum B { option allow_alias=true;
FALSE = 0; TRUE = 1; YES = 1; }
message M { optional A f1 = 1; optional B f2 = 2; }
"],
%% Basic contains
Hrl1 = compile_to_string_get_hrl(Proto1, [gen_enum_macros]),
assert_contains_regexp(Hrl1, "-define.'A.NO', *'NO'"),
assert_contains_regexp(Hrl1, "-define.'A.YES', *'YES'"),
assert_contains_regexp(Hrl1, "-define.'B.FALSE', *'FALSE'"),
assert_contains_regexp(Hrl1, "-define.'B.TRUE', *'TRUE'"),
assert_contains_regexp(Hrl1, "-define.'B.YES', *'YES'"),
%%
%% Use packages
Hrl2 = compile_to_string_get_hrl(Proto1, [gen_enum_macros, use_packages]),
assert_contains_regexp(Hrl2, "-define.'foo.bar.A.NO', *'NO'"),
%% Prefix and suffix (string)
Hrl3 = compile_to_string_get_hrl(Proto1, [gen_enum_macros,
{enum_macro_prefix, "abc/"},
{enum_macro_suffix, "/xyz"}]),
assert_contains_regexp(Hrl3, "-define.'abc/A.NO/xyz', *'NO'"),
%% Prefix and suffix (atom)
Hrl3 = compile_to_string_get_hrl(Proto1, [gen_enum_macros,
{enum_macro_prefix, 'abc/'},
{enum_macro_suffix, '/xyz'}]),
assert_contains_regexp(Hrl3, "-define.'abc/A.NO/xyz', *'NO'"),
ok.


-ifndef(NO_HAVE_MAPS).
enum_macros_means_hrl_even_with_maps_test() ->
Proto = ["enum A { NO = 0; YES = 1; }
message M { optional A f1 = 1; }
"],
Hrl = compile_to_string_get_hrl(Proto, [maps, gen_enum_macros]),
assert_contains_regexp(Hrl, "-define.'A.YES', *'YES'"),
ok.
-endif. % NO_HAVE_MAPS

%% --- nif generation tests -----------------

generates_nif_as_binary_and_file_test() ->
Expand Down Expand Up @@ -5443,6 +5493,15 @@ preserve_unknown_fields_cmdline_opts_test() ->
gpb_compile:parse_opts_and_args(["-preserve-unknown-fields",
"x.proto"]).

gen_enum_macros_cmdline_opts_test() ->
{ok, {[gen_enum_macros,
{enum_macro_prefix, "a-"},
{enum_macro_suffix, "-z"}], ["x.proto"]}} =
gpb_compile:parse_opts_and_args(["-gen-enum-macros",
"-enum-macro-prefix", "a-",
"-enum-macro-suffix", "-z",
"x.proto"]).

verify_decode_required_present_cmdline_opts_test() ->
{ok, {[verify_decode_required_present],
["x.proto"]}} =
Expand Down

0 comments on commit 600516b

Please sign in to comment.