Current section

Files

Jump to
gpb src gpb_decoders_lib.erl
Raw

src/gpb_decoders_lib.erl

%%% Copyright (C) 2019 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
%%% @private
%%% @doc Common functions for gpb_gen_decoders and gpb_gen_json_decoders
-module(gpb_decoders_lib).
-export([init_exprs/6]).
-export([calc_field_infos/2]).
-export([decoder_read_field_param/3]).
-export([decoder_maybe_check_required_present/3]).
-export([decoder_finalize_result/5]).
-export([run_morph_ops/2]).
-export([underscore_unused_vars/0]).
-export([explode_param_init/3]).
-export([explode_param_pass/3]).
-export([change_undef_marker_in_clauses/1]).
-export([implode_to_map_exprs_all_mandatory/0]).
-export([implode_to_map_exprs/3]).
-export([rework_records_to_maps/3]).
-export([finalize_marked_map_exprs/1]).
-include("../include/gpb.hrl").
-include("gpb_codegen.hrl").
-include("gpb_compile.hrl").
-include("gpb_decoders_lib.hrl").
-import(gpb_lib, [replace_term/2, replace_tree/2]).
%% @doc Generate field initialization expressions.
%% Such expressions include `[]' initially for repeated fields, or the
%% proto3 type-default value for optional fields (or `undefined' for
%% optional fields in proto2 messages). Also of concern is that there
%% may be translators that set other initial values.
init_exprs(MsgName, MsgDef, Defs, TrUserDataVar, AnRes, Opts)->
UseDefaults = proplists:get_bool(defaults_for_omitted_optionals, Opts),
UseTypeDefaults = proplists:get_bool(type_defaults_for_omitted_optionals,
Opts),
DecVfy = proplists:get_bool(verify_decode_required_present, Opts),
MappingUnset = gpb_lib:get_mapping_and_unset_by_opts(Opts),
R = % Whether required fields are present in init-exprs
case MappingUnset of
records -> o;
#maps{} -> if DecVfy -> m;
true -> o
end
end,
ExprInfos1 =
[case Field of
#?gpb_field{name=FName, occurrence=Occurrence, type=Type,
opts=FOpts} ->
HasDefault = lists:keymember(default, 1, FOpts),
SubMsgType = is_msg_type(Type),
{Undefined, Undef, P} =
if SubMsgType ->
{?expr(undefined), ?expr('$undef'), o};
Occurrence == defaulty ->
TD = gpb_lib:proto3_type_default(Type, Defs, Opts),
ATD = erl_syntax:abstract(TD),
{ATD, ATD, o};
UseDefaults, HasDefault ->
{default,D} = lists:keyfind(default, 1, FOpts),
AD = erl_syntax:abstract(D),
{AD, AD, m};
UseTypeDefaults ->
TD = gpb_lib:proto2_type_default(Type, Defs, Opts),
ATD = erl_syntax:abstract(TD),
{ATD, ATD, m};
true ->
Pr = if HasDefault -> d;
true -> o
end,
{?expr(undefined), ?expr('$undef'), Pr}
end,
case Occurrence of
repeated -> {FName, m, ?expr([]), ?expr([])};
required -> {FName, R, ?expr(undefined), ?expr('$undef')};
optional -> {FName, P, Undefined, Undef};
defaulty -> {FName, o, Undefined, Undef}
end;
#gpb_oneof{name=FName} ->
{FName, o, ?expr(undefined), ?expr('$undef')}
end
|| Field <- MsgDef],
ExprInfos2 =
[begin
ElemPath = [MsgName, FName],
TranslFn = gpb_gen_translators:find_translation(
ElemPath,
decode_init_default,
AnRes),
TrInitExpr = ?expr('Tr'('InitExpr', 'TrUserData'),
[replace_tree('InitExpr', InitExpr),
replace_term('Tr', TranslFn),
replace_tree('TrUserData', TrUserDataVar)]),
TrMOExpr = ?expr('Tr'('MOExpr', 'TrUserData'),
[replace_tree('MOExpr', MOExpr),
replace_term('Tr', TranslFn),
replace_tree('TrUserData', TrUserDataVar)]),
{FName, Presence, TrInitExpr, TrMOExpr}
end
|| {FName, Presence, InitExpr, MOExpr} <- ExprInfos1],
case gpb_lib:get_field_pass(MsgName, AnRes) of
pass_as_params ->
case MappingUnset of
records ->
[{FName, Expr} || {FName, _, Expr, _MOExpr} <- ExprInfos2];
#maps{unset_optional=present_undefined} ->
[{FName, Expr} || {FName, _, Expr, _MOExpr} <- ExprInfos2];
#maps{unset_optional=omitted} ->
[{FName, MapsOmittedExpr}
|| {FName, _, _Expr, MapsOmittedExpr} <- ExprInfos2]
end;
pass_as_record ->
case MappingUnset of
records ->
[{FName, Expr} || {FName, P, Expr, _} <- ExprInfos2,
P == m orelse P == d];
#maps{unset_optional=present_undefined} ->
[{FName, Expr} || {FName, _, Expr, _} <- ExprInfos2];
#maps{unset_optional=omitted} ->
[{FName, Expr} || {FName, m, _, Expr} <- ExprInfos2]
end
end.
is_msg_type({msg,_}) -> true;
is_msg_type(_) -> false.
%% @doc Generate an expression `#record{field_1 = R1, ... field_n=RN} = Msg',
%% and return it and also its constituent parts:
%% ```
%% {<the Msg var>,
%% <that expr>,
%% [{<name of field_1>, <the R_1 var>, repeated | required}, ...]}
%% '''
decoder_read_field_param(MsgName, MsgDef, Opts) ->
MappingVar = ?expr(Msg),
FFieldNamesOcc =
case proplists:get_bool(verify_decode_required_present, Opts) of
true -> required_or_repeated_field_names(MsgDef);
false -> repeated_field_names(MsgDef)
end,
FFields = [{FName, gpb_lib:var_n("R", I), Occ}
|| {I,{FName,Occ}} <- gpb_lib:index_seq(FFieldNamesOcc)],
FNameVars = [{FName, FVar} || {FName, FVar, _Occ} <- FFields],
FMatch = gpb_lib:record_match(MsgName, FNameVars),
FParam = ?expr('#r{f1=R1..fn=RN}' = '<Msg>',
[replace_tree('#r{f1=R1..fn=RN}', FMatch),
replace_tree('<Msg>', MappingVar)]),
{MappingVar, FParam, FFields}.
required_or_repeated_field_names(MsgDef) ->
[{FName, Occ} || #?gpb_field{name=FName, occurrence=Occ} <- MsgDef,
Occ == required orelse Occ == repeated].
repeated_field_names(MsgDef) ->
[{FName, Occ} || #?gpb_field{name=FName, occurrence=Occ} <- MsgDef,
Occ == repeated].
%% @doc Create expressions that check whether required fields
%% have been set, if the option indicates we should.
decoder_maybe_check_required_present(MsgName, FFields, Opts) ->
case proplists:get_bool(verify_decode_required_present, Opts) of
true ->
decoder_check_required_present(MsgName, FFields);
false ->
[]
end.
decoder_check_required_present(MsgName, FFields) ->
[?expr(if 'FVar' == undefined ->
error({gpb_error,
{decoding_failure,
{missing_required_msg_field,
'MsgName', 'FName'}}});
true ->
ok
end,
[replace_tree('FVar', FVar),
replace_term('MsgName', MsgName),
replace_term('FName', FName)])
|| {FName, FVar, required} <- FFields].
%% @doc Create a finalization expression by generating calls to
%% finalization translators. To give a notion what this is about,
%% a default such translator for repeated fields is `lists:reverse'.
decoder_finalize_result(MsgVar, FFields, MsgName, TrUserDataVar, AnRes) ->
gpb_lib:record_update(
MsgVar,
MsgName,
[begin
ElemPath = [MsgName, FName],
Finalizer = gpb_gen_translators:find_translation(
ElemPath,
decode_repeated_finalize,
AnRes),
FValueExpr = ?expr('lists:reverse'('<FVar>', 'TrUserData'),
[replace_term('lists:reverse',Finalizer),
replace_tree('<FVar>', FVar),
replace_tree('TrUserData',
TrUserDataVar)]),
{FName, FValueExpr}
end
|| {FName, FVar, repeated} <- FFields]).
%% @doc Compute optionality information for each field of a message.
calc_field_infos(MsgDef, Opts) ->
case gpb_lib:get_mapping_and_unset_by_opts(Opts) of
#maps{unset_optional=omitted, oneof=flat} ->
field_infos(MsgDef, true);
_ ->
field_infos(MsgDef, false)
end.
field_infos(MsgDef, FlattenOnoef) ->
[case Field of
#?gpb_field{name=FName, type={msg,_}} ->
{FName, optional};
#?gpb_field{name=FName, occurrence=defaulty} ->
%% On finalization, treat proto3 (scalar) fields as
%% requried, to avoid redundant if F == '$undef' -> ...
%% checks; it can never be '$undef' since it has a
%% type-default. (except for sub messages and onoef)
{FName, required};
#?gpb_field{name=FName} ->
{FName, gpb_lib:get_field_occurrence(Field)};
#gpb_oneof{name=FName} ->
if not FlattenOnoef ->
{FName, optional};
FlattenOnoef ->
{FName, flatten_oneof}
end
end
|| Field <- MsgDef].
%% @doc Process a list of morping operation over a list of `#fn{}'s
%% (function with some associated meta info).
%%
%% A morphing operation is a fun that in some way or another change the
%% syntax tree of the function in the `#fn{}', but only for the `#fn{}'s
%% to which the morphing operation is applicable.
run_morph_ops([Op | Rest], Fns) ->
run_morph_ops(Rest, Op(Fns));
run_morph_ops([], Fns) ->
[gpb_codegen:erl_prettypr_format_nl(Tree) || #fn{tree=Tree} <- Fns].
%% @doc Replace unused function params and case clause patterns with
%% underscore.
%%
%% The intention is that code generation can issue match patterns for
%% all fields of a message, or all auxilliary working parameters and not
%% have to worry about underscoring unused ones to avoid warning messages
%% about unused variables.
underscore_unused_vars() ->
fun(Fns) ->
loop_fns(fun gpb_codemorpher:underscore_unused_vars/1,
process_all(),
Fns)
end.
%% @doc Passing and updating message fields as decoding-function
%% parameters instead of as a record/map. This often generates faster code.
%%
%% Applies to decoding-initialization functions only.
explode_param_init(MsgName, InitExprs, ArgPos) ->
fun(Fns) ->
loop_fns(
fun(FnTree) ->
gpb_codemorpher:explode_record_fields_to_params_init(
FnTree, ArgPos, {MsgName, InitExprs})
end,
process_initializer(),
Fns)
end.
%% @doc Passing and updating message fields as decoding-function
%% parameters instead of as a record/map. This often generates faster code.
%%
%% Applies to field decoding/passing functions only.
explode_param_pass(MsgName, FNames, ArgPos) ->
fun(Fns) ->
loop_fns(
fun(FnTree) ->
gpb_codemorpher:explode_record_fields_to_params(
FnTree, ArgPos, {MsgName, FNames})
end,
process_msg_passers(),
Fns)
end.
%% @doc For records, `undefined' is used to indicate an omitted optional value.
%% However, that atom is also a valid enum. For maps, we can do better,
%% so offer a way to change it to some other atom, such as `$undef', which is
%% not a valid protobuf enum.
change_undef_marker_in_clauses(Undef) ->
fun(Fns) ->
loop_fns(
fun(FnTree) ->
gpb_codemorpher:change_undef_marker_in_clauses(
FnTree, Undef)
end,
process_all(),
Fns)
end.
%% @doc The opposite of the {@link explode_param_init/3}, when a map is
%% constructed from all field parameters. Useful for maps with unset
%% optional = preset_undefined.
%%
%% Applies to finalization functions only.
implode_to_map_exprs_all_mandatory() ->
fun(Fns) ->
loop_fns(fun gpb_codemorpher:implode_to_map_expr/1,
process_finalizers(),
Fns)
end.
%% @doc The opposite of the {@link explode_param_init/3}, when a map is
%% constructed from all field parameters. Useful for maps with omitted
%% unset optionals (which is the default)
%%
%% Applies to finalization functions only.
implode_to_map_exprs(F1Pos, FieldInfos, Undef) ->
fun(Fns) ->
loop_fns(
fun(FnTree) ->
gpb_codemorpher:implode_to_map_exprs(
FnTree, F1Pos, FieldInfos, Undef)
end,
process_finalizers(),
Fns)
end.
%% @doc Change record expressions to map expressions. Useful when passing
%% messages as maps/records.
rework_records_to_maps(RecordParamPos, FieldInfos, Undef) ->
fun(Fns) ->
loop_fns(
fun(FnTree) ->
gpb_codemorpher:rework_records_to_maps(
FnTree, RecordParamPos, FieldInfos, Undef)
end,
process_initializers_finalizers_and_msg_passers(),
Fns)
end.
%% @doc Finalize expressions that have been marked as map expressions.
%%
%% Initially of importance but now of continuously diminising value, gpb
%% can generate code for maps also when hosted on a pre Erlang-17
%% systems where maps does not exist. It does this by rendering the map
%% expressions to string format using erl_syntax:text() nodes, which are
%% rendered in verbatim by the erl_prettypr:format machinery.
%%
%% However, the code morpher must still be able to manipulate
%% expressions. To make this possible, it uses marked record
%% expressions. The markup is specially tagged tuples. As the last
%% step, these tagged record expressions are converted to map
%% expressions, possibly by using erl_syntax text nodes if on old
%% systems, and otherwise map syntax nodes.
%%
%% That's what this morping step performs.
finalize_marked_map_exprs(Opts) ->
F = fun(MarkedExpr) ->
gpb_codemorpher:marked_map_expr_to_map_expr(MarkedExpr, Opts)
end,
fun(Fns) ->
loop_fns(F, process_initializers_finalizers_and_msg_passers(), Fns)
end.
loop_fns(MapFun, Filter, Fns) ->
[case matches_filter(Fn, Filter) of
true -> Fn#fn{tree = MapFun(FnTree)};
false -> Fn
end
|| #fn{tree=FnTree}=Fn <- Fns].
matches_filter(#fn{}=Fn, Filter) ->
Filter(Fn).
process_all() -> fun(#fn{}) -> true end.
process_initializer() -> fun(#fn{initializes_fields=Bool}) -> Bool end.
process_finalizers() -> fun(#fn{has_finalizer=Bool}) -> Bool end.
process_msg_passers() -> fun(#fn{passes_msg=Bool}) -> Bool end.
process_initializers_finalizers_and_msg_passers() ->
fun(#fn{initializes_fields=B1,
has_finalizer=B2,
passes_msg=B3}) ->
B1 or B2 or B3
end.