%%% 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 Generation of routines for converting JSON to internal format -module(gpb_gen_json_decoders). -export([format_exports/2]). -export([format_top_function/3]). -export([format_decoders/3]). -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, splice_trees/2, repeat_clauses/2]). format_exports(Defs, Opts) -> DoNif = proplists:get_bool(nif, Opts), NoNif = not DoNif, [?f("-export([from_json/2"),[", from_json/3" || NoNif], ?f("]).~n"), [[[begin NoWrapperFnName = gpb_lib:mk_fn(from_json_msg_, MsgName), if DoNif -> ?f("-export([~p/1]).~n", [NoWrapperFnName]); not DoNif -> ?f("-export([~p/1, ~p/2]).~n", [NoWrapperFnName, NoWrapperFnName]) end end || {{msg,MsgName}, _Fields} <- Defs], "\n"] || gpb_lib:get_bypass_wrappers_by_opts(Opts)]]. format_top_function(Defs, AnRes, Opts) -> case gpb_lib:contains_messages(Defs) of true -> format_top_function_msgs(Defs, AnRes, Opts); false -> format_top_function_no_msgs(Opts) end. format_decoders(Defs, AnRes, Opts) -> [format_msg_decoders(Defs, AnRes, Opts)]. format_top_function_no_msgs(_Opts) -> ["-spec from_json(term(), atom()) -> no_return().\n", gpb_codegen:format_fn( from_json, fun(_Json, _MsgName) -> erlang:error({gpb_error, no_messages}) end), "-spec from_json(term(), atom(), list()) -> no_return().\n", gpb_codegen:format_fn( from_json, fun(_Json, _MsgName, _Opts) -> erlang:error({gpb_error, no_messages}) end)]. format_top_function_msgs(Defs, AnRes, Opts) -> Error = ("error({gpb_error," ++ "" "{from_json_failure," ++ "" " {Json, MsgName, {Class, Reason, StackTrace}}}})"), FromJsonMsg1Catch_GetStackTraceAsPattern = ?f("from_json_1_catch(Json, MsgName, TrUserData) ->~n" " try from_json_2_doit(MsgName, Json, TrUserData)~n" " catch Class:Reason:StackTrace -> ~s~n" " end.~n", [Error]), FromJsonMsg1Catch_GetStackTraceAsCall = ?f("from_json_1_catch(Json, MsgName, TrUserData) ->~n" " try from_json_2_doit(MsgName, Json, TrUserData)~n" " catch Class:Reason ->~n" " StackTrace = erlang:get_stacktrace(),~n" " ~s~n" " end.~n", [Error]), DoNif = proplists:get_bool(nif, Opts), [gpb_codegen:format_fn( from_json, fun(Json, MsgName) -> call_self(Json, MsgName, []) end), gpb_codegen:format_fn( from_json, fun(Json, MsgName, Opts) -> TrUserData = proplists:get_value(user_data, Opts), from_json_1_catch(Json, MsgName, TrUserData) end), ["-ifdef('OTP_RELEASE').\n", % This macro appeared in Erlang 21 FromJsonMsg1Catch_GetStackTraceAsPattern, "-else.\n", FromJsonMsg1Catch_GetStackTraceAsCall, "-endif.\n\n"], gpb_codegen:format_fn( from_json_2_doit, fun('MsgName', Json, TrUserData) -> 'Tr'('from-json-fn'(Json, 'TrUserData'), TrUserData) end, [repeat_clauses( 'MsgName', [begin ElemPath = [MsgName], Transl = gpb_gen_translators:find_translation( ElemPath, decode, AnRes), [replace_term('MsgName', MsgName), replace_term('Tr', Transl), replace_term('from-json-fn', gpb_lib:mk_fn(from_json_msg_, MsgName)), splice_trees('TrUserData', if DoNif -> []; true -> [?expr(TrUserData)] end)] end || {{msg, MsgName}, _Fields} <- Defs])])]. format_msg_decoders(Defs, AnRes, Opts) -> [[[gpb_codegen:format_fn( gpb_lib:mk_fn(from_json_msg_, MsgName), fun(Bin) -> %% The undefined is the default TrUserData call_self(Bin, undefined) end) || {{msg,MsgName},_Fields} <- Defs] || gpb_lib:get_bypass_wrappers_by_opts(Opts)], [format_msg_decoder(Name, MsgDef, Defs, AnRes, Opts) || {_Type, Name, MsgDef} <- gpb_lib:msgs_or_groups(Defs)], format_helpers(Defs, AnRes, Opts)]. format_msg_decoder(MsgName, MsgDef, Defs, AnRes, Opts) -> %% Compared to decoding from protobuf wire-format (gpb_gen_decoders), %% We do not need to do any merging here intertwined with decoding. %% In Json, a key can occur only once. %% %% (In contrast, the protobuf wire-format is a stream of fields, %% if a field occurs more than once, the value is to be merged: %% - overwritten if it is an an optional or required scalar field, %% - appended to the sequence if the field is a repeated, %% - recursively merged if the field is another sub message. %% ) %% TrUserDataVar = ?expr(TrUserData), InitExprs = gpb_decoders_lib:init_exprs(MsgName, MsgDef, Defs, TrUserDataVar, AnRes, Opts), IsProto3 = gpb:is_msg_proto3(MsgName, Defs), FieldInfos = gpb_decoders_lib:calc_field_infos(MsgDef, IsProto3, Opts), if MsgDef == [] -> format_msg_decoder_no_fields(MsgName, Opts); MsgDef /= [] -> [format_msg_init_decoder(MsgName, InitExprs, FieldInfos, TrUserDataVar, Opts), format_msg_decoder_loop(MsgName, MsgDef, TrUserDataVar, AnRes, Opts)] end. format_msg_decoder_no_fields(MsgName, Opts) -> InitExprs1 = calc_init_exprs(MsgName, [], [], Opts), gpb_codegen:format_fn( gpb_lib:mk_fn(from_json_msg_, MsgName), fun(_Json, _TrUserData) -> '' end, [replace_tree('', InitExprs1)]). format_msg_init_decoder(MsgName, InitExprs, FieldInfos, TrUserDataVar, Opts) -> InitExprs1 = calc_init_exprs(MsgName, InitExprs, FieldInfos, Opts), FromJAuxFn = gpb_lib:mk_fn(fj_msg_, MsgName), gpb_codegen:format_fn( gpb_lib:mk_fn(from_json_msg_, MsgName), fun(Json, 'TrUserData') -> ''(fj_next(fj_iter(Json)), '', 'TrUserData') end, [replace_term('', FromJAuxFn), replace_tree('', InitExprs1), replace_tree('TrUserData', TrUserDataVar)]). calc_init_exprs(MsgName, InitExprs, FieldInfos, Opts) -> MappingUnset = gpb_lib:get_mapping_and_unset_by_opts(Opts), case MappingUnset of records -> gpb_lib:record_create(MsgName, InitExprs); #maps{unset_optional=present_undefined} -> gpb_lib:map_create(InitExprs, Opts); #maps{unset_optional=omitted, oneof=tuples} -> InitExprs1 = repeated_or_required_init_exprs(InitExprs, FieldInfos), gpb_lib:map_create(InitExprs1, Opts); #maps{unset_optional=omitted, oneof=flat} -> InitExprs1 = repeated_or_required_init_exprs(InitExprs, FieldInfos), gpb_lib:map_create(InitExprs1, Opts) end. repeated_or_required_init_exprs(InitExprs, FieldInfos) -> [{FName, InitExpr} || {FName, InitExpr, Occ} <- kzip(InitExprs, FieldInfos), Occ == required orelse Occ == repeated]. kzip(L1, L2) -> lists:map(fun({{K, V1}, {K, V2}}) -> {K, V1, V2} end, lists:zip(L1, L2)). format_msg_decoder_loop(MsgName, MsgDef, TrUserDataVar, AnRes, Opts) -> JValueExpr = ?expr(JValue), EMsgVar = ?expr(EMsg), Decodings = calc_decodings(MsgName, MsgDef, JValueExpr, EMsgVar, TrUserDataVar, AnRes, Opts), gpb_codegen:format_fn( gpb_lib:mk_fn(fj_msg_, MsgName), fun({JKey, JValue, JRest}, EMsg, TrUserData) -> EMsg2 = case JKey of '' -> ''; _ -> EMsg end, call_self(fj_next(JRest), EMsg2, TrUserData); (none, EMsg, _TrUserData) -> EMsg end, [repeat_clauses( '', [[replace_tree('', KeyPattern), replace_tree('', DecodeExpr)] || {KeyPattern, DecodeExpr} <- Decodings])]). calc_decodings(MsgName, MsgDef, JValueExpr, EMsgVar, TrUserDataVar, AnRes, Opts) -> %% In Json, the oneof fields are like flat oneofs: %% %% message Msg { %% oneof c { %% uint32 alt_one = 1; %% string alt_two = 2; %% } %% uint32 outside_oneof = 10; %% } %% %% -> {"alt_one": 17} %% or {"alt_two": "x"} in Json %% %% The Erlang-internal format (if maps) may be either %% #{c => {alt_one, 18}} if option {maps_oneof,flat} is _not_ set, or %% #{alt_one => 18} if option {maps_oneof,flat} is set. %% %% Additionally, in Json, "[p]arsers accept both the lowerCamelCase name %% (or the one specified by the json_name option) and the original proto %% field name." %% (src: https://developers.google.com/protocol-buffers/docs/proto3#json) %% %% So build a mapping from Json field name (binary, atom or string) %% to decoding-and-update expressions: %% KeyFormat = gpb_lib:json_key_format_by_opts(Opts), lists:reverse( gpb_lib:fold_msgdef_fields_o( fun(#?gpb_field{}=Field, IsOneof, Acc)-> KeyPatterns = key_patterns(Field, KeyFormat), DecExpr = mk_check_null_decode_value_update_field_expr( MsgName, Field, IsOneof, JValueExpr, EMsgVar, TrUserDataVar, AnRes, Opts), [{KP, DecExpr} || KP <- KeyPatterns] ++ Acc end, [], MsgDef)). key_patterns(#?gpb_field{name=FName}=Field, KeyFormat) -> FNameS = atom_to_list(FName), LowerCamelCase = gpb_lib:get_field_json_name(Field), Strs = lists:usort([FNameS, LowerCamelCase]), case KeyFormat of atom -> [erl_syntax:atom(JFName) || JFName <- Strs]; binary -> %% Want <<"fname">> and not <<102,110,97,109,101>> %% so make a text node [erl_syntax:text(?ff("<<~p>>", [JFName])) || JFName <- Strs]; string -> [erl_syntax:string(JFName) || JFName <- Strs] end. mk_check_null_decode_value_update_field_expr( MsgName, #?gpb_field{type=Type, occurrence=Occurrence}=Field, IsOneof, JValueExpr, EMsgVar, TrUserDataVar, AnRes, Opts) -> %% FIXME: handle repeated %% FIXME: handle map<_,_> %% FIXME: gpb_compile: remove transl gen workaround for no from_json calls JNull = gpb_lib:json_null(Opts), DecExpr = case occurrence_or_mapfield(Occurrence, Type) of optional -> type_decode_expr(Type, JValueExpr, TrUserDataVar); required -> type_decode_expr(Type, JValueExpr, TrUserDataVar); repeated -> repeated_field_decode_expr(MsgName, Field, JValueExpr, TrUserDataVar, AnRes); mapfield -> mapfield_decode_expr(MsgName, Field, JValueExpr, TrUserDataVar, AnRes) end, ?expr(if 'JValueExpr' =:= null -> 'EMsgVar'; true -> '' end, [replace_tree('JValueExpr', JValueExpr), replace_term(null, JNull), replace_tree('EMsgVar', EMsgVar), replace_tree('', mk_decode_value_update_field_expr( MsgName, Field, IsOneof, EMsgVar, DecExpr, TrUserDataVar, AnRes, Opts))]). mk_decode_value_update_field_expr(MsgName, #?gpb_field{name=FName}, IsOneof, EMsgVar, DecExpr, TrUserDataVar, AnRes, Opts) -> MappingUnset = gpb_lib:get_mapping_and_unset_by_opts(Opts), case {MappingUnset, IsOneof} of {records, false} -> ElemPath = [MsgName, FName], FValue = tr_wrap({ElemPath, decode, AnRes}, DecExpr, TrUserDataVar), ?expr(''#''{'' = ''}, [replace_tree('', EMsgVar), replace_term('', MsgName), replace_term('', FName), replace_tree('', FValue)]); {records, {true, CFName}} -> %% Create CTr({tag, OTr(, TrUserData)}, TrUserData) CElemPath = [MsgName, CFName], OElemPath = [MsgName, CFName, FName], FValue = tr_wrap({CElemPath, decode, AnRes}, tag_wrap(FName, tr_wrap({OElemPath, decode, AnRes}, DecExpr, TrUserDataVar)), TrUserDataVar), ?expr(''#''{'' = ''}, [replace_tree('', EMsgVar), replace_term('', MsgName), replace_term('', CFName), replace_tree('', FValue)]); {#maps{oneof=tuples}, false} -> ElemPath = [MsgName, FName], FValue = tr_wrap({ElemPath, decode, AnRes}, DecExpr, TrUserDataVar), gpb_lib:map_set(EMsgVar, [{FName, FValue}], Opts); {#maps{oneof=tuples}, {true, CFName}} -> %% Create CTr({tag, OTr(, TrUserData)}, TrUserData) CElemPath = [MsgName, CFName], OElemPath = [MsgName, CFName, FName], FValue = tr_wrap({CElemPath, decode, AnRes}, tag_wrap(FName, tr_wrap({OElemPath, decode, AnRes}, DecExpr, TrUserDataVar)), TrUserDataVar), gpb_lib:map_set(EMsgVar, [{CFName, FValue}], Opts); {#maps{oneof=flat}, false} -> ElemPath = [MsgName, FName], FValue = tr_wrap({ElemPath, decode, AnRes}, DecExpr, TrUserDataVar), gpb_lib:map_set(EMsgVar, [{FName, FValue}], Opts); {#maps{oneof=flat}, {true, CFName}} -> %% Generate maps:without(...)? To guarantee at most one oneof %% field, also in case the json would be "malformed" containing %% for instance both alt_one and alt_two? CElemPath = [MsgName, CFName], OElemPath = [MsgName, CFName, FName], FValue = tr_wrap({CElemPath, decode, AnRes}, tr_wrap({OElemPath, decode, AnRes}, DecExpr, TrUserDataVar), TrUserDataVar), gpb_lib:map_set(EMsgVar, [{FName, FValue}], Opts) end. repeated_field_decode_expr(MsgName, #?gpb_field{name=FName, type=Type}, JValueExpr, TrUserDataVar, AnRes) -> [TrEmptySeq, TrAddElem, TrFinalize] = [gpb_gen_translators:find_translation([MsgName, FName], Op, AnRes) || Op <- [decode_init_default, decode_repeated_add_elem, decode_repeated_finalize]], ElemPath = [MsgName, FName, []], JElemVar = gpb_lib:var("JElem@~s", [FName]), ElemTrDecExpr = tr_wrap({ElemPath, decode, AnRes}, type_decode_expr(Type, JElemVar, TrUserDataVar), TrUserDataVar), ?expr('lists:reverse'( lists:foldl( fun('JElem@FVar', Acc) -> '[New|Acc]'('', Acc, 'TrUserData') end, '[]'([], 'TrUserData'), fj_array('JValue')), 'TrUserData'), [replace_tree('JValue', JValueExpr), replace_tree('JElem@FVar', JElemVar), replace_tree('', ElemTrDecExpr), replace_term('lists:reverse', TrFinalize), replace_term('[New|Acc]', TrAddElem), %% Get it from EMsg instead to avoid translator called twice? replace_term('[]', TrEmptySeq), replace_tree('TrUserData', TrUserDataVar)]). mapfield_decode_expr(MsgName, #?gpb_field{name=FName, type={map, KT, VT}=FType}, JValueExpr, TrUserDataVar, AnRes) -> MapMsgName = gpb_lib:map_type_to_msg_name(KT, VT), ElemPath = [MsgName, FName, []], [TrEmptySeq, TrAddElem, TrFinalize] = [gpb_gen_translators:find_translation([MsgName, FName], Op, AnRes) || Op <- [decode_init_default, decode_repeated_add_elem, decode_repeated_finalize]], KElemDecExpr = tr_wrap( {ElemPath, decode, AnRes}, map_key_type_decode_expr(KT, ?expr(Key), TrUserDataVar), TrUserDataVar), VElemDecExpr = tr_wrap( {ElemPath, decode, AnRes}, type_decode_expr(VT, ?expr(Value), TrUserDataVar), TrUserDataVar), ElemDecExpr = tr_wrap( {ElemPath, decode, AnRes}, type_decode_expr(FType, JValueExpr, TrUserDataVar), TrUserDataVar), ?expr(fj_mapfield_fold( '', fun 'new-[]'/2, fun 'add-elem-[New|Acc]'/3, fun 'lists:reverse'/2, fun(Key) -> '' end, fun(Value) -> '' end, 'Tr(JValueExpr,TrUserData)', 'TrUserData'), [replace_term('', MapMsgName), %% to avoid translator called twice? replace_term('new-[]', TrEmptySeq), % Get it from EMsg instead? replace_term('add-elem-[New|Acc]', TrAddElem), replace_term('lists:reverse', TrFinalize), replace_tree('', KElemDecExpr), replace_tree('', VElemDecExpr), replace_tree('Tr(JValueExpr,TrUserData)', ElemDecExpr), replace_tree('TrUserData', TrUserDataVar)]). tr_wrap({ElemPath, Op, AnRes}, Expr, TrUserDataVar) -> Tr = gpb_gen_translators:find_translation(ElemPath, Op, AnRes), ?expr('Tr'('', 'TrUserData'), [replace_term('Tr', Tr), replace_tree('', Expr), replace_tree('TrUserData', TrUserDataVar)]). tag_wrap(Tag, Expr) -> ?expr({'', ''}, [replace_term('', Tag), replace_tree('', Expr)]). map_key_type_decode_expr(Type, JValueExpr, TrUserDataVar) when Type /= bool -> type_decode_expr(Type, JValueExpr, TrUserDataVar); map_key_type_decode_expr(bool, JValueExpr, TrUserDataVar) -> %% All types that can appear as map keys are acceptable %% as string on JSON->internal. Except booleans, so %% need to make an extra pass for them JValueExpr2 = ?expr(case '' of <<"true">> -> true; <<"false">> -> false end, [replace_tree('', JValueExpr)]), type_decode_expr(bool, JValueExpr2, TrUserDataVar). type_decode_expr(Type, JValueExpr, TrUserDataVar) -> case Type of Int32 when Int32 == sint32; Int32 == int32; Int32 == uint32 -> ?expr(fj_int('Var'), [replace_tree('Var', JValueExpr)]); Int64 when Int64 == sint64; Int64 == int64; Int64 == uint64 -> ?expr(fj_int('Var'), [replace_tree('Var', JValueExpr)]); bool -> ?expr(fj_bool('Var'), [replace_tree('Var', JValueExpr)]); {enum,EnumName} -> DecodeEnumFn = gpb_lib:mk_fn(fj_enum_, EnumName), ?expr('fj_enum_'('Var'), [replace_tree('Var', JValueExpr), replace_term('fj_enum_', DecodeEnumFn)]); Int32 when Int32 == fixed32; Int32 == sfixed32 -> ?expr(fj_int('Var'), [replace_tree('Var', JValueExpr)]); Int64 when Int64 == fixed64; Int64 == sfixed64 -> ?expr(fj_int('Var'), [replace_tree('Var', JValueExpr)]); Float when Float == float; Float == double -> ?expr(fj_float('Var'), [replace_tree('Var', JValueExpr)]); string -> ?expr(fj_string('Var'), [replace_tree('Var', JValueExpr)]); bytes -> ?expr(fj_bytes('Var'), [replace_tree('Var', JValueExpr)]); {msg,MsgName} -> FnName = gpb_lib:mk_fn(from_json_msg_, MsgName), ?expr('from_json_msg_'('Var', 'TrUserData'), [replace_term('from_json_msg_', FnName), replace_tree('Var', JValueExpr), replace_tree('TrUserData', TrUserDataVar)]); {map,_KT,_VT} -> JValueExpr; % handled on a higher level {group,GName} -> FnName = gpb_lib:mk_fn(from_json_msg_, GName), ?expr('from_json_msg_'('Var', 'TrUserData'), [replace_term('from_json_msg_', FnName), replace_tree('Var', JValueExpr), replace_tree('TrUserData', TrUserDataVar)]) end. format_helpers(Defs, AnRes, Opts) -> [format_json_msg_iterator_helpers(Defs, Opts), format_json_array_helpers(Defs, AnRes, Opts), format_mapfield_helper(AnRes, Opts), format_json_type_helpers(Defs, AnRes, Opts)]. format_json_msg_iterator_helpers(Defs, Opts) -> HaveNonemptyMsgs = have_nonempty_msg(Defs), HaveMapIterators = gpb_lib:target_has_map_iterators(Opts), [[case gpb_lib:json_object_format_by_opts(Opts) of eep18 -> [gpb_codegen:format_fn( fj_iter, fun([{}]) -> []; (Proplist) -> Proplist end), gpb_codegen:format_fn( fj_next, fun([{Key,Value} | Rest]) -> {Key, Value, Rest}; ([]) -> none end)]; {proplist} -> [gpb_codegen:format_fn( fj_iter, fun({Proplist}) -> Proplist end), gpb_codegen:format_fn( fj_next, fun([{Key,Value} | Rest]) -> {Key, Value, Rest}; ([]) -> none end)]; {Tag, proplist} -> [gpb_codegen:format_fn( fj_iter, fun({struct, Proplist}) -> Proplist end, [replace_term(struct, Tag)]), gpb_codegen:format_fn( fj_next, fun([{Key,Value} | Rest]) -> {Key, Value, Rest}; ([]) -> none end)]; map when HaveMapIterators -> [gpb_codegen:format_fn( fj_iter, fun(Map) -> maps:iterator(Map) end), gpb_codegen:format_fn( fj_next, fun(Iter) -> maps:next(Iter) end)]; map when not HaveMapIterators -> [gpb_codegen:format_fn( fj_iter, fun(Map) -> maps:to_list(Map) end), gpb_codegen:format_fn( fj_next, fun([{Key,Value} | Rest]) -> {Key, Value, Rest}; ([]) -> none end)] end] || HaveNonemptyMsgs]. format_json_array_helpers(Defs, #anres{map_types=MapTypes}, Opts) -> HaveMapfields = sets:size(MapTypes) > 0, HaveRepeated = have_repeated_fields(Defs) orelse HaveMapfields, [[case gpb_lib:json_array_format_by_opts(Opts) of list -> [gpb_lib:nowarn_unused_function(fj_array, 1), gpb_codegen:format_fn( fj_array, fun(L) -> L end)]; {Tag, list} -> [gpb_lib:nowarn_unused_function(fj_array, 1), gpb_codegen:format_fn( fj_array, fun({array, L}) -> L end, [replace_term(array, Tag)])] end] || HaveRepeated]. format_mapfield_helper(#anres{map_types=MapTypes}, Opts) -> HaveMapfields = sets:size(MapTypes) > 0, [[gpb_codegen:format_fn( fj_mapfield_fold, fun(MapMsgName, New, AddElem, Finalize, DecodeKey, DecodeValue, JMapfield, TrUserData) -> Finalize( fj_mapfield_fold_aux(fj_iter(JMapfield), MapMsgName, DecodeKey, DecodeValue, AddElem, New([], TrUserData), TrUserData), TrUserData) end), case gpb_lib:get_2tuples_or_maps_for_maptype_fields_by_opts(Opts) of '2tuples' -> gpb_codegen:format_fn( fj_mapfield_fold_aux, fun(JIter, MapMsgName, DecodeKey, DecodeValue, AddElem, Acc, TrUserData) -> case fj_next(JIter) of {JKey, JValue, JRest} -> EKey = DecodeKey(JKey), EValue = DecodeValue(JValue), TmpMsg = {MapMsgName, EKey, EValue}, Acc1 = AddElem(TmpMsg, Acc, TrUserData), call_self(JRest, MapMsgName, DecodeKey, DecodeValue, AddElem, Acc1, TrUserData); none -> Acc end end); maps -> {EKey, EValue} = {?expr(EKey), ?expr(EValue)}, MkMapExpr = gpb_lib:map_create([{key, EKey}, {value, EValue}], Opts), gpb_codegen:format_fn( fj_mapfield_fold_aux, fun(JIter, MapMsgName, DecodeKey, DecodeValue, AddElem, Acc, TrUserData) -> case fj_next(JIter) of {JKey, JValue, JRest} -> EKey = DecodeKey(JKey), EValue = DecodeValue(JValue), TmpMsg = '#{key => EKey, value => EValue}', Acc1 = AddElem(TmpMsg, Acc, TrUserData), call_self(JRest, MapMsgName, DecodeKey, DecodeValue, AddElem, Acc1, TrUserData); none -> Acc end end, [replace_tree('#{key => EKey, value => EValue}', MkMapExpr)]) end] || HaveMapfields]. format_json_type_helpers(Defs, #anres{used_types=UsedTypes}, Opts) -> StrBin = gpb_lib:get_strings_as_binaries_by_opts(Opts), NeedIntType = lists:any(fun(T) -> gpb_lib:smember(T, UsedTypes) end, [sint32, int32, uint32, sint64, int64, uint64, fixed32, sfixed32, fixed64, sfixed64]), NeedBoolType = gpb_lib:smember(bool, UsedTypes), NeedEnumType = lists:any(fun({enum,_}) -> true; (_) -> false end, sets:to_list(UsedTypes)), NeedFloatType = lists:any(fun(T) -> gpb_lib:smember(T, UsedTypes) end, [float, double]), NeedStringType = gpb_lib:smember(string, UsedTypes), NeedBytesType = gpb_lib:smember(bytes, UsedTypes), [[gpb_codegen:format_fn( fj_int, %% Leading zeros are not allowed according to RFC7159, %% so no octal representation decoding or anything such is needed. fun(N) when is_integer(N) -> N; (S) when is_binary(S) -> list_to_integer(binary_to_list(S)); (S) when is_list(S) -> list_to_integer(S) end) || NeedIntType], [[%% The protobuf also accepts both boolean values as well as %% string representation of the same. It seems to also accepts %% string representations of 0 and 1, but oddly enough %% not the integers 0 and 1. (protobuf 3.8.0-rc1) gpb_codegen:format_fn( fj_bool, fun(B) when is_boolean(B) -> B; (B) when is_binary(B) -> case fj_bool_bin_casecanon(B, <<>>) of (<<"true">>) -> true; (<<"false">>) -> false; (<<"1">>) -> true; (<<"0">>) -> false end; (S) when is_list(S) -> call_self(list_to_binary(S)) end), gpb_codegen:format_fn( fj_bool_bin_casecanon, % to lowercase fun(<>, Acc) when $A =< C, C =< $Z -> call_self(Rest, <>); % $a - $A == 32 (<>, Acc) -> call_self(Rest, <>); (<<>>, Acc) -> Acc end)] || NeedBoolType], [begin JSymStrsToSyms = canonify_enum_jstrs(unalias_enum_syms(Enums), Opts), IntStrsToSyms = enum_ints_to_syms(Enums), [{_, Sym1} | _] = JSymStrsToSyms, StrSyms = JSymStrsToSyms ++ IntStrsToSyms, gpb_codegen:format_fn( gpb_lib:mk_fn(fj_enum_, EnumName), fun(S) when is_binary(S) -> case fj_casecanon_enum(S, <<>>) of '<<"Str">>' -> ''; _ -> 'FirstSym' end; (N) when is_integer(N) -> 'd_enum_'(N); (S) when is_list(S) -> call_self(list_to_binary(S)) end, [replace_term('d_enum_', gpb_lib:mk_fn(d_enum_, EnumName)), replace_term('FirstSym', Sym1), repeat_clauses( '<<"Str">>', [[replace_tree('<<"Str">>', bstr(Str)), replace_term('', Sym)] || {Str, Sym} <- StrSyms])]) end || {{enum,EnumName}, Enums} <- Defs, NeedEnumType], [%% Extra enum helper(s) case proplists:get_bool(json_case_insensitive_enum_parsing, Opts) of true -> [gpb_codegen:format_fn( fj_casecanon_enum, fun(<>, Acc) -> call_self(Tl, <>); (<<>>, Acc) -> Acc end), gpb_codegen:format_fn( fj_casecanon_char, % to uppercase fun(C) when $a =< C, C =< $z -> C - 32; % $a - $A == 32 ($-) -> $_; (C) -> C end)]; false -> [gpb_codegen:format_fn( fj_casecanon_enum, fun(B, _Acc) -> B end)] end || NeedEnumType], [[%% float with helper gpb_codegen:format_fn( fj_float, fun(N) when is_integer(N) -> float(N); (N) when is_float(N) -> N; (S) when is_binary(S) -> call_self(binary_to_list(S)); ("NaN") -> nan; ("Infinity") -> infinity; ("-Infinity") -> '-infinity'; (S) when is_list(S) -> case fj_d_num2e(S, integer, "") of {integer, S2} -> float(list_to_integer(S2)); {float, S2} -> list_to_float(S2) end end), gpb_codegen:format_fn( fj_d_num2e, fun("-"++Rest, St, Acc) -> call_self(Rest, St, "-" ++ Acc); ("+"++Rest, St, ""=Acc) -> %% Ignore leading plus call_self(Rest, St, Acc); ([D | Rest], St, Acc) when $0 =< D, D =< $9 -> call_self(Rest, St, [D | Acc]); ("."++_ = S, _, Acc) when Acc =:= ""; Acc =:= "-" -> %% Initial leading decimal point: prepend a leading zero %% and continue processing call_self(S, float, "0" ++ Acc); ("."++[D|_] = S, _, Acc) when $0 =< D, D =< $9 -> %% A decimal point: the rest (if wellformed as per rfc 7159) %% will be parseable as an erlang float {float, lists:reverse(Acc, S)}; ("."++Rest, _, Acc) -> %% A decimal point followed by non-digit (exponent or %% or end of string): Turn "." into ".0" to make %% it parseable as an Erlang float {float, lists:reverse(Acc, ".0" ++ Rest)}; ([E | _]=Rest, integer, Acc) when E == $e; E == $E -> %% Exponent: not preceded by a decimal point: %% add ".0" before the exponent, and it will (if wellformed) %% be parseable as an erlang float {float, lists:reverse(Acc, ".0"++Rest)}; ("", St, Acc) -> {St, lists:reverse(Acc)} end)] || NeedFloatType], [[%% String case StrBin of true -> gpb_codegen:format_fn( fj_string, fun(S) when is_binary(S); is_list(S) -> unicode:characters_to_binary(S) end); false -> gpb_codegen:format_fn( fj_string, fun(S) when is_binary(S); is_list(S) -> unicode:characters_to_list(S) end) end || NeedStringType]], [gpb_codegen:format_fn( fj_bytes, fun(S) when is_binary(S); is_list(S) -> %% Convert any url-safe encoding to normal base64 encoding B64 = <<< $+; C =:= $_ -> $/; true -> C end>> || <> <= iolist_to_binary(S)>>, base64:decode(B64) end) || NeedBytesType]]. occurrence_or_mapfield(repeated, {map, _, _}) -> mapfield; occurrence_or_mapfield(Occurrence, _) -> Occurrence. have_nonempty_msg([{{msg,_MsgName}, Fields} | Rest]) -> if Fields /= [] -> true; Fields == [] -> have_nonempty_msg(Rest) end; have_nonempty_msg([_ | Rest]) -> have_nonempty_msg(Rest); have_nonempty_msg([]) -> false. have_repeated_fields([{{msg,_Msg}, Fields} | Rest]) -> case have_repeated_aux(Fields) of true -> true; false -> have_repeated_fields(Rest) end; have_repeated_fields([_ | Rest]) -> have_repeated_fields(Rest); have_repeated_fields([]) -> false. have_repeated_aux([#?gpb_field{occurrence = repeated} | _]) -> true; have_repeated_aux([_ | Rest]) -> have_repeated_aux(Rest); have_repeated_aux([]) -> false. bstr(S) when is_list(S) -> %% Want <<"str">> and not <<102,110,97,109,101>> %% so make a text node erl_syntax:text(?ff("<<~p>>", [S])). canonify_enum_jstrs(JStrsToSyms, _Opts) -> [{gpb_lib:uppercase(JStr), Sym} || {JStr, Sym} <- JStrsToSyms]. unalias_enum_syms(Enums) -> %% Enum can also have {option, allow_alias, true} elements. Enums1 = [Enum || {_Sym,_Num}=Enum <- Enums], %% In case of aliases: make a mapping: %% If .proto is: Then resulting mapping is: %% enum E { E_0 = 0; [{"E_0", 'E_0'}, %% E_1a = 1; {"E_1a", 'E_1a'}, %% E_1b = 1; } {"E_1b", 'E_1a'}] [{atom_to_list(Sym), ensure_sym_unaliased(Sym, Enums1)} || {Sym, _Num} <- Enums1]. ensure_sym_unaliased(Sym, Enums) -> {Sym, Num} = lists:keyfind(Sym, 1, Enums), {Sym1, Num} = lists:keyfind(Num, 2, Enums), Sym1. enum_ints_to_syms(Enums) -> [{integer_to_list(Num), Sym} || {Sym,Num} <- gpb_lib:unalias_enum(Enums)].