-module(euneus_encoder). %% -------------------------------------------------------------------- %% API function exports %% -------------------------------------------------------------------- -export([encode/2]). -export([continue/2]). -export([codec_callback/2]). -export([key_to_binary/1]). -export([escape/1]). -export([encode_integer/2]). -export([encode_float/2]). -export([encode_atom/2]). -export([encode_list/2]). -export([encode_map/2]). -export([encode_tuple/2]). -export([encode_pid/2]). -export([encode_port/2]). -export([encode_reference/2]). -export([encode_term/2]). % -ignore_xref([continue/2]). -ignore_xref([codec_callback/2]). -ignore_xref([key_to_binary/1]). -ignore_xref([escape/1]). -ignore_xref([encode_integer/2]). -ignore_xref([encode_float/2]). -ignore_xref([encode_atom/2]). -ignore_xref([encode_list/2]). -ignore_xref([encode_map/2]). -ignore_xref([encode_tuple/2]). -ignore_xref([encode_pid/2]). -ignore_xref([encode_port/2]). -ignore_xref([encode_reference/2]). -ignore_xref([encode_term/2]). -hank([ {unnecessary_function_arguments, [ encode_float/2, encode_integer/2 ]} ]). %% -------------------------------------------------------------------- %% Type exports %% -------------------------------------------------------------------- -export_type([options/0]). -export_type([codec_fun/0]). -export_type([codec_result/0]). -export_type([codec_callback/0]). -export_type([is_proplist/0]). -export_type([encode/1]). -export_type([state/0]). %% -------------------------------------------------------------------- %% Macros %% -------------------------------------------------------------------- -define(IS_MIN(X, Min), (is_integer(X) andalso X >= Min) ). -define(IN_RANGE(X, Min, Max), (is_integer(X) andalso X >= Min andalso X =< Max) ). % -elvis([{elvis_style, no_macros, #{allow => ['IS_MIN', 'IN_RANGE']}}]). %% -------------------------------------------------------------------- %% Types %% -------------------------------------------------------------------- -type options() :: #{ codecs => [codec()], codec_callback => codec_callback(), nulls => [term()], skip_values => [term()], key_to_binary => fun((term()) -> binary()), sort_keys => boolean(), proplists => boolean() | {true, is_proplist()}, escape => fun((binary()) -> iodata()), encode_integer => encode(integer()), encode_float => encode(float()), encode_atom => encode(atom()), encode_list => encode(list()), encode_map => encode(map()), encode_tuple => encode(tuple()), encode_pid => encode(pid()), encode_port => encode(port()), encode_reference => encode(reference()), encode_term => encode(term()) }. -type codec() :: timestamp | datetime | ipv4 | ipv6 | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}} | codec_fun() | custom_codec(). -export_type([codec/0]). -type codec_fun() :: fun((tuple()) -> codec_result()). -type codec_result() :: next | {halt, term()}. -type custom_codec() :: term(). -type codec_callback() :: fun((codec(), tuple()) -> codec_result()). -type is_proplist() :: fun((list()) -> boolean()). -type encode(Type) :: fun((Type, state()) -> iodata()). -record(state, { codecs :: [codec()], codec_callback :: codec_callback(), nulls :: #{term() := null}, skip_values :: #{term() := skip}, key_to_binary :: fun((term()) -> binary()), sort_keys :: boolean(), proplists :: boolean() | {true, is_proplist()}, escape :: fun((binary()) -> iodata()), encode_integer :: encode(integer()), encode_float :: encode(float()), encode_atom :: encode(atom()), encode_list :: encode(list()), encode_map :: encode(map()), encode_tuple :: encode(tuple()), encode_pid :: encode(pid()), encode_port :: encode(port()), encode_reference :: encode(reference()), encode_term :: encode(term()) }). -opaque state() :: #state{}. %% -------------------------------------------------------------------- %% DocTest %% -------------------------------------------------------------------- -if(?OTP_RELEASE >= 27). -ifdef(TEST). -include_lib("doctest/include/doctest.hrl"). -endif. -endif. %% -------------------------------------------------------------------- %% API functions %% -------------------------------------------------------------------- -spec encode(Term, Options) -> iodata() when Term :: term(), Options :: options(). %% @doc Encode a term into an iodata JSON. %% %% Example: %% %% ``` %% 1> euneus_encoder:encode(foo, #{}). %% [$", <<"foo">>, $"] %% ''' %% %% Option details: %% %% encode(Term, Opts) -> json:encode(Term, encoder(new_state(Opts))). -spec continue(Term, State) -> iodata() when Term :: term(), State :: state(). %% @doc Used by encoders to continue encoding. %% %% Example: %% %% ``` %% 1> euneus_encoder:encode({}, #{ %% .. encode_tuple => fun(Tuple, State) -> %% .. euneus_encoder:continue(tuple_to_list(Tuple), State) %% .. end %% .. }). %% <<"[]">> %% ''' continue(Bin, State) when is_binary(Bin) -> (State#state.escape)(Bin); continue(Int, State) when is_integer(Int) -> (State#state.encode_integer)(Int, State); continue(Float, State) when is_float(Float) -> (State#state.encode_float)(Float, State); continue(Atom, State) when is_atom(Atom) -> (State#state.encode_atom)(Atom, State); continue(List, State) when is_list(List) -> (State#state.encode_list)(List, State); continue(Map, State) when is_map(Map) -> (State#state.encode_map)(Map, State); continue(Tuple, State) when is_tuple(Tuple) -> case traverse_codecs(State#state.codecs, State#state.codec_callback, Tuple) of NewTuple when is_tuple(NewTuple) -> (State#state.encode_tuple)(NewTuple, State); NewTerm -> continue(NewTerm, State) end; continue(Pid, State) when is_pid(Pid) -> (State#state.encode_pid)(Pid, State); continue(Port, State) when is_port(Port) -> (State#state.encode_port)(Port, State); continue(Ref, State) when is_reference(Ref) -> (State#state.encode_reference)(Ref, State); continue(Term, State) -> (State#state.encode_term)(Term, State). -spec codec_callback(Codec, Tuple) -> codec_result() when Codec :: codec(), Tuple :: tuple(). codec_callback(timestamp, Tuple) -> timestamp_codec_callback(Tuple); codec_callback(datetime, Tuple) -> datetime_codec_callback(Tuple); codec_callback(ipv4, Tuple) -> ipv4_codec_callback(Tuple); codec_callback(ipv6, Tuple) -> ipv6_codec_callback(Tuple); codec_callback({records, Records}, Tuple) -> records_codec_callback(Tuple, Records); codec_callback(CodecFun, Tuple) -> CodecFun(Tuple). -spec key_to_binary(Term) -> binary() when Term :: binary() | string() | atom() | integer(). key_to_binary(Bin) when is_binary(Bin) -> Bin; key_to_binary(Str) when is_list(Str) -> iolist_to_binary(Str); key_to_binary(Atom) when is_atom(Atom) -> atom_to_binary(Atom, utf8); key_to_binary(Int) when is_integer(Int) -> integer_to_binary(Int, 10). -spec escape(binary()) -> iodata(). escape(Binary) -> json:encode_binary(Binary). -spec encode_integer(integer(), state()) -> iodata(). encode_integer(Integer, _State) -> erlang:integer_to_binary(Integer, 10). -spec encode_float(float(), state()) -> iodata(). -if(?OTP_RELEASE >= 25). encode_float(Float, _State) -> erlang:float_to_binary(Float, [short]). -else. encode_float(Float, _State) -> erlang:float_to_binary(Float, [compact, {decimals, 10}]). -endif. -spec encode_atom(atom(), state()) -> iodata(). encode_atom(true, _State) -> <<"true">>; encode_atom(false, _State) -> <<"false">>; encode_atom(Atom, #state{nulls = Nulls}) when is_map_key(Atom, Nulls) -> <<"null">>; encode_atom(Atom, #state{escape = Escape}) -> Escape(atom_to_binary(Atom, utf8)). -spec encode_list(list(), state()) -> iodata(). encode_list(List, #state{proplists = false} = State) -> json:encode_list(List, encoder(State)); encode_list(List, #state{proplists = true} = State) -> case is_proplist(List) of true -> encode_proplist(List, State); false -> json:encode_list(List, encoder(State)) end; encode_list(List, #state{proplists = {true, IsProplist}} = State) -> case IsProplist(List) of true -> encode_proplist(List, State); false -> json:encode_list(List, encoder(State)) end. -spec encode_map(map(), state()) -> iodata(). -if(?OTP_RELEASE >= 26). encode_map(Map, #state{sort_keys = false, skip_values = ValuesToSkip} = State) -> do_encode_map([ [$,, escape_map_key(Key, State), $: | continue(Value, State)] || Key := Value <- Map, not is_map_key(Value, ValuesToSkip) ]); encode_map(Map, State) -> encode_sort_keys_map(Map, State). -else. encode_map(Map, #state{sort_keys = false, skip_values = ValuesToSkip} = State) -> do_encode_map( maps:fold( fun(Key, Value, Acc) -> case is_map_key(Value, ValuesToSkip) of true -> []; false -> [ [ $,, escape_map_key(Key, State), $: | continue(Value, State) ] | Acc ] end end, [], Map ) ); encode_map(Map, State) -> encode_sort_keys_map(Map, State). -endif. -spec encode_tuple(tuple(), state()) -> no_return(). encode_tuple(Tuple, State) -> error(unsupported_tuple, [Tuple, State]). -spec encode_pid(pid(), state()) -> no_return(). encode_pid(Pid, State) -> error(unsupported_pid, [Pid, State]). -spec encode_port(port(), state()) -> no_return(). encode_port(Port, State) -> error(unsupported_port, [Port, State]). -spec encode_reference(reference(), state()) -> no_return(). encode_reference(Ref, State) -> error(unsupported_reference, [Ref, State]). -spec encode_term(term(), state()) -> no_return(). encode_term(Term, State) -> error(unsupported_term, [Term, State]). %% -------------------------------------------------------------------- %% Internal functions %% -------------------------------------------------------------------- % State new_state(Opts) -> #state{ codecs = maps:get(codecs, Opts, []), codec_callback = maps:get(codec_callback, Opts, fun codec_callback/2), nulls = maps:from_keys(maps:get(nulls, Opts, [null]), null), skip_values = maps:from_keys(maps:get(skip_values, Opts, [undefined]), skip), key_to_binary = maps:get(key_to_binary, Opts, fun key_to_binary/1), sort_keys = maps:get(sort_keys, Opts, false), proplists = maps:get(proplists, Opts, false), escape = maps:get(escape, Opts, fun escape/1), encode_integer = maps:get(encode_integer, Opts, fun encode_integer/2), encode_float = maps:get(encode_float, Opts, fun encode_float/2), encode_atom = maps:get(encode_atom, Opts, fun encode_atom/2), encode_list = maps:get(encode_list, Opts, fun encode_list/2), encode_map = maps:get(encode_map, Opts, fun encode_map/2), encode_tuple = maps:get(encode_tuple, Opts, fun encode_tuple/2), encode_pid = maps:get(encode_pid, Opts, fun encode_pid/2), encode_port = maps:get(encode_port, Opts, fun encode_port/2), encode_reference = maps:get(encode_reference, Opts, fun encode_reference/2), encode_term = maps:get(encode_term, Opts, fun encode_term/2) }. % Codecs traverse_codecs([Codec | Codecs], CodecCallback, Tuple) -> case CodecCallback(Codec, Tuple) of next -> traverse_codecs(Codecs, CodecCallback, Tuple); {halt, NewTerm} -> NewTerm end; traverse_codecs([], _CodecCallback, Tuple) -> Tuple. timestamp_codec_callback({MegaSecs, Secs, MicroSecs} = Timestamp) when ?IS_MIN(MegaSecs, 0), ?IS_MIN(Secs, 0), ?IS_MIN(MicroSecs, 0) -> MilliSecs = MicroSecs div 1000, {{YYYY, MM, DD}, {H, M, S}} = calendar:now_to_datetime(Timestamp), DateTime = iolist_to_binary( io_lib:format( "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0B.~3.10.0BZ", [YYYY, MM, DD, H, M, S, MilliSecs] ) ), {halt, DateTime}; timestamp_codec_callback(_Tuple) -> next. datetime_codec_callback({{YYYY, MM, DD}, {H, M, S}}) when ?IS_MIN(YYYY, 0), ?IN_RANGE(MM, 1, 12), ?IN_RANGE(DD, 1, 31), ?IN_RANGE(H, 0, 23), ?IN_RANGE(M, 0, 59), ?IN_RANGE(S, 0, 59) -> DateTime = iolist_to_binary( io_lib:format( "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ", [YYYY, MM, DD, H, M, S] ) ), {halt, DateTime}; datetime_codec_callback(_Tuple) -> next. ipv4_codec_callback({_A, _B, _C, _D} = Tuple) -> case inet_parse:ntoa(Tuple) of {error, einval} -> next; Ipv4 -> {halt, list_to_binary(Ipv4)} end; ipv4_codec_callback(_Tuple) -> next. ipv6_codec_callback({_A, _B, _C, _D, _E, _F, _G, _H} = Tuple) -> case inet_parse:ntoa(Tuple) of {error, einval} -> next; Ipv6 -> {halt, list_to_binary(Ipv6)} end; ipv6_codec_callback(_Tuple) -> next. records_codec_callback(Tuple, Records) when tuple_size(Tuple) > 1 -> Name = element(1, Tuple), case Records of #{Name := {Fields, Size}} when tuple_size(Tuple) =:= Size -> [Name | Values] = tuple_to_list(Tuple), Map = proplists:to_map(lists:zip(Fields, Values)), {halt, Map}; #{} -> next end; records_codec_callback(_Tuple, _Records) -> next. % Encoders encoder(State) -> fun(Term, _Encode) -> continue(Term, State) end. encode_proplist(Proplist, State) -> continue(proplists:to_map(Proplist), State). is_proplist([]) -> false; is_proplist(List) -> lists:all(fun is_proplist_prop/1, List). % Must be the same types handled by key_to_binary/1. is_proplist_prop({Key, _}) -> is_binary(Key) orelse is_list(Key) orelse is_atom(Key) orelse is_integer(Key); is_proplist_prop(Key) -> is_atom(Key). encode_sort_keys_map(Map, #state{sort_keys = true} = State) -> do_encode_map([ [$,, escape_map_key(Key, State), $: | continue(Value, State)] || {Key, Value} <- lists:keysort(1, maps:to_list(Map)), not is_map_key(Value, State#state.skip_values) ]). escape_map_key(Key, State) -> (State#state.escape)((State#state.key_to_binary)(Key)). do_encode_map([]) -> <<"{}">>; do_encode_map([[_Comma | Entry] | Rest]) -> ["{", Entry, Rest, "}"].