Packages
gpb
4.19.2
5.0.0
4.21.7
4.21.6
4.21.5
4.21.4
4.21.3
4.21.2
4.21.1
4.21.0
4.20.0
4.19.9
4.19.8
4.19.7
4.19.6
4.19.5
4.19.4
4.19.3
4.19.2
4.19.1
4.19.0
4.18.0
4.17.7
4.17.6
4.17.5
4.17.3
4.17.2
4.17.1
4.17.0
4.16.2
4.16.1
4.16.0
4.15.2
4.15.1
4.14.2
4.14.1
4.14.0
4.13.0
4.12.0
4.11.2
4.11.1
4.11.0
4.10.6
4.10.5
4.10.4
4.10.3
4.10.2
4.10.1
4.10.0
4.9.3
4.9.2
4.9.1
4.9.0
4.8.0
4.7.3
4.7.2
4.7.1
4.7.0
4.6.0
4.5.1
4.5.0
4.4.1
4.4.0
4.3.3
4.3.2
4.3.1
4.3.0
4.2.3
4.2.2
4.2.1
4.2.0
4.1.9
4.1.8
4.1.7
4.1.6
4.1.5
4.1.4
4.1.3
4.1.2
4.1.1
4.1.0
4.0.2
4.0.1
4.0.0
3.28.1
3.28.0
3.27.7
3.27.6
3.27.5
3.27.4
3.27.3
3.27.2
3.27.1
3.27.0
3.26.8
3.26.7
3.26.6
3.26.5
3.26.4
3.26.3
3.26.2
3.26.1
3.26.0
3.25.2
3.25.1
3.25.0
3.24.4
3.24.3
3.24.2
3.24.1
3.24.0
3.23.2
3.23.1
3.23.0
3.22.5
3.22.4
3.22.3
3.22.2
3.22.1
3.22.0
3.21.3
3.21.2
3.21.1
3.21.0
3.20.3
3.20.2
3.20.0
3.19.0
3.18.10
3.18.9
3.18.8
3.18.7
3.18.6
3.18.5
3.18.4
3.18.3
3.18.2
3.18.1
3.18.0
3.17.13
3.17.12
3.17.11
3.17.10
3.17.9
3.17.8
3.17.5
3.17.4
3.17.3
3.17.2
3.17.1
3.17.0
3.16.0
3.15.0
3.14.0
3.13.0
3.12.2
3.12.1
3.12.0
3.11.0
A compiler for Google protocol buffer definitions files for Erlang.
Current section
Files
Jump to
Current section
Files
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).