%%% Copyright (C) 2019 Tomas Abrahamsson %%% %%% Author: Tomas Abrahamsson %%% %%% 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: %% ``` %% {, %% , %% [{, , 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}' = '', [replace_tree('#r{f1=R1..fn=RN}', FMatch), replace_tree('', 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'('', 'TrUserData'), [replace_term('lists:reverse',Finalizer), replace_tree('', 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 %% required, 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 auxiliary 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.