diff --git a/README.md b/README.md index 9fdb2a5..cf91b80 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,8 @@ # JSON encoding with records and 'null'/'undefined' mapping -This is a wrapper around `jsx` to handle encoding and decoding of Erlang records. +Originally, this was a wrapper around `jsx` to handle encoding and decoding of Erlang records, but [euneus](https://github.com/williamthome/euneus) gives to +jsxrecord a better performance. ## JSON null handling @@ -56,25 +57,25 @@ Decoding returns the `#test{}`: Defaults are automatically added for fields missing in the JSON: #test{ a = 1, b = 2, c = undefined } = jsxrecord:decode(<<"{\"_record\":\"test\"}">>). - + ### Encoding and decoding datetime and timestamp tuples Datetime tuples are assumed to be in UTC, and are converted into an ISO8601 string: <<"\"2008-12-10T13:30:00Z\"">> = jsxrecord:encode({{2008, 12, 10}, {13, 30, 0}}) - + They are converted back into a datetime tuple: - + {{2008, 12, 10}, {13, 30, 0}} = jsxrecord:decode(<<"\"2008-12-10T13:30:00Z\"">>) - + Erlang timestamp tuples are also converted into an ISO8601 string, but with added precision: <<"\"2020-06-12T14:00:11.571Z\"">> = jsxrecord:encode({1591,970411,571321}) - + A little bit of precision is lost when converting it back to a timestamp tuple: {1591,970411,571000} = jsxrecord:decode(<<"\"2020-06-12T14:00:11.571Z\"">>) - + ## Configuration diff --git a/rebar.config b/rebar.config index 72625b5..20babec 100644 --- a/rebar.config +++ b/rebar.config @@ -1,7 +1,6 @@ {require_min_otp_vsn, "21"}. {deps, [ - {jsx, "3.1.0"}, {euneus, "0.6.0"} ]}. diff --git a/rebar.test.config b/rebar.test.config index dcdd209..50fcc16 100644 --- a/rebar.test.config +++ b/rebar.test.config @@ -10,6 +10,5 @@ {deps, [ {lager, "3.8.0"}, {proper, "1.3.0"}, - {jsx, "3.1.0"}, {euneus, "0.6.0"} ]}. diff --git a/src/jsxrecord.app.src b/src/jsxrecord.app.src index d52916b..804dd36 100644 --- a/src/jsxrecord.app.src +++ b/src/jsxrecord.app.src @@ -2,7 +2,7 @@ {description, "JSX wrapper to handle records and 'undefined'"}, {vsn, "git"}, {registered, []}, - {applications, [ kernel, stdlib, syntax_tools, compiler, jsx ]}, + {applications, [ kernel, stdlib, syntax_tools, compiler ]}, {env, [ {record_modules, [ ]} ]}, diff --git a/src/jsxrecord.erl b/src/jsxrecord.erl index 97845fa..68e58ec 100644 --- a/src/jsxrecord.erl +++ b/src/jsxrecord.erl @@ -1,6 +1,6 @@ %% @author Marc Worrell %% @copyright 2018-2023 Marc Worrell -%% @doc JSON with records and 'undefined'/'null' mapping. Wrapper around jsx. +%% @doc JSON with records and 'undefined'/'null' mapping. %% @end %% Copyright 2018-2023 Marc Worrell @@ -31,9 +31,6 @@ -define(RECORD_TYPE, <<"_type">>). --define(IS_NUMBER(C), C >= $0, C =< $9). - - -include_lib("kernel/include/logger.hrl"). %%==================================================================== @@ -91,93 +88,91 @@ do_load_records(Modules, CurrRecordDefs) -> Records), compile_module(New). - -encode_json(undefined) -> <<"null">>; -encode_json(null) -> <<"null">>; -encode_json(true) -> <<"true">>; -encode_json(false) -> <<"false">>; -encode_json({struct, _} = MochiJSON) -> - encode_json( mochijson_to_map(MochiJSON) ); encode_json(Term) -> - Options = [ - {error_handler, fun jsx_error/3} - ], - jsx:encode(expand_records(Term), Options). + Options = #{ + nulls => [undefined, null], + list_encoder => fun + ([{K, _} | _] = Proplist, Opts) + when is_binary(K); is_atom(K); is_integer(K) -> + Map = proplists:to_map(Proplist), + euneus_encoder:encode_map(Map, Opts); + (List, Opts) -> + euneus_encoder:encode_list(List, Opts) + end, + unhandled_encoder => fun + ({struct, MochiJSON}, Opts) -> + Map = mochijson_to_map(MochiJSON), + euneus_encoder:encode_map(Map, Opts); + (R, _Opts) when is_tuple(R), is_atom(element(1, R)) -> + T = atom_to_binary(element(1, R), utf8), + case maps:find(T, record_defs()) of + {ok, Def} -> + encode_json(expand_record_1( + Def, 2, R, #{ ?RECORD_TYPE => T } + )); + error -> + euneus_encoder:throw_unsupported_type_error(R) + end; + (T, _Opts) -> + euneus_encoder:throw_unsupported_type_error(T) + end, + error_handler => fun jsx_error/3 + }, + case euneus:encode_to_binary(Term, Options) of + {ok, JSON} -> + JSON; + {error, Reason} -> + error(Reason) + end. decode_json(<<>>) -> undefined; -decode_json(<<"null">>) -> undefined; -decode_json(<<"true">>) -> true; -decode_json(<<"false">>) -> false; -decode_json(B) -> reconstitute_records( jsx:decode(B, [return_maps]) ). +decode_json(B) -> + Options = #{ + objects => fun(M1, _Opts) -> + case maps:find(?RECORD_TYPE, M1) of + {ok, Type} -> + case maps:find(Type, record_defs_int()) of + {ok, Def} -> + Rec = lists:foldl( + fun({F, Default}, Acc) -> + V1 = case maps:get(F, M1, Default) of + V when is_map(V), is_list(Default) -> + make_proplist(V); + V -> + V + end, + [ V1 | Acc ] + end, + [ binary_to_atom(Type, utf8) ], + Def), + list_to_tuple( lists:reverse(Rec) ); + error -> + M1 + end; + error -> + M1 + end + end + }, + case euneus:decode(B, Options) of + {ok, Term} -> + Term; + {error, Reason} -> + error(Reason) + end. -jsx_error([T|Terms], {parser, State, Handler, Stack}, Config) -> +jsx_error(throw, {{token, Token}, Rest, Opts, Input, Pos, Buffer}, _Stacktrace) -> ?LOG_ERROR(#{ in => jsxrecord, text => <<"Error mapping value to JSON">>, result => error, reason => json_token, - token => T + token => Token }), - Config1 = jsx_config:parse_config(Config), - jsx_parser:resume([null|Terms], State, Handler, Stack, Config1); -jsx_error(_Terms, _Error, _Config) -> - erlang:error(badarg). - - -reconstitute_records( M ) when is_map(M) -> - M1 = maps:map( fun(_K, V) -> reconstitute_records(V) end, M ), - case maps:find(?RECORD_TYPE, M1) of - {ok, Type} -> - case maps:find(Type, record_defs_int()) of - {ok, Def} -> - Rec = lists:foldl( - fun({F, Default}, Acc) -> - V1 = case maps:get(F, M1, Default) of - V when is_map(V), is_list(Default) -> - make_proplist(V); - V -> - V - end, - [ V1 | Acc ] - end, - [ binary_to_atom(Type, utf8) ], - Def), - list_to_tuple( lists:reverse(Rec) ); - error -> - M1 - end; - error -> - M1 - end; -reconstitute_records( L ) when is_list(L) -> - [ reconstitute_records(X) || X <- L ]; -reconstitute_records( null ) -> - undefined; -reconstitute_records( <> ) - when ?IS_NUMBER(Y4), ?IS_NUMBER(Y3), ?IS_NUMBER(Y2), ?IS_NUMBER(Y1), - ?IS_NUMBER(M2), ?IS_NUMBER(M1), - ?IS_NUMBER(D2), ?IS_NUMBER(D1), - ?IS_NUMBER(H2), ?IS_NUMBER(H1), - ?IS_NUMBER(Min2), ?IS_NUMBER(Min1), - ?IS_NUMBER(S2), ?IS_NUMBER(S1), - ?IS_NUMBER(Mil3), ?IS_NUMBER(Mil2), ?IS_NUMBER(Mil1) -> - DateTime = {{chars_to_integer(Y4, Y3, Y2, Y1), chars_to_integer(M2, M1), chars_to_integer(D2, D1)}, - {chars_to_integer(H2, H1), chars_to_integer(Min2, Min1), chars_to_integer(S2, S1)}}, - MilliSeconds = chars_to_integer(Mil3, Mil2, Mil1), - Seconds = calendar:datetime_to_gregorian_seconds(DateTime) - 62167219200, - %% 62167219200 == calendar:datetime_to_gregorian_seconds({{1970, 1, 1}, {0, 0, 0}}) - {Seconds div 1000000, Seconds rem 1000000, MilliSeconds * 1000}; -reconstitute_records( <> ) - when ?IS_NUMBER(Y4), ?IS_NUMBER(Y3), ?IS_NUMBER(Y2), ?IS_NUMBER(Y1), - ?IS_NUMBER(M2), ?IS_NUMBER(M1), - ?IS_NUMBER(D2), ?IS_NUMBER(D1), - ?IS_NUMBER(H2), ?IS_NUMBER(H1), - ?IS_NUMBER(Min2), ?IS_NUMBER(Min1), - ?IS_NUMBER(S2), ?IS_NUMBER(S1) -> - {{chars_to_integer(Y4, Y3, Y2, Y1), chars_to_integer(M2, M1), chars_to_integer(D2, D1)}, - {chars_to_integer(H2, H1), chars_to_integer(Min2, Min1), chars_to_integer(S2, S1)}}; -reconstitute_records( T ) -> - T. + Replacement = null, + euneus_decoder:resume(Token, Replacement, Rest, Opts, Input, Pos, Buffer); +jsx_error(Class, Reason, Stacktrace) -> + euneus_decoder:handle_error(Class, Reason, Stacktrace). make_proplist(Map) -> L = maps:to_list(Map), @@ -194,56 +189,12 @@ make_proplist(Map) -> end, L). -expand_records(R) when is_tuple(R), is_atom(element(1, R)) -> - T = atom_to_binary(element(1, R), utf8), - case maps:find(T, record_defs()) of - {ok, Def} -> - expand_record_1(Def, 2, R, #{ ?RECORD_TYPE => T }); - error -> - R - end; -expand_records({MegaSecs, Secs, MicroSecs}=Timestamp) when is_integer(MegaSecs) andalso is_integer(Secs) andalso is_integer(MicroSecs) -> - % Timestamp, map to date in UTC - MilliSecs = MicroSecs div 1000, - {{Year, Month, Day}, {Hour, Min, Sec}} = calendar:now_to_datetime(Timestamp), - unicode:characters_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", - [Year, Month, Day, Hour, Min, Sec, MilliSecs])); - -expand_records({{Year,Month,Day},{Hour,Minute,Second}}) when is_integer(Year) andalso is_integer(Month) andalso is_integer(Second) andalso - is_integer(Hour) andalso is_integer(Minute) andalso is_integer(Second) -> - % Date tuple, assume it to be in UTC - unicode:characters_to_binary(io_lib:format( - "~4.10.0B-~2.10.0B-~2.10.0BT~2.10.0B:~2.10.0B:~2.10.0BZ", - [Year, Month, Day, Hour, Minute, Second])); - -expand_records({A, B, Params} = Mime) when is_binary(A), is_binary(B), is_list(Params) -> - % Assume to be a MIME content type - format_content_type(Mime); -expand_records({K, V}) when is_number(K) -> - [ K, V ]; -expand_records({K, V}) -> - {expand_records(K), expand_records(V)}; -expand_records(L) when is_list(L) -> - lists:map( - fun - ({K, V}) when is_binary(K); is_atom(K); is_number(K) -> {K, expand_records(V)}; - (V) -> expand_records(V) - end, - L); -expand_records(M) when is_map(M) -> - maps:map( fun(_K, V) -> expand_records(V) end, M ); -expand_records(undefined) -> - null; -expand_records(X) -> - X. - expand_record_1([ {F, _} | Fs ], N, R, Acc) -> - Acc1 = Acc#{ F => expand_records( element(N, R) ) }, + Acc1 = Acc#{ F => element(N, R) }, expand_record_1(Fs, N+1, R, Acc1); expand_record_1([], _N, _R, Acc) -> Acc. - mochijson_to_map({struct, L}) -> maps:from_list([ mochijson_to_map(V) || V <- L ]); mochijson_to_map({K, V}) -> @@ -251,12 +202,6 @@ mochijson_to_map({K, V}) -> mochijson_to_map(V) -> V. -format_content_type({T1, T2, []}) -> - <>; -format_content_type({T1, T2, Params}) -> - ParamsBin = [ [$;, Param, $=, Value] || {Param,Value} <- Params ], - iolist_to_binary([T1, $/, T2, ParamsBin]). - %% @doc Compile the record defs to a module, for effictient caching of all definitions -spec compile_module( map() ) -> ok. compile_module( Defs ) -> @@ -316,13 +261,3 @@ to_field_name({record_field, _Line, {atom, _, FieldName}}) -> {FieldName, undefined}; to_field_name({record_field, _Line, {atom, _, FieldName}, InitExpr}) -> {FieldName, erl_syntax:concrete(InitExpr)}. - -chars_to_integer(N2, N1) -> - ((N2 - $0) * 10) + (N1 - $0). - -chars_to_integer(N3, N2, N1) -> - ((N3 - $0) * 100) + ((N2 - $0) * 10) + (N1 - $0). - -chars_to_integer(N4, N3, N2, N1) -> - ((N4 - $0) * 1000) + ((N3 - $0) * 100) + ((N2 - $0) * 10) + (N1 - $0). - diff --git a/src/jsxrecordx.erl b/src/jsxrecordx.erl deleted file mode 100644 index 7f5c033..0000000 --- a/src/jsxrecordx.erl +++ /dev/null @@ -1,263 +0,0 @@ -%% @author Marc Worrell -%% @copyright 2018-2023 Marc Worrell -%% @doc JSON with records and 'undefined'/'null' mapping. Wrapper around jsx. -%% @end - -%% Copyright 2018-2023 Marc Worrell -%% -%% Licensed under the Apache License, Version 2.0 (the "License"); -%% you may not use this file except in compliance with the License. -%% You may obtain a copy of the License at -%% -%% http://www.apache.org/licenses/LICENSE-2.0 -%% -%% Unless required by applicable law or agreed to in writing, software -%% distributed under the License is distributed on an "AS IS" BASIS, -%% WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -%% See the License for the specific language governing permissions and -%% limitations under the License. - --module(jsxrecordx). - --author('Marc Worrell '). - --export([ - encode/1, - decode/1, - - load_records/1, - record_defs/0 -]). - --define(RECORD_TYPE, <<"_type">>). - --include_lib("kernel/include/logger.hrl"). - -%%==================================================================== -%% API -%%==================================================================== - --spec encode( term() ) -> binary(). -encode(Source) -> - encode_json(Source). - --spec decode( binary() | undefined ) -> term(). -decode(undefined) -> - undefined; -decode(Bin) when is_binary(Bin) -> - decode_json(Bin). - -%% @doc Load all record definitions. --spec record_defs() -> map(). -record_defs() -> - try jsxrecord_defs:defs() - catch _:_ -> - _ = application:start(jsxrecord), - {ok, Ms} = application:get_env(jsxrecord, record_modules), - ok = do_load_records(Ms, #{}), - jsxrecord_defs:defs() - end. - --spec load_records( module() | list( module( )) ) -> ok. -load_records(Module) when is_atom(Module) -> - load_records([ Module ]); -load_records(Modules) -> - do_load_records(Modules, record_defs_int()). - -%%==================================================================== -%% Internal -%%==================================================================== - -%% @doc Load all record definitions. --spec record_defs_int() -> map(). -record_defs_int() -> - try - erlang:apply(jsxrecord_defs, defs, []) - catch _:_ -> - #{} - end. - -do_load_records(Modules, CurrRecordDefs) -> - Records = lists:flatten( lists:map( fun(M) -> extract_records(M) end, Modules ) ), - New = lists:foldl( - fun({Name, Fs}, Acc) -> - FsB = [ {atom_to_binary(F, utf8), Init} || {F,Init} <- Fs ], - Acc#{ atom_to_binary(Name, utf8) => FsB } - end, - CurrRecordDefs, - Records), - compile_module(New). - -encode_json(Term) -> - Options = #{ - nulls => [undefined, null], - list_encoder => fun - ([{K, _} | _] = Proplist, Opts) - when is_binary(K); is_atom(K); is_integer(K) -> - Map = proplists:to_map(Proplist), - euneus_encoder:encode_map(Map, Opts); - (List, Opts) -> - euneus_encoder:encode_list(List, Opts) - end, - unhandled_encoder => fun - ({struct, MochiJSON}, Opts) -> - Map = mochijson_to_map(MochiJSON), - euneus_encoder:encode_map(Map, Opts); - (R, _Opts) when is_tuple(R), is_atom(element(1, R)) -> - T = atom_to_binary(element(1, R), utf8), - case maps:find(T, record_defs()) of - {ok, Def} -> - encode_json(expand_record_1( - Def, 2, R, #{ ?RECORD_TYPE => T } - )); - error -> - euneus_encoder:throw_unsupported_type_error(R) - end; - (T, _Opts) -> - euneus_encoder:throw_unsupported_type_error(T) - end, - error_handler => fun jsx_error/3 - }, - case euneus:encode_to_binary(Term, Options) of - {ok, JSON} -> - JSON; - {error, Reason} -> - error(Reason) - end. - -decode_json(<<>>) -> undefined; -decode_json(B) -> - Options = #{ - objects => fun(M1, _Opts) -> - case maps:find(?RECORD_TYPE, M1) of - {ok, Type} -> - case maps:find(Type, record_defs_int()) of - {ok, Def} -> - Rec = lists:foldl( - fun({F, Default}, Acc) -> - V1 = case maps:get(F, M1, Default) of - V when is_map(V), is_list(Default) -> - make_proplist(V); - V -> - V - end, - [ V1 | Acc ] - end, - [ binary_to_atom(Type, utf8) ], - Def), - list_to_tuple( lists:reverse(Rec) ); - error -> - M1 - end; - error -> - M1 - end - end - }, - case euneus:decode(B, Options) of - {ok, Term} -> - Term; - {error, Reason} -> - error(Reason) - end. - -jsx_error(throw, {{token, Token}, Rest, Opts, Input, Pos, Buffer}, _Stacktrace) -> - ?LOG_ERROR(#{ - in => jsxrecord, - text => <<"Error mapping value to JSON">>, - result => error, - reason => json_token, - token => Token - }), - Replacement = null, - euneus_decoder:resume(Token, Replacement, Rest, Opts, Input, Pos, Buffer); -jsx_error(Class, Reason, Stacktrace) -> - euneus_decoder:handle_error(Class, Reason, Stacktrace). - -make_proplist(Map) -> - L = maps:to_list(Map), - lists:map( - fun - ({K,V}) when is_binary(K) -> - try - {binary_to_existing_atom(K, utf8), V} - catch - _:_ -> {K, V} - end; - (KV) -> - KV - end, - L). - -expand_record_1([ {F, _} | Fs ], N, R, Acc) -> - Acc1 = Acc#{ F => element(N, R) }, - expand_record_1(Fs, N+1, R, Acc1); -expand_record_1([], _N, _R, Acc) -> - Acc. - -mochijson_to_map({struct, L}) -> - maps:from_list([ mochijson_to_map(V) || V <- L ]); -mochijson_to_map({K, V}) -> - {K, mochijson_to_map(V)}; -mochijson_to_map(V) -> - V. - -%% @doc Compile the record defs to a module, for effictient caching of all definitions --spec compile_module( map() ) -> ok. -compile_module( Defs ) -> - {ok, Module, Bin} = compile(Defs), - code:purge(Module), - {module, _} = code:load_binary(Module, "jsxrecord_defs.erl", Bin), - ok. - --spec compile( map() ) -> {ok, atom(), binary()}. -compile(Defs) -> - ModuleAst = erl_syntax:attribute(erl_syntax:atom(module), [ erl_syntax:atom(jsxrecord_defs) ]), - ExportAst = erl_syntax:attribute( - erl_syntax:atom(export), - [ erl_syntax:list([ - erl_syntax:arity_qualifier(erl_syntax:atom(defs), erl_syntax:integer(0)) - ]) - ]), - FunAst = erl_syntax:function( - erl_syntax:atom(defs), - [ erl_syntax:clause([], none, [ erl_syntax:abstract(Defs) ]) ]), - Forms = [ erl_syntax:revert(X) || X <- [ ModuleAst, ExportAst, FunAst ] ], - {ok, Module, Bin} = compile:forms(Forms, []), - {ok, Module, Bin}. - --spec extract_records( module() ) -> list( {atom(), list(atom())} ). -extract_records(Module) -> - case code:which(Module) of - BeamFile when is_list(BeamFile) -> - case beam_lib:chunks(BeamFile, [ abstract_code ]) of - {ok, {_, [ {abstract_code, {_, AbstractCode }} ]} } -> - extract_records_abs(AbstractCode); - _ -> - [] - end; - - _Other -> - [] - end. - -%% @doc Extract all record definitions from the abstract code -extract_records_abs( AbstractCode ) -> - lists:filtermap( - fun - ({attribute, _Pos, record, {Name, Fields}}) -> - {true, {Name, to_field_names(Fields)}}; - (_) -> - false - end, - AbstractCode). - -to_field_names(Fields) -> - [ to_field_name(Field) || Field <- Fields ]. - -to_field_name({typed_record_field, RecField, _Type}) -> - to_field_name(RecField); -to_field_name({record_field, _Line, {atom, _, FieldName}}) -> - {FieldName, undefined}; -to_field_name({record_field, _Line, {atom, _, FieldName}, InitExpr}) -> - {FieldName, erl_syntax:concrete(InitExpr)}. diff --git a/test/jsxrecord_SUITE.erl b/test/jsxrecord_SUITE.erl index 57970e5..1495093 100644 --- a/test/jsxrecord_SUITE.erl +++ b/test/jsxrecord_SUITE.erl @@ -47,52 +47,52 @@ all() -> %%-------------------------------------------------------------------- undefined_value(_Config) -> - <<"{\"a\":null}">> = jsxrecord:encode( #{ a => undefined } ), - #{ <<"a">> := undefined } = jsxrecord:decode( <<"{\"a\":null}">> ), + <<"{\"a\":null}">> = encode( #{ a => undefined } ), + #{ <<"a">> := undefined } = decode( <<"{\"a\":null}">> ), ok. records(_Config) -> ok = jsxrecord:load_records(?MODULE), - #test{} = jsxrecord:decode( jsxrecord:encode( #test{} ) ), - #test{ a = <<"a">> } = jsxrecord:decode( jsxrecord:encode( #test{ a = a } ) ), - #test{ a = undefined } = jsxrecord:decode( jsxrecord:encode( #test{ a = undefined } ) ), + #test{} = decode( encode( #test{} ) ), + #test{ a = <<"a">> } = decode( encode( #test{ a = a } ) ), + #test{ a = undefined } = decode( encode( #test{ a = undefined } ) ), ok. records_nested(_Config) -> - #test{ a = #test{} } = jsxrecord:decode( jsxrecord:encode( #test{ a = #test{} } ) ), - <<"{\"a\":{\"_type\":\"test\",\"a\":1,\"b\":2,\"c\":null}}">> = jsxrecord:encode(#{ a => #test{} }), - #{ <<"a">> := #test{} } = jsxrecord:decode( jsxrecord:encode(#{ a => #test{} }) ), + #test{ a = #test{} } = decode( encode( #test{ a = #test{} } ) ), + <<"{\"a\":{\"_type\":\"test\",\"a\":1,\"b\":2,\"c\":null}}">> = encode(#{ a => #test{} }), + #{ <<"a">> := #test{} } = decode( encode(#{ a => #test{} }) ), ok. record_defaults(_Config) -> - #test{ a = 1, b = 2, c = undefined } = jsxrecord:decode( <<"{\"_type\":\"test\"}">> ), + #test{ a = 1, b = 2, c = undefined } = decode( <<"{\"_type\":\"test\"}">> ), ok. dates(_Config) -> - <<"\"2008-12-10T13:30:00Z\"">> = jsxrecord:encode({{2008, 12, 10}, {13, 30, 0}}), - {{2008, 12, 10}, {13, 30, 0}} = jsxrecord:decode(<<"\"2008-12-10T13:30:00Z\"">>), + <<"\"2008-12-10T13:30:00Z\"">> = encode({{2008, 12, 10}, {13, 30, 0}}), + {{2008, 12, 10}, {13, 30, 0}} = decode(<<"\"2008-12-10T13:30:00Z\"">>), <<"[\"2008-12-10T13:30:00Z\",\"2008-12-10T13:30:00Z\"]">> = - jsxrecord:encode([{{2008, 12, 10}, {13, 30, 0}}, {{2008, 12, 10}, {13, 30, 0}}]), + encode([{{2008, 12, 10}, {13, 30, 0}}, {{2008, 12, 10}, {13, 30, 0}}]), [{{2008, 12, 10}, {13, 30, 0}}, {{2008, 12, 10}, {13, 30, 0}}] = - jsxrecord:decode(<<"[\"2008-12-10T13:30:00Z\",\"2008-12-10T13:30:00Z\"]">>), + decode(<<"[\"2008-12-10T13:30:00Z\",\"2008-12-10T13:30:00Z\"]">>), ok. times(_Config) -> - <<"\"2020-06-12T14:00:11.571Z\"">> = jsxrecord:encode({1591,970411,571321}), + <<"\"2020-06-12T14:00:11.571Z\"">> = encode({1591,970411,571321}), % We loose a little bit of precision, but that is ok. - {1591,970411,571000} = jsxrecord:decode( <<"\"2020-06-12T14:00:11.571Z\"">> ), + {1591,970411,571000} = decode( <<"\"2020-06-12T14:00:11.571Z\"">> ), ok. proplist(_Config) -> - <<"{\"a\":1}">> = jsxrecord:encode([ {a, 1} ]), + <<"{\"a\":1}">> = encode([ {a, 1} ]), ok. record_proplist(_Config) -> Tr = #trans{ tr = [ {en, <<"hello">>} ]}, - Json = jsxrecord:encode(Tr), - Tr = jsxrecord:decode(Json), + Json = encode(Tr), + Tr = decode(Json), ok. mixed_list(_Config) -> @@ -107,9 +107,29 @@ mixed_list(_Config) -> {95,828167}, {99,828167}, {999,828167}], - JSON = <<"{\"n\":7,\"mean\":166347,\"min\":750,\"max\":828167,\"median\":880,", - "\"50\":880,\"75\":1143,\"90\":828167,\"95\":828167,\"99\":828167,", - "\"999\":828167}">>, - JSON = jsxrecord:encode(L), + E = [ + "\"n\":7", + "\"mean\":166347", + "\"min\":750", + "\"max\":828167", + "\"median\":880", + "\"50\":880", + "\"75\":1143", + "\"90\":828167", + "\"95\":828167", + "\"99\":828167", + "\"999\":828167" + ], + JSON = encode(L), + [ match = re:run(JSON, RE, [{capture, none}]) || RE <- E ], ok. +%%-------------------------------------------------------------------- +%% SUPPORT FUNCTIONS +%%-------------------------------------------------------------------- + +encode(Source) -> + jsxrecord:encode(Source). + +decode(Bin) -> + jsxrecord:decode(Bin). diff --git a/test/jsxrecordx_SUITE.erl b/test/jsxrecordx_SUITE.erl deleted file mode 100644 index f641758..0000000 --- a/test/jsxrecordx_SUITE.erl +++ /dev/null @@ -1,135 +0,0 @@ --module(jsxrecordx_SUITE). - --compile(export_all). - --include_lib("common_test/include/ct.hrl"). - --record(test, { - a = 1, - b = 2, - c -}). - --record(trans, { - tr = [] -}). - -%%-------------------------------------------------------------------- -%% COMMON TEST CALLBACK FUNCTIONS -%%-------------------------------------------------------------------- -init_per_suite(Config) -> - Config. - -end_per_suite(_Config) -> - ok. - -init_per_testcase(_TestCase, Config) -> - Config. - -end_per_testcase(_TestCase, _Config) -> - ok. - -all() -> - [ - undefined_value, - records, - records_nested, - record_defaults, - dates, - times, - proplist, - record_proplist, - mixed_list - ]. - -%%-------------------------------------------------------------------- -%% TEST CASES -%%-------------------------------------------------------------------- - -undefined_value(_Config) -> - <<"{\"a\":null}">> = encode( #{ a => undefined } ), - #{ <<"a">> := undefined } = decode( <<"{\"a\":null}">> ), - ok. - -records(_Config) -> - ok = jsxrecord:load_records(?MODULE), - #test{} = decode( encode( #test{} ) ), - #test{ a = <<"a">> } = decode( encode( #test{ a = a } ) ), - #test{ a = undefined } = decode( encode( #test{ a = undefined } ) ), - ok. - -records_nested(_Config) -> - #test{ a = #test{} } = decode( encode( #test{ a = #test{} } ) ), - <<"{\"a\":{\"_type\":\"test\",\"a\":1,\"b\":2,\"c\":null}}">> = encode(#{ a => #test{} }), - #{ <<"a">> := #test{} } = decode( encode(#{ a => #test{} }) ), - ok. - -record_defaults(_Config) -> - #test{ a = 1, b = 2, c = undefined } = decode( <<"{\"_type\":\"test\"}">> ), - ok. - -dates(_Config) -> - <<"\"2008-12-10T13:30:00Z\"">> = encode({{2008, 12, 10}, {13, 30, 0}}), - {{2008, 12, 10}, {13, 30, 0}} = decode(<<"\"2008-12-10T13:30:00Z\"">>), - - <<"[\"2008-12-10T13:30:00Z\",\"2008-12-10T13:30:00Z\"]">> = - encode([{{2008, 12, 10}, {13, 30, 0}}, {{2008, 12, 10}, {13, 30, 0}}]), - [{{2008, 12, 10}, {13, 30, 0}}, {{2008, 12, 10}, {13, 30, 0}}] = - decode(<<"[\"2008-12-10T13:30:00Z\",\"2008-12-10T13:30:00Z\"]">>), - - ok. - -times(_Config) -> - <<"\"2020-06-12T14:00:11.571Z\"">> = encode({1591,970411,571321}), - % We loose a little bit of precision, but that is ok. - {1591,970411,571000} = decode( <<"\"2020-06-12T14:00:11.571Z\"">> ), - ok. - -proplist(_Config) -> - <<"{\"a\":1}">> = encode([ {a, 1} ]), - ok. - -record_proplist(_Config) -> - Tr = #trans{ tr = [ {en, <<"hello">>} ]}, - Json = encode(Tr), - Tr = decode(Json), - ok. - -mixed_list(_Config) -> - L = [{n,7}, - {mean,166347}, - {min,750}, - {max,828167}, - {median,880}, - {50,880}, - {75,1143}, - {90,828167}, - {95,828167}, - {99,828167}, - {999,828167}], - E = [ - "\"n\":7", - "\"mean\":166347", - "\"min\":750", - "\"max\":828167", - "\"median\":880", - "\"50\":880", - "\"75\":1143", - "\"90\":828167", - "\"95\":828167", - "\"99\":828167", - "\"999\":828167" - ], - JSON = encode(L), - [ match = re:run(JSON, RE, [{capture, none}]) || RE <- E ], - ok. - -%%-------------------------------------------------------------------- -%% SUPPORT FUNCTIONS -%%-------------------------------------------------------------------- - -encode(Source) -> - jsxrecordx:encode(Source). - -decode(Bin) -> - jsxrecordx:decode(Bin).