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"]}} =