Current section

Files

Jump to
gpb src gpb_gen_types.erl
Raw

src/gpb_gen_types.erl

%%% Copyright (C) 2017 Tomas Abrahamsson
%%%
%%% Author: Tomas Abrahamsson <tab@lysator.liu.se>
%%%
%%% This library is free software; you can redistribute it and/or
%%% modify it under the terms of the GNU Lesser General Public
%%% License as published by the Free Software Foundation; either
%%% version 2.1 of the License, or (at your option) any later version.
%%%
%%% This library is distributed in the hope that it will be useful,
%%% but WITHOUT ANY WARRANTY; without even the implied warranty of
%%% MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
%%% Lesser General Public License for more details.
%%%
%%% You should have received a copy of the GNU Lesser General Public
%%% License along with this library; if not, write to the Free Software
%%% Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
%%% MA 02110-1301 USA
%%% @doc Generation of type specs and record definitinos.
%%% @private
-module(gpb_gen_types).
-export([format_msg_record/5]).
-export([format_maps_as_msgs_record_defs/1]).
-export([format_enum_typespec/3]).
-export([format_export_types/3]).
-include("../include/gpb.hrl").
-include("gpb_compile.hrl").
%% Collect some type spec info top-level wise instead
%% of needing check opts at many places.
-record(t_env,
{type_specs :: boolean(),
can_do_map_presence :: boolean(),
mapping_and_unset :: records | #maps{},
map_key_type :: atom | binary,
map_type_fields :: '2tuples' | maps,
module :: module(),
nif :: boolean(),
verify_decode_required_present :: boolean()}).
-record(type_text,
{text :: string(),
tag :: atom() | undefined}).
%% List of fields with some info/annotations to be rendered
%% either as record fields
%% or as map associations
-record(field_info, % order sort of from left to right
{field :: #?gpb_field{} | #gpb_oneof{},
elem_path :: [atom() | []] % translation elem path
| undefined, % initially
out_comment :: boolean() % if entire field must be out-commented
| undefined, % initially
name :: string() % field name, single-quoted if needed
| undefined, % initially
default :: string() % for records
| undefined, % if no default
type_sep :: string() % "::" (records) or "=>" or ":=" (maps)
| undefined, % initially
base_type_comment :: string()
| undefined, % if no base info
type_text :: #type_text{} % for #?gpb_field{} fields
| [#type_text{}] % for #gpb_oneof{} fields
| undefined, % if no type specs
or_undefined :: boolean() % whether " | undefined"
| undefined,
comment_chunks :: [string()]}).
format_msg_record(Msg, Fields, AnRes, Opts, Defs) ->
Def = list_to_atom(gpb_lib:uppercase(lists:concat([Msg, "_PB_H"]))),
TEnv = t_env(Opts),
[?f("-ifndef(~p).~n", [Def]),
?f("-define(~p, true).~n", [Def]),
?f("-record(~p,~n", [Msg]),
?f(" {"),
gpb_lib:outdent_first(
format_hfields(Msg, 8+1, Fields, AnRes, Opts, Defs, TEnv)),
"\n",
?f(" }).~n"),
?f("-endif.~n")].
format_maps_as_msgs_record_defs(MapsAsMsgs) ->
[begin
FNames = [atom_to_list(FName) || #?gpb_field{name=FName} <- Fields],
?f("-record(~p,{~s}).~n", [MsgName, gpb_lib:comma_join(FNames)])
end
|| {{msg,MsgName},Fields} <- MapsAsMsgs].
format_export_types(Defs, AnRes, Opts) ->
#t_env{type_specs=TypeSpecs} = TEnv = t_env(Opts),
AllMsgsTypesText = format_all_msgs_types(Defs, AnRes, TEnv),
if not TypeSpecs ->
AllMsgsTypesText;
TypeSpecs ->
iolist_to_binary(
["%% enumerated types\n",
gpb_lib:nl_join([format_enum_typespec(Enum, Enumeration, AnRes)
|| {{enum, Enum}, Enumeration} <- Defs]),
"\n",
?f("-export_type([~s]).",
[gpb_lib:comma_join([format_enum_export(Enum, AnRes)
|| {{enum, Enum}, _} <- Defs])]),
"\n\n",
"%% message types\n",
gpb_lib:nl_join(
[format_record_typespec(Name, Fields, Defs, AnRes, Opts, TEnv)
|| {_, Name, Fields} <- gpb_lib:msgs_or_groups(Defs)]),
"\n",
?f("-export_type([~s]).",
[gpb_lib:comma_join(
["'"++atom_to_list(Name)++"'/0"
|| {_, Name, _} <- gpb_lib:msgs_or_groups(Defs)])]),
"\n",
AllMsgsTypesText])
end.
format_all_msgs_types(Defs, AnRes,
#t_env{type_specs=TypeSpecs,
mapping_and_unset=MappingAndUnset}) ->
MsgNames = gpb_lib:msg_names(Defs),
StrNames = if MsgNames == [] ->
"none()";
MsgNames /= [] ->
gpb_lib:or_join([?f("~p", [Nm]) || Nm <- MsgNames])
end,
StrTypes =
if MsgNames == [] ->
"none()";
not TypeSpecs,
MappingAndUnset == records ->
gpb_lib:or_join([?f("#~p{}", [Nm]) || Nm <- MsgNames]);
not TypeSpecs,
is_record(MappingAndUnset, maps) ->
"map()";
TypeSpecs ->
TypeNames = [rename_msg_type(Name, AnRes) || Name <- MsgNames],
gpb_lib:or_join([?f("~p()", [Nm]) || Nm <- TypeNames])
end,
%% Use "$" to so types won't collide with messages' types.
MsgNamesType = ?f("-type '$msg_name'() :: ~s.~n", [StrNames]),
MsgsType = ?f("-type '$msg'() :: ~s.~n", [StrTypes]),
ExportTypes = "-export_type(['$msg_name'/0, '$msg'/0]).\n",
if MsgNames == [],
not TypeSpecs ->
%% No type specs and no messages: no -spec for eg encode_msg
%% will refer to this, so don't generate anything
"";
TypeSpecs ->
[MsgNamesType,
MsgsType,
ExportTypes];
not TypeSpecs ->
%% The generated encode_msg (eg) will refer to this,
%% but don't export the types.
[MsgNamesType,
MsgsType]
end.
format_enum_typespec(Enum, Enumeration, AnRes) ->
Enum1 = rename_enum_type(Enum, AnRes),
Enumerators = gpb_lib:or_join([?f("~p", [EName])
|| {EName, _, _} <- Enumeration]),
?f("-type ~p() :: ~s.", [Enum1, Enumerators]).
format_enum_export(Enum, AnRes) ->
Enum1 = rename_enum_type(Enum, AnRes),
?f("~p/0", [Enum1]).
format_record_typespec(Msg, Fields, Defs, AnRes, Opts,
#t_env{mapping_and_unset=MappingAndUnset}=TEnv) ->
MsgType = rename_msg_type(Msg, AnRes),
case MappingAndUnset of
records ->
?f("-type ~p() :: #~p{}.~n", [MsgType, Msg]);
#maps{} ->
HFields = format_hfields(Msg, 7 + 1, Fields,
AnRes, Opts, Defs, TEnv),
BType = calc_keytype_override(Fields, TEnv),
if BType == no_override ->
?f("-type ~p() ::~n"
" #{~s~n"
" }.~n",
[MsgType, gpb_lib:outdent_first(HFields)]);
true ->
?f("-type ~p() ::~n"
" #{~s~n" % all fields gets rendered as comments
" ~s~n"
" }.~n",
[MsgType, gpb_lib:outdent_first(HFields), BType])
end
end.
t_env(Opts) ->
TypeSpecs = gpb_lib:get_type_specs_by_opts(Opts),
TypespecsCanIndicateMapItemPresence =
gpb_lib:target_can_specify_map_item_presence_in_typespecs(Opts),
MappingAndUnset = gpb_lib:get_mapping_and_unset_by_opts(Opts),
KeyType = gpb_lib:get_maps_key_type_by_opts(Opts),
MapTypeFieldsRepr = gpb_lib:get_2tuples_or_maps_for_maptype_fields_by_opts(
Opts),
Mod = proplists:get_value(module, Opts),
Nif = proplists:get_bool(nif, Opts),
DecVfy = proplists:get_bool(verify_decode_required_present, Opts),
#t_env{type_specs = TypeSpecs,
can_do_map_presence = TypespecsCanIndicateMapItemPresence,
mapping_and_unset = MappingAndUnset,
map_key_type = KeyType,
map_type_fields = MapTypeFieldsRepr,
module = Mod,
nif = Nif,
verify_decode_required_present = DecVfy}.
calc_keytype_override([], _TEnv) ->
no_override;
calc_keytype_override(_Fields, TEnv) ->
#t_env{map_key_type=KeyType,
mapping_and_unset=#maps{unset_optional=UnsetOpt},
can_do_map_presence=TypespecsCanIndicateMapItemPresence}=TEnv,
case KeyType of
atom ->
no_override;
binary ->
if TypespecsCanIndicateMapItemPresence,
UnsetOpt == present_undefined ->
"binary() := _";
TypespecsCanIndicateMapItemPresence,
UnsetOpt == omitted ->
"binary() => _";
true ->
"binary() => _"
end
end.
format_hfields(MsgName, Indent, Fields, AnRes, Opts, Defs, TEnv) ->
FieldInfos = analyze_field_infos(MsgName, Fields, AnRes, Opts, Defs, TEnv),
RenderedFieldTexts = render_field_infos(FieldInfos, Indent),
Sep = "\n" ++ gpb_lib:indent(Indent, ""),
gpb_lib:string_join(RenderedFieldTexts, Sep).
%% --------------------------------------------------------------
%% Collect info needed later for rendering
%%
analyze_field_infos(MsgName, Fields, AnRes, Opts, Defs, TEnv) ->
FieldInfos0 = [#field_info{field=Field,
comment_chunks = []}
|| Field <- Fields],
lists:foldl(
fun(F, FieldInfos) -> F(FieldInfos) end,
FieldInfos0,
[fun(FIs) -> augment_elem_paths(FIs, MsgName) end,
fun(FIs) -> maybe_expand_oneofs(FIs, TEnv) end,
fun(FIs) -> add_base_type_comment(FIs, TEnv) end,
fun(FIs) -> augment_type_texts(FIs, Defs, AnRes, TEnv) end,
fun(FIs) -> apply_type_transforms(FIs, AnRes, TEnv) end,
fun(FIs) -> augment_type_or_undefined(FIs, TEnv) end,
fun(FIs) -> augment_field_name_strs(FIs) end,
fun(FIs) -> augment_default_values(FIs, Opts, Defs, TEnv) end,
fun(FIs) -> augment_type_sep(FIs, TEnv) end,
fun(FIs) -> augment_out_commentation(FIs, TEnv) end,
fun(FIs) -> augment_occurrence(FIs) end,
fun(FIs) -> augment_field_number(FIs) end]).
-ifndef(NO_HAVE_MAPS).
%% For debugging: add fun dump_field_infos/1 after interesting steps
-compile({nowarn_unused_function, dump_field_infos/1}).
dump_field_infos(FieldInfos) ->
io:format(user, "~nFIs=~p~n",
[[maps:from_list(
lists:zip(record_info(fields, field_info),
tl(tuple_to_list(FI))))
|| FI <- FieldInfos]]),
FieldInfos.
-endif. % -ifndef(NO_HAVE_MAPS).
%% Step:
%% Add elem paths
augment_elem_paths(FieldInfos, MsgName) ->
[FI#field_info{elem_path=[MsgName,gpb_lib:get_field_name(Field)]}
|| #field_info{field=Field}=FI <- FieldInfos].
%% Step:
%% If maps with flat oneofs, expand those to #field_info{field = #?gpb_field{}}
%% elements, with adjusted `elem_path's (for any type spec translations).
%%
%% NB: This step may leave #field_info{field = #gpb_oneof{}} if no flat oneofs.
maybe_expand_oneofs(FieldInfos, TEnv) ->
#t_env{mapping_and_unset=MappingAndUnset} = TEnv,
case MappingAndUnset of
records ->
FieldInfos;
#maps{oneof=flat} ->
expand_oneofs(FieldInfos);
#maps{} ->
FieldInfos
end.
expand_oneofs([F | Rest]) ->
case F of
#field_info{elem_path=BaseElemPath,
field=#gpb_oneof{fields=OFields}} ->
OFieldInfos =
[#field_info{elem_path = BaseElemPath ++ [OFName],
field = OField,
comment_chunks = []}
|| #?gpb_field{name=OFName}=OField <- OFields],
OFieldInfos ++ expand_oneofs(Rest);
_ ->
[F | expand_oneofs(Rest)]
end;
expand_oneofs([]) ->
[].
%% Step:
%% Add a base type-comment.
%% (will be removed later for any type-spec translations)
add_base_type_comment(FieldInfos, TEnv) ->
[FI#field_info{base_type_comment = base_type_comment(Field, TEnv)}
|| #field_info{field=Field}=FI <- FieldInfos].
base_type_comment(#?gpb_field{type=Type}=Field, TEnv) ->
#t_env{type_specs=TypeSpecs,
can_do_map_presence=TypespecsCanIndicateMapItemPresence} = TEnv,
IsMapTypeField = is_map_type_field(Field),
case Type of
sint32 -> "32 bits";
sint64 -> "64 bits";
int32 -> "32 bits";
int64 -> "64 bits";
uint32 -> "32 bits";
uint64 -> "64 bits";
fixed32 -> "32 bits";
fixed64 -> "64 bits";
sfixed32 -> "32 bits";
sfixed64 -> "64 bits";
{enum,E} -> "enum "++atom_to_list(E);
_ -> if not TypeSpecs ->
?f("~p", [Type]);
not TypespecsCanIndicateMapItemPresence,
IsMapTypeField ->
?f("~p", [Type]);
true ->
undefined
end
end;
base_type_comment(#gpb_oneof{}, _TEnv) ->
"oneof".
%% Step:
%% Calculate type text or texts (for oneof not expanded).
%% The reason for having a list of texts for unexpanded oneofs,
%% is to be able to replace individual ones in case of type spec translations.
augment_type_texts(FieldInfos, _Defs, _AnRes, #t_env{type_specs=false}) ->
FieldInfos;
augment_type_texts(FieldInfos, Defs, AnRes, TEnv) ->
[FI#field_info{type_text = type_text(Field, Defs, AnRes, TEnv)}
|| #field_info{field=Field}=FI <- FieldInfos].
type_text(#?gpb_field{type=Type, occurrence=Occurrence}=Field,
Defs, AnRes, TEnv) ->
TypeStr = type_to_typestr(Type, Defs, AnRes, TEnv),
IsMapTypeField = is_map_type_field(Field),
case Occurrence of
required ->
#type_text{text = TypeStr};
repeated when IsMapTypeField ->
#type_text{text = TypeStr};
repeated ->
#type_text{text = "[" ++ TypeStr ++ "]"};
optional ->
#type_text{text = TypeStr};
defaulty ->
#type_text{text = TypeStr}
end;
type_text(#gpb_oneof{fields=OFields}, Defs, AnRes, TEnv) ->
[#type_text{tag=OFName,
text=type_to_typestr(OFType, Defs, AnRes, TEnv)}
|| #?gpb_field{name=OFName, type=OFType} <- OFields].
%% Step:
%% Apply any type spec translations
apply_type_transforms(FieldInfos, _AnRes, #t_env{type_specs=false}) ->
FieldInfos;
apply_type_transforms(FieldInfos, AnRes, _TEnv) ->
[case has_type_spec_translation(Field, ElemPath, TypeText0, AnRes) of
{true, TypeStr} ->
FI#field_info{type_text = TypeStr, base_type_comment = undefined};
{partly, TypeStrs} ->
FI#field_info{type_text = TypeStrs}; % retain "oneof" comment
false ->
FI
end
|| #field_info{field=Field,
elem_path=ElemPath,
type_text=TypeText0}=FI <- FieldInfos].
has_type_spec_translation(#?gpb_field{occurrence=Occurrence}=Field,
ElemPath, _TypeText0, AnRes) ->
case gpb_gen_translators:has_type_spec_translation(ElemPath, AnRes) of
{true, TypeStr} ->
{true, #type_text{text = TypeStr}};
false ->
IsMapTypeField = is_map_type_field(Field),
case Occurrence of
required ->
false;
repeated when IsMapTypeField ->
false;
repeated ->
RElemPath = ElemPath ++ [[]],
case gpb_gen_translators:has_type_spec_translation(
RElemPath, AnRes) of
{true, RTypeStr} ->
{true, #type_text{text = "[" ++ RTypeStr ++ "]"}};
false ->
false
end;
optional ->
false;
defaulty ->
false
end
end;
has_type_spec_translation(#gpb_oneof{}, ElemPath, TypeTexts0, AnRes) ->
case gpb_gen_translators:has_type_spec_translation(ElemPath, AnRes) of
{true, TypeStr} ->
{true, #type_text{text = TypeStr}};
false ->
{partly,
lists:map(
fun(#type_text{tag=OFName}=TT0) ->
OFElemPath = ElemPath ++ [OFName],
case gpb_gen_translators:has_type_spec_translation(
OFElemPath, AnRes) of
{true, TypeStr} ->
TT0#type_text{text=TypeStr};
false ->
TT0
end
end,
TypeTexts0)}
end.
%% Step:
%% Add information in " | undefined" to maybe later be added to each type
augment_type_or_undefined(FieldInfos, TEnv) ->
#t_env{mapping_and_unset=MappingAndUnset} = TEnv,
OrUndefined = case MappingAndUnset of
records ->
true;
#maps{unset_optional=present_undefined} ->
true;
#maps{unset_optional=omitted} ->
false
end,
[FI#field_info{or_undefined = OrUndefined}
|| #field_info{}=FI <- FieldInfos].
%% Step:
%% Set the 'name' field
augment_field_name_strs(FieldInfos) ->
[FI#field_info{name = ?f("~p", [gpb_lib:get_field_name(Field)])}
|| #field_info{field=Field}=FI <- FieldInfos].
%% Step:
%% Set field default values (for use when generating records)
augment_default_values(FieldInfos, Opts, Defs, TEnv) ->
#t_env{mapping_and_unset = MappingAndUnset} = TEnv,
case MappingAndUnset of
records ->
[case Field of
#?gpb_field{}=Field ->
Default = record_field_default(Field, Opts, Defs, TEnv),
FI#field_info{default = Default};
#gpb_oneof{} ->
FI
end
|| #field_info{field=Field}=FI <- FieldInfos];
#maps{} ->
FieldInfos
end.
record_field_default(#?gpb_field{type=Type,
occurrence=Occurence,
opts=FOpts}=Field,
Opts, Defs, TEnv) ->
#t_env{map_type_fields = MapTypeFieldsRepr} = TEnv,
case proplists:get_value(default, FOpts, '$no') of
'$no' ->
IsMapTypeField = is_map_type_field(Field),
if IsMapTypeField, MapTypeFieldsRepr == maps ->
"#{}";
IsMapTypeField, MapTypeFieldsRepr == '2tuples' ->
"[]";
Occurence == repeated ->
"[]";
Occurence == defaulty ->
Default = gpb_lib:proto3_type_default(Type, Defs, Opts),
?f("~p", [Default]);
true ->
undefined
end;
Default ->
?f("~p", [Default])
end.
%% Step:
%% Set the type separator to "::" (records) or "=>" or ":=" (for maps).
augment_type_sep(FieldInfos, TEnv) ->
[FI#field_info{type_sep = calc_field_type_sep(Field, TEnv)}
|| #field_info{field=Field}=FI <- FieldInfos].
%% Step:
%% Set whether the field needs to be out-commented.
augment_out_commentation(FieldInfos, TEnv) ->
#t_env{mapping_and_unset = MappingAndUnset,
map_key_type = KeyType} = TEnv,
case MappingAndUnset of
records ->
[FI#field_info{out_comment = false}
|| FI <- FieldInfos];
#maps{} when KeyType == binary ->
%% Unconveniently enough, this is not allowed:
%% #{<<"abc">> => some_type()}
%% since <<"abc">> is not a valid type spec
%% Only <<>>, <<_:M>>, <<_:_*N>> and a few more are.
%% So out-comment entries
[FI#field_info{out_comment = true}
|| FI <- FieldInfos];
#maps{unset_optional=present_undefined} ->
[FI#field_info{out_comment = false}
|| FI <- FieldInfos];
#maps{unset_optional=omitted} ->
[FI#field_info{out_comment = false}
|| FI <- FieldInfos]
end.
%% Step:
%% Prepend the fields occurrence as a comment.
augment_occurrence(FieldInfos) ->
[case Field of
#?gpb_field{occurrence=Occurrence} ->
IsMapTypeField = is_map_type_field(Field),
Chunks2 = if IsMapTypeField -> Chunks;
Occurrence == required -> ["required" | Chunks];
Occurrence == optional -> ["optional" | Chunks];
Occurrence == defaulty -> ["optional" | Chunks];
Occurrence == repeated -> ["repeated" | Chunks]
end,
FI#field_info{comment_chunks = Chunks2};
#gpb_oneof{} ->
FI#field_info{comment_chunks = Chunks}
end
|| #field_info{field=Field, comment_chunks=Chunks}=FI <- FieldInfos].
%% Step:
%% Prepend the field's number (from the .proto) as a comment
augment_field_number(FieldInfos) ->
[case Field of
#?gpb_field{fnum=FNum} ->
Text = ?f("= ~p", [FNum]),
FI#field_info{comment_chunks = [Text | Chunks]};
#gpb_oneof{} ->
FI#field_info{comment_chunks = Chunks}
end
|| #field_info{field=Field, comment_chunks=Chunks}=FI <- FieldInfos].
%% --------------------------------------------------------------
%% Render the fields to text
%% Each line is later to be indented `Indent' spaces. (This amount
%% is here used for lineup calculations.)
%%
%% Return a list (one per field) of iolists.
%%
render_field_infos(FieldInfos, Indent) ->
IndexedFieldInfos = gpb_lib:index_seq(FieldInfos),
LastIndex = find_last_non_out_commented_field(IndexedFieldInfos),
lists:map(
fun({I, #field_info{out_comment=OutCommented,
name=FName,
default=Default,
type_sep=TypeSep,
type_text=TypeText,
or_undefined=OrUndefined,
comment_chunks=CommentChunks,
base_type_comment=BaseTypeComment}}) ->
Type = render_type_text(TypeText, OrUndefined),
LineLead = if OutCommented -> "%% ";
not OutCommented -> ""
end,
DefaultStr = if Default == undefined -> "";
true -> ?f(" = ~s", [Default])
end,
CommaSep = if OutCommented -> "";
not OutCommented ->
case has_next(I, LastIndex) of
true -> ",";
false -> ""
end
end,
Comment = render_comment(CommentChunks, BaseTypeComment),
FieldTxt1 = ?f("~s~s~s", [LineLead, FName, DefaultStr]),
FieldTxt2 = if Type /= undefined ->
%% with type specs
LineUp = lineup(Indent, FieldTxt1, 32),
?f("~s~s~s ~s~s", [FieldTxt1, LineUp,
TypeSep, Type, CommaSep]);
Type == undefined ->
%% no type specs
?f("~s~s", [FieldTxt1, CommaSep])
end,
LineUpCol2 = if Type /= undefined -> 52;
Type == undefined -> 40
end,
LineUpStr2 = lineup(Indent, FieldTxt2, LineUpCol2),
CommentStr = if Comment == "" -> "";
Comment /= "" -> "% " ++ Comment
end,
?f("~s~s~s", [FieldTxt2, LineUpStr2, CommentStr])
end,
IndexedFieldInfos).
find_last_non_out_commented_field(IndexedFieldInfos) ->
lists:foldr(
fun(_, Last) when is_integer(Last) ->
Last;
({_, #field_info{out_comment=true}}, Acc) ->
Acc;
({I, #field_info{out_comment=false}}, _Acc) ->
I
end,
undefined,
IndexedFieldInfos).
has_next(I, Last) ->
if is_integer(Last) ->
%% Index of last non-outcommented item
I < Last;
Last == undefined ->
%% All items are out-com
false
end.
render_type_text(#type_text{text=TypeStr}, OrUndefined) ->
if OrUndefined -> TypeStr ++ " | undefined";
not OrUndefined -> TypeStr
end;
render_type_text(TypeTexts, OrUndefined) when is_list(TypeTexts) -> % oneof
gpb_lib:or_join([?ff("{~p, ~s}", [Tag, TypeStr])
|| #type_text{tag=Tag, text=TypeStr} <- TypeTexts]
++ ["undefined" || OrUndefined]);
render_type_text(undefined, _) -> % no type specs
undefined.
render_comment(CommentChunks, BaseTypeComment) ->
if BaseTypeComment == undefined ->
gpb_lib:comma_join(CommentChunks);
is_list(BaseTypeComment) ->
gpb_lib:comma_join(CommentChunks ++ [BaseTypeComment])
end.
%% --------------------------------------------------------------
%% Helpers...
calc_field_type_sep(#?gpb_field{occurrence=Occurrence},
#t_env{mapping_and_unset=MappingAndUnset,
verify_decode_required_present=DecVfy}=TEnv) ->
case MappingAndUnset of
records ->
"::";
#maps{unset_optional=present_undefined} ->
mandatory_map_item_type_sep(TEnv);
#maps{unset_optional=omitted} ->
%% Even for required (proto2) fields, we generally use "=>",
%% since we cannot guarantee that we will always decode
%% to a map with all required fields always set,
%% since it depends on the input binary.
%%
%% However, with the `verify_decode_required_present'
%% option set, we can actually use ":=".
if Occurrence == required,
DecVfy ->
mandatory_map_item_type_sep(TEnv);
true ->
"=>"
end
end;
calc_field_type_sep(#gpb_oneof{}, #t_env{mapping_and_unset=MappingAndUnset}) ->
case MappingAndUnset of
records -> "::";
#maps{} -> "=>"
end.
mandatory_map_item_type_sep(TEnv) ->
%% With Erlang 19 we write #{n := integer()} to say that a
%% map must contain a map item with key `n' and an integer value.
%%
%% With earlier Erlang versions, we can only write #{n => integer()}
%% and we can never distinguish between map items that may or must
%% be present.
%%
%% Ideally, we would want to know for which version of Erlang we're
%% generating code. For now, we assume the run-time version is the
%% same as the compile-time version, which is not necessarily true. For
%% instance, we can generate code for maps even on pre-map Erlang R15.
%%
%% (At the time of this writing, the OTP_RELEASE pre-defined macro
%% does not exist, but even if it had existed, it would have been of
%% limited value because it would have linked the Erlang version at
%% proto-encoding run-time with the Erlang version at compile-time
%% of `gpb' not at compile-time of the .proto file. In some
%% scenario with a package manager, it might have a `gpb'
%% pre-compiled with an old Erlang-version to be compatible with
%% many environments. Better to check version at run-time.)
%%
#t_env{can_do_map_presence = TypespecsCanIndicateMapItemPresence} = TEnv,
case TypespecsCanIndicateMapItemPresence of
true -> ":=";
false -> "=>"
end.
type_to_typestr(sint32, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(sint64, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(int32, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(int64, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(uint32, _Defs, _AnRes, _TEnv) -> "non_neg_integer()";
type_to_typestr(uint64, _Defs, _AnRes, _TEnv) -> "non_neg_integer()";
type_to_typestr(bool, _Defs, _AnRes, _TEnv) -> "boolean() | 0 | 1";
type_to_typestr(fixed32, _Defs, _AnRes, _TEnv) -> "non_neg_integer()";
type_to_typestr(fixed64, _Defs, _AnRes, _TEnv) -> "non_neg_integer()";
type_to_typestr(sfixed32, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(sfixed64, _Defs, _AnRes, _TEnv) -> "integer()";
type_to_typestr(float, _Defs, _AnRes, _TEnv) -> float_spec();
type_to_typestr(double, _Defs, _AnRes, _TEnv) -> float_spec();
type_to_typestr(string, _Defs, _AnRes, _TEnv) -> "unicode:chardata()";
type_to_typestr(bytes, _Defs, _AnRes, _TEnv) -> "iodata()";
type_to_typestr({enum,E}, Defs, _AnRes, TEnv) ->
enum_typestr(E, Defs, TEnv);
type_to_typestr({msg,M}, _Defs, AnRes, TEnv) ->
msg_to_typestr(M, AnRes, TEnv);
type_to_typestr({group,G}, _Defs, AnRes, TEnv) ->
msg_to_typestr(G, AnRes, TEnv);
type_to_typestr({map,KT,VT}, Defs, AnRes, TEnv) ->
#t_env{map_type_fields=MapTypeFieldsRepr,
can_do_map_presence=TypespecsCanIndicateMapItemPresence} = TEnv,
KTStr = type_to_typestr(KT, Defs, AnRes, TEnv),
VTStr = type_to_typestr(VT, Defs, AnRes, TEnv),
case {MapTypeFieldsRepr, TypespecsCanIndicateMapItemPresence} of
{'2tuples', _} -> ?f("[{~s, ~s}]", [KTStr, VTStr]);
{maps, true} -> ?f("#{~s => ~s}", [KTStr, VTStr]); % map can be empty
{maps, false} -> "#{}" % map can be empty
end;
type_to_typestr(unknown, _Defs, _AnRes, _TEnv) ->
"term()".
float_spec() ->
"float() | integer() | infinity | '-infinity' | nan".
msg_to_typestr(M, AnRes, TEnv) ->
MsgType = rename_msg_type(M, AnRes),
#t_env{mapping_and_unset=MappingAndUnset,
module = Mod} = TEnv,
case MappingAndUnset of
records ->
%% Prefix with module since records live in an hrl file
?f("~p:~p()", [Mod, MsgType]);
#maps{} ->
?f("~p()", [MsgType])
end.
enum_typestr(E, Defs, #t_env{nif=Nif}) ->
UnknownEnums = if Nif -> "";
true -> " | integer()"
end,
{value, {{enum,E}, Enumerations}} = lists:keysearch({enum,E}, 1, Defs),
gpb_lib:or_join(
[?f("~p", [EName]) || {EName, _, _} <- Enumerations])
++ UnknownEnums.
lineup(BaseIndent, Text, TargetCol) ->
CurrentCol = BaseIndent + iolist_size(Text),
if CurrentCol < TargetCol ->
lists:duplicate(TargetCol - CurrentCol, $\s);
true ->
" "
end.
is_map_type_field(#?gpb_field{type={map,_,_}, occurrence=repeated}) -> true;
is_map_type_field(_) -> false.
rename_enum_type(Name, #anres{renamings=Renamings}) ->
gpb_names:apply_enum_type_renaming(Name, Renamings).
rename_msg_type(Name, #anres{renamings=Renamings}) ->
gpb_names:apply_msg_type_renaming(Name, Renamings).