diff --git a/src/gpb_compile.erl b/src/gpb_compile.erl
index fa8e1830..2da2ca87 100644
--- a/src/gpb_compile.erl
+++ b/src/gpb_compile.erl
@@ -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) |
@@ -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()} |
@@ -399,6 +402,10 @@ file(File) ->
%% {@link name_part()}},
%% {module_name
%% {@link new_name()}},
+%% {enum_macro_prefix,
+%% {@link name_part()}},
+%% {enum_macro_suffix,
+%% {@link name_part()}},
%%
%%
What to generate and how
%% use_packages,
@@ -418,6 +425,7 @@ file(File) ->
%% {@link target_erlang_version()}},
%% preserve_unknown_fields,
+%% gen_enum_macros,
%% {erlc_compile_options,
%% string()}
%%
@@ -833,6 +841,19 @@ file(File) ->
%% Corresponding command line option:
%% -modname.
%%
+%%
+%%
+%% {enum_macro_prefix, {@link name_part()}}
+%% {enum_macro_suffix, {@link name_part()}}
+%%
+%% 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:
+%% -enum-macro-refix and
+%% -enum-macro-suffix.
+%%
%%
%%
%% What to generate and how
@@ -997,6 +1018,33 @@ file(File) ->
%% -preserve-unknown-fields.
%%
+%% `gen_enum_macros'
+%%
+%% 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 `maps' option.
+%%
+%% See also the `enum_macro_prefix'
+%% and `enum_macro_suffix' options.
+%%
+%% Corresponding command line option:
+%% -gen-enum-macros.
+%%
%%
%% `{erlc_compile_options, string()}'
%%
@@ -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 =
@@ -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)).
@@ -2624,6 +2677,16 @@ c() ->
%% Specify the name of the generated module.
%% Corresponding Erlang-level option:
%% module_name
+%%
+%% `-enum-macro-prefix Prefix'
+%% Prefix each enum-macro with `Prefix'.
+%% Corresponding Erlang-level option:
+%% enum_macro_prefix
+%%
+%% `-enum-macro-suffix Suffix'
+%% Suffix each enum-macro with `Suffix'.
+%% Corresponding Erlang-level option:
+%% enum_macro_suffix
%%
%%
%% What to generate and how
@@ -2701,6 +2764,12 @@ c() ->
%% Corresponding Erlang-level option:
%% preserve_unknown_fields
+%%
+%% `-gen-enum-macros'
+%% Generate macro definitions for enum symbols. Note that this causes
+%% a `.hrl' file to be generated even with the `-maps' option.
+%% Corresponding Erlang-level option:
+%% gen_enum_macros
%%
%% `-erlc_compile_options Options'
%% Specifies compilation options, in a comma separated string, to pass
@@ -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"
@@ -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"},
@@ -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"
@@ -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).
diff --git a/src/gpb_lib.erl b/src/gpb_lib.erl
index 692f61d5..28b5a363 100644
--- a/src/gpb_lib.erl
+++ b/src/gpb_lib.erl
@@ -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]).
@@ -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 ->
diff --git a/test/gpb_compile_tests.erl b/test/gpb_compile_tests.erl
index cccfc93e..53b7a6a5 100644
--- a/test/gpb_compile_tests.erl
+++ b/test/gpb_compile_tests.erl
@@ -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"]),
@@ -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() ->
@@ -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"]}} =