diff --git a/README.md b/README.md index 8f94918..acc0227 100644 --- a/README.md +++ b/README.md @@ -36,32 +36,38 @@ The second argument of `euneus:encode/2` are options, and this is the spec: ```erlang -type options() :: #{ + codecs => [codec()], nulls => [term()], skip_values => [term()], - escape => fun((binary()) -> iodata()), - integer => encode(integer()), - float => encode(float()), - atom => encode(atom()), - list => encode(list()), - proplist => boolean() | {true, is_proplist()}, - map => encode(map()), + key_to_binary => fun((term()) -> binary()), sort_keys => boolean(), - tuple => - encode(tuple()) - | [ - datetime - | timestamp - | ipv4 - | ipv6 - | {record, - #{Name :: atom() => {Fields :: [atom()], Size :: pos_integer()}} - | [{Name :: atom(), Fields :: [atom()]}]} - | fun((tuple()) -> next | {halt, term()}) - ], - pid => encode(pid()), - port => encode(port()), - reference => encode(reference()) + 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()) }. + +-type codec() :: + datetime + | timestamp + | ipv4 + | ipv6 + % {records, #{foo => {record_info(fields, foo), record_info(size, foo)}}} + | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}} + | codec_callback(). + +-type codec_callback() :: fun((tuple()) -> next | {halt, term()}). + +-type is_proplist() :: fun((list()) -> boolean()). + +-type encode(Type) :: fun((Type, json:encoder(), state()) -> iodata()). ``` ### Encode example @@ -86,33 +92,35 @@ The second argument of `euneus:decode/2` are options, and this is the spec: ```erlang -type options() :: #{ - codecs => [ - copy - | timestamp - | datetime - | ipv4 - | ipv6 - | pid - | port - | reference - | fun((binary()) -> next | {halt, term()}) - ], + codecs => [codec()], + null => term(), + binary_to_float => json:from_binary_fun(), + binary_to_integer => json:from_binary_fun(), array_start => json:array_start_fun(), array_push => json:array_push_fun(), array_finish => json:array_finish_fun(), object_start => json:object_start_fun(), object_keys => - binary - | copy + copy | atom | existing_atom | json:from_binary_fun(), object_push => json:object_push_fun(), - object_finish => json:object_finish_fun(), - float => json:from_binary_fun(), - integer => json:from_binary_fun(), - null => term() + object_finish => json:object_finish_fun() }. + +-type codec() :: + copy + | timestamp + | datetime + | ipv4 + | ipv6 + | pid + | port + | reference + | codec_callback(). + +-type codec_callback() :: fun((binary()) -> next | {halt, term()}). ``` ### Decode example diff --git a/src/euneus_decoder.erl b/src/euneus_decoder.erl index ccae0f8..a808317 100644 --- a/src/euneus_decoder.erl +++ b/src/euneus_decoder.erl @@ -12,7 +12,7 @@ % > The call json:decode_continue(JSON::binary(), State::json:continuation_state()) % > contains an opaque term as 2nd argument when terms of different types % > are expected in these positions --dialyzer({no_opaque, [do_decode_continue/2]}). +-dialyzer({no_opaque, [decode_continue/2]}). %% -------------------------------------------------------------------- %% Macros @@ -31,38 +31,47 @@ -elvis([{elvis_style, no_macros, #{allow => ['IS_NUMBER', 'IN_RANGE']}}]). %% -------------------------------------------------------------------- -%% Types (and their exports) +%% Type exports +%% -------------------------------------------------------------------- + +-export_type([options/0]). +-export_type([codec/0]). +-export_type([codec_callback/0]). + +%% -------------------------------------------------------------------- +%% Types %% -------------------------------------------------------------------- -type options() :: #{ - codecs => [ - copy - | timestamp - | datetime - | ipv4 - | ipv6 - | pid - | port - | reference - | fun((binary()) -> next | {halt, term()}) - ], + codecs => [codec()], + null => term(), + binary_to_float => json:from_binary_fun(), + binary_to_integer => json:from_binary_fun(), array_start => json:array_start_fun(), array_push => json:array_push_fun(), array_finish => json:array_finish_fun(), object_start => json:object_start_fun(), object_keys => - binary - | copy + copy | atom | existing_atom | json:from_binary_fun(), object_push => json:object_push_fun(), - object_finish => json:object_finish_fun(), - float => json:from_binary_fun(), - integer => json:from_binary_fun(), - null => term() + object_finish => json:object_finish_fun() }. --export_type([options/0]). + +-type codec() :: + copy + | timestamp + | datetime + | ipv4 + | ipv6 + | pid + | port + | reference + | codec_callback(). + +-type codec_callback() :: fun((binary()) -> next | {halt, term()}). %% -------------------------------------------------------------------- %% API functions @@ -70,9 +79,9 @@ -spec decode(binary(), options()) -> term(). decode(JSON, Opts) when is_binary(JSON), is_map(Opts) -> - Codecs = norm_codecs(maps:get(codecs, Opts, [])), + Codecs = maps:get(codecs, Opts, []), Decoders = decoders(Codecs, Opts), - do_decode(Codecs, JSON, Decoders). + decode(Codecs, JSON, Decoders). %% -------------------------------------------------------------------- %% Internal functions @@ -84,22 +93,22 @@ decode(JSON, Opts) when is_binary(JSON), is_map(Opts) -> % > decode(<<"\"1970-01-01T00:00:00Z\"">>, #{codecs => [datetime]}). % > {{1970,1,1},{0,0,0}} % Otherwise, the result above will be the date as string. -do_decode([], JSON, Decoders) -> +decode([], JSON, Decoders) -> {Result, [], <<>>} = json:decode(JSON, [], Decoders), Result; -do_decode(Codecs, JSON, Decoders) -> +decode(Codecs, JSON, Decoders) -> case json:decode_start(JSON, [], Decoders) of {Result, [], <<>>} -> traverse_codecs(Codecs, Result); Continue -> - do_decode_continue(Continue, JSON) + decode_continue(Continue, JSON) end. -do_decode_continue({continue, State}, JSON) -> - do_decode_continue(json:decode_continue(JSON, State), JSON); -do_decode_continue({Result, [], <<>>}, _JSON) -> +decode_continue({continue, State}, JSON) -> + decode_continue(json:decode_continue(JSON, State), JSON); +decode_continue({Result, [], <<>>}, _JSON) -> Result; -do_decode_continue({_Result, [], Rest}, _JSON) -> +decode_continue({_Result, [], Rest}, _JSON) -> invalid_byte(Rest, 0). % This is a copy of json:invalid_byte/2, since it is not exported. @@ -110,35 +119,117 @@ invalid_byte(Bin, Skip) -> error_info(Skip) -> [{error_info, #{cause => #{position => Skip}}}]. +%% Decoders + +decoders(Codecs, Opts) -> + #{ + array_start => array_start_decoder(maps:get(array_start, Opts, empty)), + array_push => array_push_decoder(maps:get(array_push, Opts, traverse_codecs), Codecs), + array_finish => array_finish_decoder(maps:get(array_finish, Opts, ordered)), + object_push => object_push_decoder( + maps:get(object_push, Opts, push), + maps:get(object_keys, Opts, binary), + Codecs + ), + object_finish => object_finish_decoder(maps:get(object_finish, Opts, map)), + integer => maps:get(binary_to_integer, Opts, fun erlang:binary_to_integer/1), + float => maps:get(binary_to_float, Opts, fun erlang:binary_to_float/1), + % string => We skip this, since it transforms any string, including object keys. + null => maps:get(null, Opts, null) + }. + +array_start_decoder(empty) -> + fun(_) -> [] end; +array_start_decoder(Decoder) when is_function(Decoder, 1) -> + Decoder. + +array_push_decoder(traverse_codecs, Codecs) -> + fun(Elem, Acc) -> [traverse_codecs(Codecs, Elem) | Acc] end; +array_push_decoder(Decoder, _Codecs) when is_function(Decoder, 2) -> + Decoder. + +array_finish_decoder(ordered) -> + fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end; +array_finish_decoder(reversed) -> + fun(Acc, OldAcc) -> {Acc, OldAcc} end; +array_finish_decoder(Decoder) when is_function(Decoder, 2) -> + Decoder. + +object_push_decoder(push, binary, Codecs) -> + fun(Key, Value, Acc) -> + [{Key, traverse_codecs(Codecs, Value)} | Acc] + end; +object_push_decoder(push, copy, Codecs) -> + fun(Key, Value, Acc) -> + [{binary:copy(Key), traverse_codecs(Codecs, Value)} | Acc] + end; +object_push_decoder(push, atom, Codecs) -> + fun(Key, Value, Acc) -> + [{binary_to_atom(Key, utf8), traverse_codecs(Codecs, Value)} | Acc] + end; +object_push_decoder(push, existing_atom, Codecs) -> + fun(Key, Value, Acc) -> + [{binary_to_existing_atom(Key, utf8), traverse_codecs(Codecs, Value)} | Acc] + end; +object_push_decoder(push, NormKey, Codecs) when is_function(NormKey, 1) -> + fun(Key, Value, Acc) -> + [{NormKey(Key), traverse_codecs(Codecs, Value)} | Acc] + end; +object_push_decoder(Decoder, _NormKey, _Codecs) when is_function(Decoder, 3) -> + Decoder. + +object_finish_decoder(map) -> + fun(Acc, OldAcc) -> {maps:from_list(Acc), OldAcc} end; +object_finish_decoder(proplist) -> + fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end; +object_finish_decoder(reversed_proplist) -> + fun(Acc, OldAcc) -> {Acc, OldAcc} end; +object_finish_decoder(Decoder) when is_function(Decoder, 2) -> + Decoder. + %% Codecs -norm_codecs(Codecs) when is_list(Codecs) -> - [norm_codec(Codec) || Codec <- Codecs]. - -norm_codec(copy) -> - fun copy_codec/1; -norm_codec(timestamp) -> - fun timestamp_codec/1; -norm_codec(datetime) -> - fun datetime_codec/1; -norm_codec(ipv4) -> - fun ipv4_codec/1; -norm_codec(ipv6) -> - fun ipv6_codec/1; -norm_codec(pid) -> - fun pid_codec/1; -norm_codec(port) -> - fun port_codec/1; -norm_codec(reference) -> - fun reference_codec/1; -norm_codec(Codec) when is_function(Codec, 1) -> - Codec. - -copy_codec(Bin) -> +traverse_codecs([], Term) -> + Term; +traverse_codecs(Codecs, Bin) when is_binary(Bin) -> + do_traverse_codecs(Codecs, Bin); +traverse_codecs(_Codecs, Term) -> + Term. + +do_traverse_codecs([Codec | Codecs], Bin) -> + case codec_callback(Codec, Bin) of + next -> + do_traverse_codecs(Codecs, Bin); + {halt, Value} -> + Value + end; +do_traverse_codecs([], Bin) -> + Bin. + +codec_callback(copy, Bin) -> + copy_codec_callback(Bin); +codec_callback(timestamp, Bin) -> + timestamp_codec_callback(Bin); +codec_callback(datetime, Bin) -> + datetime_codec_callback(Bin); +codec_callback(ipv4, Bin) -> + ipv4_codec_callback(Bin); +codec_callback(ipv6, Bin) -> + ipv6_codec_callback(Bin); +codec_callback(pid, Bin) -> + pid_codec_callback(Bin); +codec_callback(port, Bin) -> + port_codec_callback(Bin); +codec_callback(reference, Bin) -> + reference_codec_callback(Bin); +codec_callback(Callback, Bin) -> + Callback(Bin). + +copy_codec_callback(Bin) -> {halt, binary:copy(Bin)}. % <<"\"1970-01-01T00:00:00.000Z\"">> = {0,0,0} -timestamp_codec( +timestamp_codec_callback( < {halt, - timestamp( + chars_to_timestamp( {Y4, Y3, Y2, Y1, M2, M1, D2, D1}, {H2, H1, Min2, Min1, S2, S1}, {MSec3, MSec2, MSec1} )}; -timestamp_codec(_Bin) -> +timestamp_codec_callback(_Bin) -> next. % <<"\"1970-01-01T00:00:00Z\"">> = {{1970,1,1},{0,0,0}} -datetime_codec( +datetime_codec_callback( <> @@ -192,24 +283,24 @@ datetime_codec( ?IS_NUMBER(S2), ?IS_NUMBER(S1) -> - {halt, datetime({Y4, Y3, Y2, Y1, M2, M1, D2, D1}, {H2, H1, Min2, Min1, S2, S1})}; -datetime_codec(_Bin) -> + {halt, chars_to_datetime({Y4, Y3, Y2, Y1, M2, M1, D2, D1}, {H2, H1, Min2, Min1, S2, S1})}; +datetime_codec_callback(_Bin) -> next. -timestamp(Date, Time, {MSec3, MSec2, MSec1}) -> - DateTime = datetime(Date, Time), +chars_to_timestamp(Date, Time, {MSec3, MSec2, MSec1}) -> + DateTime = chars_to_datetime(Date, Time), GregSeconds = calendar:datetime_to_gregorian_seconds(DateTime), Seconds = GregSeconds - 62167219200, MilliSeconds = chars_to_integer(MSec3, MSec2, MSec1), {Seconds div 1000000, Seconds rem 1000000, MilliSeconds * 1000}. -datetime(Date, Time) -> - {date(Date), time(Time)}. +chars_to_datetime(Date, Time) -> + {chars_to_date(Date), chars_to_time(Time)}. -date({Y4, Y3, Y2, Y1, M2, M1, D2, D1}) -> +chars_to_date({Y4, Y3, Y2, Y1, M2, M1, D2, D1}) -> {chars_to_integer(Y4, Y3, Y2, Y1), chars_to_integer(M2, M1), chars_to_integer(D2, D1)}. -time({H2, H1, Min2, Min1, S2, S1}) -> +chars_to_time({H2, H1, Min2, Min1, S2, S1}) -> {chars_to_integer(H2, H1), chars_to_integer(Min2, Min1), chars_to_integer(S2, S1)}. chars_to_integer(N2, N1) -> @@ -221,22 +312,22 @@ chars_to_integer(N3, N2, N1) -> chars_to_integer(N4, N3, N2, N1) -> ((N4 - $0) * 1000) + ((N3 - $0) * 100) + ((N2 - $0) * 10) + (N1 - $0). -ipv4_codec(<> = Bin) when +ipv4_codec_callback(<> = Bin) when ?IN_RANGE(A, 0, 255); ?IN_RANGE(B, 0, 255); ?IN_RANGE(C, 0, 255) -> - ipv4_codec_continue(Bin); -ipv4_codec(<> = Bin) when + ipv4_codec_parse_callback(Bin); +ipv4_codec_callback(<> = Bin) when ?IN_RANGE(A, 0, 255); ?IN_RANGE(B, 0, 255) -> - ipv4_codec_continue(Bin); -ipv4_codec(<> = Bin) when + ipv4_codec_parse_callback(Bin); +ipv4_codec_callback(<> = Bin) when ?IN_RANGE(A, 0, 255) -> - ipv4_codec_continue(Bin); -ipv4_codec(_Bin) -> + ipv4_codec_parse_callback(Bin); +ipv4_codec_callback(_Bin) -> next. -ipv4_codec_continue(Bin) -> +ipv4_codec_parse_callback(Bin) -> case inet_parse:ipv4_address(binary_to_list(Bin)) of {ok, IPv4} -> {halt, IPv4}; @@ -244,16 +335,16 @@ ipv4_codec_continue(Bin) -> next end. -ipv6_codec(<<$:, $:>>) -> +ipv6_codec_callback(<<$:, $:>>) -> {halt, {0, 0, 0, 0, 0, 0, 0, 0}}; -ipv6_codec(<<$:, $:, _/binary>> = Bin) -> - ipv6_codec_continue(Bin); -ipv6_codec(<<_/integer, _/integer, _/integer, _/integer, $:, _/binary>> = Bin) -> - ipv6_codec_continue(Bin); -ipv6_codec(_Bin) -> +ipv6_codec_callback(<<$:, $:, _/binary>> = Bin) -> + ipv6_codec_parse_callback(Bin); +ipv6_codec_callback(<<_/integer, _/integer, _/integer, _/integer, $:, _/binary>> = Bin) -> + ipv6_codec_parse_callback(Bin); +ipv6_codec_callback(_Bin) -> next. -ipv6_codec_continue(Bin) -> +ipv6_codec_parse_callback(Bin) -> case inet_parse:ipv6strict_address(binary_to_list(Bin)) of {ok, Ipv6} -> {halt, Ipv6}; @@ -261,117 +352,32 @@ ipv6_codec_continue(Bin) -> next end. -pid_codec(<<$<, _/binary>> = Bin) -> +pid_codec_callback(<<$<, _/binary>> = Bin) -> try {halt, list_to_pid(binary_to_list(Bin))} catch _:_ -> next end; -pid_codec(_Bin) -> +pid_codec_callback(_Bin) -> next. -port_codec(<<"#Port<", _/binary>> = Bin) -> +port_codec_callback(<<"#Port<", _/binary>> = Bin) -> try {halt, list_to_port(binary_to_list(Bin))} catch _:_ -> next end; -port_codec(_Bin) -> +port_codec_callback(_Bin) -> next. -reference_codec(<<"#Ref<", _/binary>> = Bin) -> +reference_codec_callback(<<"#Ref<", _/binary>> = Bin) -> try {halt, list_to_ref(binary_to_list(Bin))} catch _:_ -> next end; -reference_codec(_Bin) -> +reference_codec_callback(_Bin) -> next. - -%% Decoders - -decoders(Codecs, Opts) -> - #{ - array_start => array_start_decoder(maps:get(array_start, Opts, empty)), - array_push => array_push_decoder(maps:get(array_push, Opts, push), Codecs), - array_finish => array_finish_decoder(maps:get(array_finish, Opts, ordered)), - object_push => object_push_decoder( - maps:get(object_push, Opts, push), - maps:get(object_keys, Opts, binary), - Codecs - ), - object_finish => object_finish_decoder(maps:get(object_finish, Opts, map)), - integer => maps:get(binary_to_integer, Opts, fun erlang:binary_to_integer/1), - float => maps:get(binary_to_float, Opts, fun erlang:binary_to_float/1), - % string => We skip this, since it transforms any string, including object keys. - null => maps:get(null, Opts, null) - }. - -array_start_decoder(empty) -> - fun(_) -> [] end; -array_start_decoder(Decoder) when is_function(Decoder, 1) -> - Decoder. - -array_push_decoder(push, Codecs) -> - fun(Elem, Acc) -> [traverse_codecs(Codecs, Elem) | Acc] end; -array_push_decoder(Decoder, _Codecs) when is_function(Decoder, 2) -> - Decoder. - -array_finish_decoder(ordered) -> - fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end; -array_finish_decoder(reversed) -> - fun(Acc, OldAcc) -> {Acc, OldAcc} end; -array_finish_decoder(Decoder) when is_function(Decoder, 2) -> - Decoder. - -object_push_decoder(push, binary, Codecs) -> - fun(Key, Value, Acc) -> - [{Key, traverse_codecs(Codecs, Value)} | Acc] - end; -object_push_decoder(push, copy, Codecs) -> - fun(Key, Value, Acc) -> - [{binary:copy(Key), traverse_codecs(Codecs, Value)} | Acc] - end; -object_push_decoder(push, atom, Codecs) -> - fun(Key, Value, Acc) -> - [{binary_to_atom(Key, utf8), traverse_codecs(Codecs, Value)} | Acc] - end; -object_push_decoder(push, existing_atom, Codecs) -> - fun(Key, Value, Acc) -> - [{binary_to_existing_atom(Key, utf8), traverse_codecs(Codecs, Value)} | Acc] - end; -object_push_decoder(push, NormKey, Codecs) when is_function(NormKey, 1) -> - fun(Key, Value, Acc) -> - [{NormKey(Key), traverse_codecs(Codecs, Value)} | Acc] - end; -object_push_decoder(Decoder, _NormKey, _Codecs) when is_function(Decoder, 3) -> - Decoder. - -object_finish_decoder(map) -> - fun(Acc, OldAcc) -> {maps:from_list(Acc), OldAcc} end; -object_finish_decoder(proplist) -> - fun(Acc, OldAcc) -> {lists:reverse(Acc), OldAcc} end; -object_finish_decoder(reversed_proplist) -> - fun(Acc, OldAcc) -> {Acc, OldAcc} end; -object_finish_decoder(Decoder) when is_function(Decoder, 2) -> - Decoder. - -traverse_codecs([], Term) -> - Term; -traverse_codecs(Codecs, Bin) when is_binary(Bin) -> - do_traverse_codecs(Codecs, Bin); -traverse_codecs(_Codecs, Term) -> - Term. - -do_traverse_codecs([Codec | Codecs], Bin) -> - case Codec(Bin) of - next -> - do_traverse_codecs(Codecs, Bin); - {halt, Value} -> - Value - end; -do_traverse_codecs([], Bin) -> - Bin. diff --git a/src/euneus_encoder.erl b/src/euneus_encoder.erl index c50e0dd..938c4e6 100644 --- a/src/euneus_encoder.erl +++ b/src/euneus_encoder.erl @@ -17,6 +17,16 @@ ]} ]). +%% -------------------------------------------------------------------- +%% Type exports +%% -------------------------------------------------------------------- + +-export_type([options/0]). +-export_type([codec_callback/0]). +-export_type([is_proplist/0]). +-export_type([encode/1]). +-export_type([state/0]). + %% -------------------------------------------------------------------- %% Macros %% -------------------------------------------------------------------- @@ -34,62 +44,63 @@ -elvis([{elvis_style, no_macros, #{allow => ['IS_MIN', 'IN_RANGE']}}]). %% -------------------------------------------------------------------- -%% Types (and their exports) +%% Types %% -------------------------------------------------------------------- --record(state, { - nulls :: #{term() := null}, - skip_values :: false | {true, #{term() := skip}}, - escape :: fun((binary()) -> iodata()), - integer :: encode(integer()), - float :: encode(float()), - atom :: encode(atom()), - list :: encode(list()), - proplist :: false | {true, is_proplist()}, - map :: encode(map()), - sort_keys :: boolean(), - tuple :: encode(tuple()), - pid :: encode(pid()), - port :: encode(port()), - reference :: encode(reference()) -}). --opaque state() :: #state{}. --export_type([state/0]). - --type encode(Type) :: fun((Type, json:encoder(), #state{}) -> iodata()). --export_type([encode/1]). - --type is_proplist() :: fun((list()) -> boolean()). --export_type([is_proplist/0]). - -type options() :: #{ + codecs => [codec()], nulls => [term()], skip_values => [term()], - escape => fun((binary()) -> iodata()), - integer => encode(integer()), - float => encode(float()), - atom => encode(atom()), - list => encode(list()), - proplist => boolean() | {true, is_proplist()}, - map => encode(map()), + key_to_binary => fun((term()) -> binary()), sort_keys => boolean(), - tuple => - encode(tuple()) - | [ - datetime - | timestamp - | ipv4 - | ipv6 - | {record, - #{Name :: atom() => {Fields :: [atom()], Size :: pos_integer()}} - | [{Name :: atom(), Fields :: [atom()]}]} - | fun((tuple()) -> next | {halt, term()}) - ], - pid => encode(pid()), - port => encode(port()), - reference => encode(reference()) + 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()) }. --export_type([options/0]). + +-type codec() :: + timestamp + | datetime + | ipv4 + | ipv6 + % {records, #{foo => {record_info(fields, foo), record_info(size, foo)}}} + | {records, #{Name :: atom() := {Fields :: [atom()], Size :: pos_integer()}}} + | codec_callback(). +-export_type([codec/0]). + +-type codec_callback() :: fun((tuple()) -> next | {halt, term()}). + +-type is_proplist() :: fun((list()) -> boolean()). + +-type encode(Type) :: fun((Type, json:encoder(), state()) -> iodata()). + +-record(state, { + codecs :: [codec()], + 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()) +}). +-opaque state() :: #state{}. %% -------------------------------------------------------------------- %% API functions @@ -99,7 +110,7 @@ encode(Input, Opts) -> State = new_state(Opts), json:encode(Input, fun(Term, Encode) -> - value(Term, Encode, State) + encode_term(Term, Encode, State) end). %% -------------------------------------------------------------------- @@ -110,120 +121,37 @@ encode(Input, Opts) -> new_state(Opts) -> #state{ - nulls = nulls(maps:get(nulls, Opts, [null])), - skip_values = skip_values(maps:get(skip_values, Opts, [undefined])), - escape = escape(maps:get(escape, Opts, default)), - integer = integer(maps:get(integer, Opts, default)), - float = float(maps:get(float, Opts, default)), - atom = atom(maps:get(atom, Opts, default)), - list = list(maps:get(list, Opts, default)), - proplist = proplist(maps:get(proplist, Opts, false)), - map = map(maps:get(map, Opts, default)), - sort_keys = sort_keys(maps:get(sort_keys, Opts, false)), - tuple = tuple(maps:get(tuple, Opts, default)), - pid = pid(maps:get(pid, Opts, default)), - port = port(maps:get(port, Opts, default)), - reference = reference(maps:get(reference, Opts, default)) + codecs = maps:get(codecs, Opts, []), + 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 json:encode_binary/1), + encode_integer = maps:get(encode_integer, Opts, fun encode_integer/3), + encode_float = maps:get(encode_float, Opts, fun encode_float/3), + encode_atom = maps:get(encode_atom, Opts, fun encode_atom/3), + encode_list = maps:get(encode_list, Opts, fun encode_list/3), + encode_map = maps:get(encode_map, Opts, fun encode_map/3), + encode_tuple = maps:get(encode_tuple, Opts, fun encode_tuple/3), + encode_pid = maps:get(encode_pid, Opts, fun encode_pid/3), + encode_port = maps:get(encode_port, Opts, fun encode_port/3), + encode_reference = maps:get(encode_reference, Opts, fun encode_reference/3) }. -nulls(Nulls) when is_list(Nulls) -> - maps:from_keys(Nulls, null). - -skip_values([]) -> - false; -skip_values(Values) when is_list(Values) -> - {true, maps:from_keys(Values, skip)}. - -escape(default) -> - fun json:encode_binary/1; -escape(Escape) when is_function(Escape, 1) -> - Escape. - -integer(default) -> - fun encode_integer/3; -integer(Encode) when is_function(Encode, 3) -> - Encode. - -float(default) -> - fun encode_float/3; -float(Encode) when is_function(Encode, 3) -> - Encode. - -atom(default) -> - fun encode_atom/3; -atom(Encode) when is_function(Encode, 3) -> - Encode. - -list(default) -> - fun encode_list/3; -list(Encode) when is_function(Encode, 3) -> - Encode. - -proplist(false) -> - false; -proplist(true) -> - {true, fun is_proplist/1}; -proplist({true, IsProplist}) when is_function(IsProplist, 1) -> - {true, IsProplist}. - -is_proplist([]) -> - false; -is_proplist(List) -> - lists:all(fun is_proplist_prop/1, List). - -% Must be the same types handled by key/2. -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). - -map(default) -> - fun encode_map/3; -map(Encode) when is_function(Encode, 3) -> - Encode. - -sort_keys(Sort) when is_boolean(Sort) -> - Sort. - -tuple(default) -> - fun encode_tuple/3; -tuple(Codecs) when is_list(Codecs) -> - codecs([norm_codec(Codec) || Codec <- Codecs]); -tuple(Encode) when is_function(Encode, 3) -> - Encode. - -pid(default) -> - fun encode_pid/3; -pid(Encode) when is_function(Encode, 3) -> - Encode. - -port(default) -> - fun encode_port/3; -port(Encode) when is_function(Encode, 3) -> - Encode. - -reference(default) -> - fun encode_reference/3; -reference(Encode) when is_function(Encode, 3) -> - Encode. +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). % Codecs -codecs(Codecs) -> - fun(Tuple, Encode, State) -> - case traverse_codecs(Codecs, Tuple) of - Tuple -> - error(unsuported_tuple, [Tuple, Encode, State]); - Term -> - value(Term, Encode, State) - end - end. - traverse_codecs([Codec | Codecs], Tuple) -> - case Codec(Tuple) of + case codec_callback(Codec, Tuple) of next -> traverse_codecs(Codecs, Tuple); {halt, NewTerm} -> @@ -232,116 +160,113 @@ traverse_codecs([Codec | Codecs], Tuple) -> traverse_codecs([], Tuple) -> Tuple. -norm_codec(datetime) -> - fun datetime_codec/1; -norm_codec(timestamp) -> - fun timestamp_codec/1; -norm_codec(ipv4) -> - fun ipv4_codec/1; -norm_codec(ipv6) -> - fun ipv6_codec/1; -norm_codec({record, Records}) when is_map(Records) -> - records_codec(Records); -norm_codec({record, Records}) when is_list(Records) -> - records_codec(norm_records_list(Records)); -norm_codec(Codec) when is_function(Codec, 1) -> - Codec. - -norm_records_list(List) -> - maps:from_list([{Name, {Fields, length(Fields) + 1}} || {Name, Fields} <- List]). - -datetime_codec({{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) +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(Callback, Tuple) -> + Callback(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.0BZ", - [YYYY, MM, DD, H, M, S] + "~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}; -datetime_codec(_Tuple) -> +timestamp_codec_callback(_Tuple) -> next. -timestamp_codec({MegaSecs, Secs, MicroSecs} = Timestamp) when - ?IS_MIN(MegaSecs, 0), ?IS_MIN(Secs, 0), ?IS_MIN(MicroSecs, 0) +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) -> - 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] + "~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}; -timestamp_codec(_Tuple) -> +datetime_codec_callback(_Tuple) -> next. -ipv4_codec({_A, _B, _C, _D} = Tuple) -> +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(_Tuple) -> +ipv4_codec_callback(_Tuple) -> next. -ipv6_codec({_A, _B, _C, _D, _E, _F, _G, _H} = Tuple) -> +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(_Tuple) -> +ipv6_codec_callback(_Tuple) -> next. -records_codec(Records) -> - fun - (Tuple) 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; - (_Tuple) -> +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. + end; +records_codec_callback(_Tuple, _Records) -> + next. % Encoders -value(Bin, _Encode, State) when is_binary(Bin) -> +encode_term(Bin, _Encode, State) when is_binary(Bin) -> (State#state.escape)(Bin); -value(Int, Encode, State) when is_integer(Int) -> - (State#state.integer)(Int, Encode, State); -value(Float, Encode, State) when is_float(Float) -> - (State#state.float)(Float, Encode, State); -value(Atom, Encode, State) when is_atom(Atom) -> - (State#state.atom)(Atom, Encode, State); -value(List, Encode, State) when is_list(List) -> - (State#state.list)(List, Encode, State); -value(Map, Encode, State) when is_map(Map) -> - (State#state.map)(Map, Encode, State); -value(Tuple, Encode, State) when is_tuple(Tuple) -> - (State#state.tuple)(Tuple, Encode, State); -value(Pid, Encode, State) when is_pid(Pid) -> - (State#state.pid)(Pid, Encode, State); -value(Port, Encode, State) when is_port(Port) -> - (State#state.port)(Port, Encode, State); -value(Ref, Encode, State) when is_reference(Ref) -> - (State#state.reference)(Ref, Encode, State); -value(Term, Encode, State) -> +encode_term(Int, Encode, State) when is_integer(Int) -> + (State#state.encode_integer)(Int, Encode, State); +encode_term(Float, Encode, State) when is_float(Float) -> + (State#state.encode_float)(Float, Encode, State); +encode_term(Atom, Encode, State) when is_atom(Atom) -> + (State#state.encode_atom)(Atom, Encode, State); +encode_term(List, Encode, State) when is_list(List) -> + (State#state.encode_list)(List, Encode, State); +encode_term(Map, Encode, State) when is_map(Map) -> + (State#state.encode_map)(Map, Encode, State); +encode_term(Tuple, Encode, State) when is_tuple(Tuple) -> + case traverse_codecs(State#state.codecs, Tuple) of + NewTuple when is_tuple(NewTuple) -> + (State#state.encode_tuple)(NewTuple, Encode, State); + NewTerm -> + encode_term(NewTerm, Encode, State) + end; +encode_term(Pid, Encode, State) when is_pid(Pid) -> + (State#state.encode_pid)(Pid, Encode, State); +encode_term(Port, Encode, State) when is_port(Port) -> + (State#state.encode_port)(Port, Encode, State); +encode_term(Ref, Encode, State) when is_reference(Ref) -> + (State#state.encode_reference)(Ref, Encode, State); +encode_term(Term, Encode, State) -> error(unsuported_term, [Term, Encode, State]). encode_integer(Int, _Encode, _State) -> @@ -359,47 +284,55 @@ encode_atom(Atom, _Encode, #state{nulls = Nulls}) when is_map_key(Atom, Nulls) - encode_atom(Atom, _Encode, #state{escape = Escape}) -> Escape(atom_to_binary(Atom, utf8)). -encode_list(List, Encode, #state{proplist = false}) -> +encode_list(List, Encode, #state{proplists = false}) -> json:encode_list(List, Encode); -encode_list(List, Encode, #state{proplist = {true, IsProplist}} = State) -> +encode_list(List, Encode, #state{proplists = true} = State) -> + case is_proplist(List) of + true -> + encode_proplist(List, Encode, State); + false -> + json:encode_list(List, Encode) + end; +encode_list(List, Encode, #state{proplists = {true, IsProplist}} = State) -> case IsProplist(List) of true -> - value(proplists:to_map(List), Encode, State); + encode_proplist(List, Encode, State); false -> json:encode_list(List, Encode) end. -encode_map(Map, Encode, #state{sort_keys = false, skip_values = false} = State) -> - do_encode_map([ - [$,, key(Key, State#state.escape), $: | value(Value, Encode, State)] - || Key := Value <- Map - ]); -encode_map(Map, Encode, #state{sort_keys = false, skip_values = {true, Skip}} = State) -> +encode_proplist(Proplist, Encode, State) -> + encode_term(proplists:to_map(Proplist), Encode, 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_map(Map, Encode, #state{sort_keys = false, skip_values = ValuesToSkip} = State) -> do_encode_map([ - [$,, key(Key, State#state.escape), $: | value(Value, Encode, State)] + [$,, escape_map_key(Key, State), $: | encode_term(Value, Encode, State)] || Key := Value <- Map, - not is_map_key(Value, Skip) - ]); -encode_map(Map, Encode, #state{sort_keys = true, skip_values = false} = State) -> - do_encode_map([ - [$,, key(Key, State#state.escape), $: | value(Value, Encode, State)] - || {Key, Value} <- lists:keysort(1, maps:to_list(Map)) + not is_map_key(Value, ValuesToSkip) ]); -encode_map(Map, Encode, #state{sort_keys = true, skip_values = {true, Skip}} = State) -> +encode_map(Map, Encode, #state{sort_keys = true, skip_values = ValuesToSkip} = State) -> do_encode_map([ - [$,, key(Key, State#state.escape), $: | value(Value, Encode, State)] + [$,, escape_map_key(Key, State), $: | encode_term(Value, Encode, State)] || {Key, Value} <- lists:keysort(1, maps:to_list(Map)), - not is_map_key(Value, Skip) + not is_map_key(Value, ValuesToSkip) ]). -key(Bin, Escape) when is_binary(Bin) -> - Escape(Bin); -key(Str, Escape) when is_list(Str) -> - Escape(iolist_to_binary(Str)); -key(Atom, Escape) when is_atom(Atom) -> - Escape(atom_to_binary(Atom, utf8)); -key(Int, Escape) when is_integer(Int) -> - Escape(integer_to_binary(Int, 10)). +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, "}"]. diff --git a/test/euneus_encoder_SUITE.erl b/test/euneus_encoder_SUITE.erl index 8ae531c..b7b9e6b 100644 --- a/test/euneus_encoder_SUITE.erl +++ b/test/euneus_encoder_SUITE.erl @@ -37,24 +37,24 @@ test_encode(Config) when is_list(Config) -> {<<"null">>, null, #{}}, {<<"\"foo\"">>, foo, #{}}, {<<"\"bar\"">>, foo, #{ - atom => fun(_Term, Encode, _State) -> Encode(<<"bar">>, Encode) end + encode_atom => fun(_Term, Encode, _State) -> Encode(<<"bar">>, Encode) end }}, % Proplist - {<<"{\"foo\":\"foo\",\"bar\":true}">>, [{foo, foo}, bar], #{proplist => true}}, + {<<"{\"foo\":\"foo\",\"bar\":true}">>, [{foo, foo}, bar], #{proplists => true}}, % Sort keys {<<"{\"a\":\"a\",\"b\":\"b\",\"c\":\"c\",\"d\":\"d\",\"e\":\"e\"}">>, #{c => c, d => d, a => a, e => e, b => b}, #{sort_keys => true}}, % Datetime {<<"\"1970-01-01T00:00:00Z\"">>, {{1970, 01, 01}, {00, 00, 00}}, #{ - tuple => [datetime] + codecs => [datetime] }}, % Timestamp - {<<"\"1970-01-01T00:00:00.000Z\"">>, {0, 0, 0}, #{tuple => [timestamp]}}, + {<<"\"1970-01-01T00:00:00.000Z\"">>, {0, 0, 0}, #{codecs => [timestamp]}}, % IPv4 - {<<"\"0.0.0.0\"">>, {0, 0, 0, 0}, #{tuple => [ipv4]}}, + {<<"\"0.0.0.0\"">>, {0, 0, 0, 0}, #{codecs => [ipv4]}}, % IPv6 {<<"\"fe80::204:acff:fe17:bf38\"">>, - {16#fe80, 0, 0, 0, 16#204, 16#acff, 16#fe17, 16#bf38}, #{tuple => [ipv6]}}, + {16#fe80, 0, 0, 0, 16#204, 16#acff, 16#fe17, 16#bf38}, #{codecs => [ipv6]}}, % Record {<<"[{\"foo\":\"foo\",\"bar\":\"bar\"},{\"bar\":\"bar\",\"baz\":\"baz\"}]">>, [ @@ -62,29 +62,29 @@ test_encode(Config) when is_list(Config) -> #bar{bar = bar, baz = baz} ], #{ - tuple => [ - {record, [ - {foo, record_info(fields, foo)}, - {bar, record_info(fields, bar)} - ]} + codecs => [ + {records, #{ + foo => {record_info(fields, foo), record_info(size, foo)}, + bar => {record_info(fields, bar), record_info(size, bar)} + }} ] }}, % Pid {<<"\"<0.92.0>\"">>, list_to_pid("<0.92.0>"), #{ - pid => fun(Pid, Encode, _State) -> + encode_pid => fun(Pid, Encode, _State) -> Encode(iolist_to_binary(pid_to_list(Pid)), Encode) end }}, % Port {<<"\"#Port<0.1>\"">>, list_to_port("#Port<0.1>"), #{ - port => fun(Port, Encode, _State) -> + encode_port => fun(Port, Encode, _State) -> Encode(iolist_to_binary(port_to_list(Port)), Encode) end }}, % Reference {<<"\"#Ref<0.314572725.1088159747.110918>\"">>, list_to_ref("#Ref<0.314572725.1088159747.110918>"), #{ - reference => fun(Ref, Encode, _State) -> + encode_reference => fun(Ref, Encode, _State) -> Encode(iolist_to_binary(ref_to_list(Ref)), Encode) end }}