From 662b567675102a9b65c6979ed3ad1f0c0fc3a638 Mon Sep 17 00:00:00 2001 From: Nicholas Lundgaard Date: Tue, 22 Aug 2017 12:40:14 -0500 Subject: [PATCH] Add support for map format * Introduce support for new `map` format * Deprecate Erlang 17 and lower * Deprecate `jsn:sort/1`, `jsn:sort_keys/1`, and `jsn:sort_equal/2` --- .travis.yml | 5 +- README.md | 82 ++++++++----- include/jsn.hrl | 53 +++++--- rebar.config | 3 +- src/jsn.app.src | 2 +- src/jsn.erl | 117 +++++++++++++----- test/jsn_tests.erl | 295 +++++++++++++++++++++++++++++++++++++++++++-- 7 files changed, 463 insertions(+), 94 deletions(-) diff --git a/.travis.yml b/.travis.yml index db7bd81..00c7c24 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,4 +11,7 @@ script: - ./rebar3 compile - ./rebar3 eunit - ./rebar3 xref - - ./rebar3 edoc +# maps support breaks edoc generation +# TODO reinstate edoc generation with +# jsn 2.0.0 +# - ./rebar3 edoc diff --git a/README.md b/README.md index 0fa7bd6..3e2b4ae 100644 --- a/README.md +++ b/README.md @@ -11,52 +11,64 @@ Unlike [ej][ej], however, it supports _all three_ common JSON representations in Erlang: * `proplist` (**default**)(common to [jsx][jsx] and [jsonx][jsonx]) +* `map` (common to [jsone][jsone], [jiffy][jiffy], and [jsx][jsx]) * `eep18` (common to [jiffy][jiffy], [jsone][jsone], and [jsonx][jsonx]) -* `struct` (common to [mochijson2][mochijson2]) +* `struct` (common to [mochijson2][mochijson2]) -In addition to supporting the additional `proplist` format, jsn's path -input structure is somewhat more flexible, allowing for input of +In addition to supporting the additional `proplist` and `map` formats, jsn's +path input structure is somewhat more flexible, allowing for input of period-delimited binary strings or atoms to indicate a path through a deeply nested structure. This support is similar to [kvc][kvc]'s path format, and also likely to be familiar to users of [erlson][erlson]. This code base was originally developed as a wrapper around [ej][ej], adding support for the 'syntactic sugar' of the period-delimited keys. However, a -need arose for the library to be proplist-compatible, so it has been refactored -to be a nearly standalone library. +need arose for the library to be proplist-compatible, then map-compatible, so +it has been refactored to be a nearly standalone library. ## Caveats & known issues -### Proplist format concerns +### Deprecated: Erlang 17 or lower -It should be noted that the `proplist` format supported by jsn is [jsonx][jsonx] -compatible, and has a minor incompatibility with the comparable formats in -[jsx][jsx] and [jsone][jsone]. jsn uses the empty list (`[]`) like [jsonx][jsonx] -to represent an empty object, whereas [jsx][jsx] and [jsone][jsone] use an empty -tuple in a list (`[{}]`) to represent empty objects. jsn is incompatible with -this format. While the getter (`jsn:get/2,3`) functions are generally functional; -most other library functions are not, and may result in unpredictable behaviors. +jsn will no longer support Erlang 17 or previous Erlang releases. Allowing the +`map` format to work without breaking Erlang versions that do not support maps +(or have an incomplete implementation of maps, i.e., Erlang 17) requires +inelegant conditional macros throughout the code and test. This support will +be removed in the next major version of jsn. ### Deprecated: encoding and decoding -jsn will no longer be supporting encoding and decoding. See [below](#encode-decode) -for more information. +jsn will no longer support encoding and decoding. It will be removed in the next +major version of jsn. See [below](#encode-decode) for more information. -## Roadmap +### Deprecated: key sorting functions + +jsn will no longer support the `jsn:sort/1`, `jsn:sort_keys/1`, and +`jsn:sort_equal/2` functions. These functions are incompatible with the `map` +format; the ambiguity of library functions which are only partially compatible +with the supported formats is confusing for clients. For this reason, the +functions will be removed in the next major version of jsn. + +### Proplist format concerns -### 1.1.0 +It should be noted that the `proplist` format supported by jsn is compatible with +the [abandoned](#encode-decode) [jsonx][jsonx] library, and is not compatible +with the `proplist` format used in [jsx][jsx] and [jsone][jsone]. Specifically, +jsn uses the empty list (`[]`) like [jsonx][jsonx] to represent an empty object, +whereas [jsx][jsx] and [jsone][jsone] use an empty tuple in a list (`[{}]`) to +represent empty objects. jsn is incompatible with this format. While the getter +(`jsn:get/2,3`) functions are generally functional; most other library functions +are not, and may result in unpredictable behaviors. -* **Add maps support** (Erlang 18+ only; Erlang 17 will not be supported). jsn - will still be compatible with existing Erlang versions, but the `map` format - will not be available. -* **Deprecate Erlang 17 and lower**. This version of jsn will use conditionals - and macros to enable maps. These awkward constructions will be removed in a - future library update, which will necessitate the removal of support for - older Erlang versions. -* **Deprecate `jsn:sort/1`, `jsn:sort_keys/1`, and `jsn:sort_equal/2`**. These - functions presume a key ordering, which is incompatible with the `map` format. - While they will remain in the library, their use is discouraged due to the - object format restriction. +### Edoc generation broken by map support + +the `edoc` make target (and using `rebar3 edoc`) are currently broken due to a +parser problem triggered by the `IF_MAPS(...)` macro used to implement the +`map` format in a backwards-compatible fashion. Edoc support will be restored +when this problem is addressed in Erlang or the `map` backwards-compatibility +constructions are removed from jsn in a future version. + +## Roadmap ### 1.1.1 @@ -67,7 +79,7 @@ for more information. ### 2.0.0 * **Remove deprecated functions `jsn:sort/1`, `jsn:sort_keys/1`, and `jsn:sort_equal/2`**. -* **Remove deprecated support for Erlang 17 and lower**. Full `maps` support will be +* **Remove deprecated support for Erlang 17 and lower**. Full `map` support will be assumed by the code, and these older versions will no longer be able to compile jsn. * **Make `map` the default object format**. Maps are superior to proplists for @@ -184,6 +196,12 @@ jsn:new([{'user.id', <<"123">>}, {<<"user.name">>, <<"John">>}], [{format, struct}]). % {struct,[{<<"user">>, % {struct,[{<<"id">>,<<"123">>},{<<"name">>,<<"John">>}]}}]} + +% create a jsn object in map format +jsn:new([{'user.id', <<"123">>}, + {<<"user.name">>, <<"John">>}], [{format, map}]). +% #{<<"user">> => +% #{<<"id">> => <<"123">>,<<"name">> => <<"John">>}} ``` ### `get/2,3`, `get_list/2,3`, and `find/3,4` - Extract data from objects @@ -418,9 +436,9 @@ future version of jsn (`2.x.x`). the [jsonx][jsonx] library that jsn uses for this functionality is abandoned, and users are strongly advised to use any of the many Erlang JSON libraries available: -* [jiffy][jiffy] (`eep18` format) -* [jsone][jsone] (`eep18`, `proplist` formats) -* [jsx][jsx] (`proplist` format) +* [jiffy][jiffy] (`eep18` and `map` formats) +* [jsone][jsone] (`eep18`, `proplist`, and `map` formats) +* [jsx][jsx] (`proplist` and `map` formats) * [mochijson2][mochijson2] (`struct` format) ### `equal/3,4` - Path-wise object comparison diff --git a/include/jsn.hrl b/include/jsn.hrl index ae77185..5b17996 100644 --- a/include/jsn.hrl +++ b/include/jsn.hrl @@ -10,21 +10,28 @@ %% jsn types %%============================================================================= --type json_string() :: binary(). --type json_key() :: json_string() | atom(). --type json_number() :: integer() | float(). --type json_boolean() :: true | false. --type json_null() :: null. --type json_array() :: [json_term()]. --type json_array_index() :: first | last | pos_integer(). --type json_proplist() :: [{json_key(), json_term()}]. --type json_eep18() :: {json_proplist()}. --type json_struct() :: {struct, json_proplist()}. --type json_object() :: json_proplist() | json_eep18() | json_struct(). - --type json_term() :: json_string() | json_number() | json_array() | - json_object() | json_null() | json_boolean(). - +-type json_string() :: binary(). +-type json_key() :: json_string() | atom(). +-type json_number() :: integer() | float(). +-type json_boolean() :: true | false. +-type json_null() :: null. +-type json_array() :: [json_term()]. +-type json_no_map_array() :: [json_no_map_term()]. +-type json_array_index() :: first | last | pos_integer(). +-type json_proplist() :: [{json_key(), json_term()}]. +-type json_eep18() :: {json_proplist()}. +-type json_struct() :: {struct, json_proplist()}. +-type json_no_map_object() :: json_proplist() | json_eep18() | json_struct(). +-ifdef(maps_support). +-type json_map() :: #{json_key() => json_term()}. +-type json_object() :: json_no_map_object() | json_map(). +-else. +-type json_object() :: json_no_map_object(). +-endif. +-type json_no_map_term() :: json_string() | json_number() | json_no_map_array() | + json_null() | json_boolean() | json_no_map_object(). +-type json_term() :: json_string() | json_number() | json_array() | + json_null() | json_boolean() | json_object(). %% JSN OPTIONS %% @@ -32,14 +39,19 @@ %% from scratch. Currently, the only option is format, which can be either: %% %% * proplist (default) +%% * map %% * eep18 (a.k.a EJSON) %% * struct (mochijson2 format) %% +-ifdef(maps_support). +-type format() :: map | proplist | eep18 | struct. +-else. -type format() :: proplist | eep18 | struct. +-endif. -type jsn_option() :: {format, format()}. -type jsn_options() :: [ jsn_option() ]. -%% A path is a either a list of json keys (representing nesting from left +%% A path is either a list of json keys (representing nesting from left %% to right), a tuple of json keys and/or json array indexes (also nested %% left to right), or a single period-delimited binary/atom, where periods %% indicate nesting of keys; period-delimited binary/atom values are mapped @@ -68,4 +80,11 @@ -define(EMPTY_STRUCT, {struct, []}). --endif. \ No newline at end of file +-ifdef(maps_support). +-define(IF_MAPS(Expr), Expr). +-define(EMPTY_MAP, #{}). +-else. +-define(IF_MAPS(_), ). +-endif. + +-endif. diff --git a/rebar.config b/rebar.config index da66cc7..085aaa5 100644 --- a/rebar.config +++ b/rebar.config @@ -1,6 +1,7 @@ {deps, [{jsonx, {git, "https://github.com/alertlogic/jsonx.git", {branch, master}}}]}. -{erl_opts, [{platform_define, "^1[89]|^[2-9][0-9]+", has_rand}, +{erl_opts, [{platform_define, "^1[89]|^[2-9][0-9]+", maps_support}, + {platform_define, "^1[89]|^[2-9][0-9]+", has_rand}, inline_list_funcs, warn_deprecated_function, warn_export_vars, diff --git a/src/jsn.app.src b/src/jsn.app.src index 976bddd..d974b41 100644 --- a/src/jsn.app.src +++ b/src/jsn.app.src @@ -1,6 +1,6 @@ {application, jsn, [ {description, "Utilities for interacting with decoded JSON in erlang"}, - {vsn, "1.0.3"}, %% <- need to set this appropriately when publishing to hex.pm + {vsn, "1.1.0"}, %% <- need to set this appropriately when publishing to hex.pm {applications, [kernel, stdlib, jsonx]}, diff --git a/src/jsn.erl b/src/jsn.erl index 2a8af94..72be8cb 100644 --- a/src/jsn.erl +++ b/src/jsn.erl @@ -23,18 +23,19 @@ path_transform/2, path_elements/1 ]). -%% sort functions --export([sort/1, sort_keys/1]). %% comparison functions -export([equal/3, equal/4]). --export([sort_equal/2]). -export([is_equal/2]). -export([is_subset/2]). -%% DEPRECATED: JSON encode/decode --export([encode/1, decode/1, decode/2]). %% object format conversion -export([as_proplist/1, from_proplist/1, from_proplist/2]). +%% DEPRECATED: JSON encode/decode +-export([encode/1, decode/1, decode/2]). +%% DEPRECATED: sort functions +-export([sort/1, sort_keys/1]). +-export([sort_equal/2]). + -ifdef(TEST). -compile([export_all]). -endif. @@ -49,12 +50,10 @@ path/0, paths/0, jsn_option/0, jsn_options/0]). - %%============================================================================== %% constants %%============================================================================== - -define(DEFAULT_FORMAT, proplist). %% guard for matching a JSON string, boolean, number, null @@ -64,12 +63,10 @@ is_number(X); X =:= null ). - %%============================================================================== %% lookup/storage API %%============================================================================== - -spec new() -> json_object(). %%------------------------------------------------------------------------------ %% @doc return an empty json_object in the default format @@ -364,11 +361,13 @@ path_elements(_Type, _Path, _Acc) -> erlang:error(badarg). %%============================================================================== --spec sort(json_term()) -> json_term(). +-spec sort(json_no_map_term()) -> json_no_map_term(). %%------------------------------------------------------------------------------ %% @doc given a json term, recursively sort the keys and arrays in the object, %% if they are present; note that this will sort arrays in the object, not just -%% the keys +%% the keys. +%% +%% @deprecated This function is imcompatible with map objects; avoid using it. %%------------------------------------------------------------------------------ sort(L) when is_list(L) -> lists:sort([sort(V) || V <- L]); @@ -380,10 +379,12 @@ sort(V) -> V. --spec sort_keys(json_term()) -> json_term(). +-spec sort_keys(json_no_map_term()) -> json_no_map_term(). %%------------------------------------------------------------------------------ %% @doc given a json term or json proplist term, recursively sort keys in -%% all objects at all levels +%% all objects at all levels. +%% +%% @deprecated This function is imcompatible with map objects; avoid using it. %%------------------------------------------------------------------------------ sort_keys({struct, V}) -> @@ -456,15 +457,17 @@ equal(Paths, OriginalObject, OtherObjects, Mode) when is_list(OtherObjects) -> end. --spec sort_equal(json_term(), json_term()) -> boolean(). +-spec sort_equal(json_no_map_term(), json_no_map_term()) -> boolean(). %%------------------------------------------------------------------------------ %% @doc given two json terms, return true if they are equal after sorting -%% all contained lists (including, but not limited to, proplists) +%% all contained lists (including, but not limited to, proplists). %% %% unlike equal/3,4 this function can be used to check equality of large complex %% objects, but only when order of list items doesn't matter; note that this %% only works if both terms are in the same format: e.g., if one is a proplist %% and the other is eep18, this will always return false. +%% +%% @deprecated This function is imcompatible with map objects; avoid using it. %%------------------------------------------------------------------------------ sort_equal(A, B) -> sort(A) =:= sort(B). @@ -472,9 +475,15 @@ sort_equal(A, B) -> -spec is_equal(json_term(), json_term()) -> boolean(). %%------------------------------------------------------------------------------ -%% @doc given 2 json terms A and B in any format (eep18, struct, proplist), -%% return true if they are equivalent +%% @doc given 2 json terms A and B in any format (eep18, struct, proplist, or +%% map, if supported), return true if they are equivalent %%------------------------------------------------------------------------------ +?IF_MAPS( +is_equal(A, B) when is_map(A) -> + is_equal(maps:to_list(A), B); +is_equal(A, B) when is_map(B) -> + is_equal(A, maps:to_list(B)); +) is_equal({A}, B) when is_list(A) -> is_equal(A, B); is_equal({struct, A}, B) when is_list(A) -> @@ -502,6 +511,12 @@ is_equal(_A, _B) -> %% CAUTION: this comparison treats json array comparisons as subset comparisons, %% not just object comparisons. so, `is_subset([1,1,1], [1,2])' is `true' %%------------------------------------------------------------------------------ +?IF_MAPS( +is_subset(A, B) when is_map(A) -> + is_subset(maps:to_list(A), B); +is_subset(A, B) when is_map(B) -> + is_subset(A, maps:to_list(B)); +) is_subset({A}, B) when is_list(A) -> is_subset(A, B); is_subset({struct, A}, B) when is_list(A) -> @@ -578,6 +593,14 @@ decode(Json, _Options) -> %%------------------------------------------------------------------------------ %% @doc convert a jsn object (or list of them) into a proplist %%------------------------------------------------------------------------------ +?IF_MAPS( +as_proplist(M) when is_map(M) -> + maps:fold(fun(Key, Value, Acc) -> + [{to_binary(Key), as_proplist(Value)} | Acc] + end, + [], + M); +) as_proplist({struct, List}) when is_list(List) -> as_proplist(List); as_proplist({List}) when is_list(List) -> @@ -608,11 +631,21 @@ from_proplist(Object) -> %% from erlson decoding) into json_object using the given options %%------------------------------------------------------------------------------ from_proplist([{_,_}|_] = P0, Options) -> - P = [{to_binary(Key), from_proplist(Value, Options)} || {Key, Value} <- P0], case get_format(Options) of - proplist -> P; - struct -> {struct, P}; - eep18 -> {P} + ?IF_MAPS(map -> + lists:foldl(fun({K,V}, Acc) -> + Acc#{to_binary(K) => from_proplist(V, Options)} + end, + ?EMPTY_MAP, + P0) + ;) + OtherFormat -> + P = [{to_binary(Key), from_proplist(Value, Options)} || {Key, Value} <- P0], + case OtherFormat of + proplist -> P; + struct -> {struct, P}; + eep18 -> {P} + end end; from_proplist(List, Options) when is_list(List) -> [from_proplist(Value, Options) || Value <- List]; @@ -630,10 +663,11 @@ from_proplist(X, _Options) -> X. %%------------------------------------------------------------------------------ get_format(Options) -> case lists:keyfind(format, 1, Options) of - false -> ?DEFAULT_FORMAT; - {_, proplist} -> proplist; - {_, eep18} -> eep18; - {_, struct} -> struct; + ?IF_MAPS({_, map} -> map;) + {_, proplist} -> proplist; + {_, eep18} -> eep18; + {_, struct} -> struct; + false -> ?DEFAULT_FORMAT; _ -> erlang:error(badarg) end. @@ -644,9 +678,10 @@ get_format(Options) -> %%------------------------------------------------------------------------------ empty_object(Options) -> case get_format(Options) of + ?IF_MAPS(map -> ?EMPTY_MAP;) proplist -> ?EMPTY_PROPLIST; eep18 -> ?EMPTY_EEP18; - struct -> ?EMPTY_STRUCT + struct -> ?EMPTY_STRUCT end. @@ -659,6 +694,10 @@ empty_object(Options) -> %% value, upsate the object at the location defined by the path to the given %% to be value, and return the updated object %%------------------------------------------------------------------------------ +?IF_MAPS( +keys_set(Keys, Object, Value) when is_map(Object) -> + keys_set(Keys, Object, Value, empty_object([{format, map}])); +) keys_set(Keys, {P} = Object, Value) when is_list(P) -> keys_set(Keys, Object, Value, empty_object([{format, eep18}])); keys_set(Keys, {struct, P} = Object, Value) when is_list(P) -> @@ -683,12 +722,13 @@ keys_set(Keys, {struct, P}, Value, Empty) when is_list(P) -> {struct, keys_set(Keys, P, Value, Empty)}; keys_set(Keys, {P}, Value, Empty) when is_list(P) -> {keys_set(Keys, P, Value, Empty)}; -keys_set([Key | Rest], P, Value, Empty) when is_binary(Key), is_list(P) -> - case key_get(Key, P, jsn__undefined) of +keys_set([Key | Rest], Object, Value, Empty) + when is_binary(Key), (is_list(Object) ?IF_MAPS(orelse is_map(Object))) -> + case key_get(Key, Object, jsn__undefined) of E when E =:= jsn__undefined; E =:= Empty -> - key_set(Key, P, keys_set(Rest, Empty, Value, Empty)); + key_set(Key, Object, keys_set(Rest, Empty, Value, Empty)); SubValue -> - key_set(Key, P, keys_set(Rest, SubValue, Value, Empty)) + key_set(Key, Object, keys_set(Rest, SubValue, Value, Empty)) end; keys_set([Index | Rest], A, Value, Empty) when is_integer(Index); Index =:= first; @@ -704,6 +744,12 @@ keys_set([Index | Rest], A, Value, Empty) when is_integer(Index); %% object at the key; if the value is a deletion, remove the key, value pair, %% if it exists %%------------------------------------------------------------------------------ +?IF_MAPS( +key_set(Key, M, jsn__delete) when is_binary(Key) andalso is_map(M) -> + maps:remove(Key, maps:remove(safe_binary_to_atom(Key), M)); +key_set(Key, M, Value) when is_binary(Key) andalso is_map(M) -> + maps:put(Key, Value, M); +) key_set(Key, [], jsn__delete) when is_binary(Key) -> []; key_set(Key, [{_,_}|_] = P, jsn__delete) when is_binary(Key) -> @@ -779,6 +825,17 @@ key_get(Key, Object) -> %% or return undefined; this is function does not support nested keys (i.e., %% paths), only single, flat keys %%------------------------------------------------------------------------------ +?IF_MAPS( +key_get(Key, M, Default) when is_map(M) -> + case maps:find(Key, M) of + error -> + case maps:find(safe_binary_to_atom(Key), M) of + error -> Default; + {ok, Value0} -> Value0 + end; + {ok, Value} -> Value + end; +) key_get(Key, {P}, Default) when is_list(P) -> key_get(Key, P, Default); key_get(Key, {struct, P}, Default) when is_list(P) -> diff --git a/test/jsn_tests.erl b/test/jsn_tests.erl index e1211d7..7b60797 100644 --- a/test/jsn_tests.erl +++ b/test/jsn_tests.erl @@ -117,29 +117,62 @@ new_1_test_() -> {{<<"foo">>, <<"bar">>, last}, 3}, {{<<"foo">>, <<"bar">>, 2}, 2}]))]. +-ifdef(maps_support). new_2_test_() -> Object1 = [{<<"foo">>, <<"bar">>}], Object1Eep18 = {[{<<"foo">>, <<"bar">>}]}, Object1Struct = {struct, [{<<"foo">>, <<"bar">>}]}, + Object1Map = #{<<"foo">> => <<"bar">>}, Object2 = [{<<"foo">>, [{<<"bar">>, <<"hoge">>}]}], Object2Eep18 = {[{<<"foo">>, {[{<<"bar">>, <<"hoge">>}]}}]}, Object2Struct = {struct, [{<<"foo">>, {struct, [{<<"bar">>, <<"hoge">>}]}}]}, + Object2Map = #{<<"foo">> => #{<<"bar">> => <<"hoge">>}}, [?_assertEqual(?EMPTY_PROPLIST, jsn:new([])), ?_assertEqual(?EMPTY_EEP18, jsn:new([], [{format, eep18}])), ?_assertEqual(?EMPTY_STRUCT, jsn:new([], [{format, struct}])), + ?_assertEqual(?EMPTY_MAP, jsn:new([], [{format, map}])), ?_assertEqual(Object1, jsn:new({<<"foo">>, <<"bar">>}, [{format, proplist}])), ?_assertEqual(Object1, jsn:new({foo, <<"bar">>}, [])), ?_assertEqual(Object1Eep18, jsn:new({foo, <<"bar">>}, [{format, eep18}])), ?_assertEqual(Object1Eep18, jsn:new([{<<"foo">>, <<"bar">>}], [{format, eep18}])), ?_assertEqual(Object1Struct, jsn:new({foo, <<"bar">>}, [{format, struct}])), ?_assertEqual(Object1Struct, jsn:new([{<<"foo">>, <<"bar">>}], [{format, struct}])), + ?_assertEqual(Object1Map, jsn:new({foo, <<"bar">>}, [{format, map}])), + ?_assertEqual(Object1Map, jsn:new({<<"foo">>, <<"bar">>}, [{format, map}])), ?_assertEqual(Object2, jsn:new({'foo.bar', <<"hoge">>}, [])), ?_assertEqual(Object2, jsn:new({'foo.bar', <<"hoge">>}, [{format, proplist}])), ?_assertEqual(Object2Eep18, jsn:new({<<"foo.bar">>, <<"hoge">>}, [{format, eep18}])), ?_assertEqual(Object2Struct, jsn:new({'foo.bar', <<"hoge">>}, [{format, struct}])), + ?_assertEqual(Object2Map, jsn:new({<<"foo.bar">>, <<"hoge">>}, [{format, map}])), + ?_assertEqual(Object2Map, jsn:new({'foo.bar', <<"hoge">>}, [{format, map}])), ?_assertError(badarg, jsn:new([], [{format, random}]))]. +-else. + +new_2_test_() -> + Object1 = [{<<"foo">>, <<"bar">>}], + Object1Eep18 = {[{<<"foo">>, <<"bar">>}]}, + Object1Struct = {struct, [{<<"foo">>, <<"bar">>}]}, + Object2 = [{<<"foo">>, [{<<"bar">>, <<"hoge">>}]}], + Object2Eep18 = {[{<<"foo">>, {[{<<"bar">>, <<"hoge">>}]}}]}, + Object2Struct = {struct, [{<<"foo">>, {struct, [{<<"bar">>, <<"hoge">>}]}}]}, + [?_assertEqual(?EMPTY_PROPLIST, jsn:new([])), + ?_assertEqual(?EMPTY_EEP18, jsn:new([], [{format, eep18}])), + ?_assertEqual(?EMPTY_STRUCT, jsn:new([], [{format, struct}])), + ?_assertEqual(Object1, jsn:new({<<"foo">>, <<"bar">>}, [{format, proplist}])), + ?_assertEqual(Object1, jsn:new({foo, <<"bar">>}, [])), + ?_assertEqual(Object1Eep18, jsn:new({foo, <<"bar">>}, [{format, eep18}])), + ?_assertEqual(Object1Eep18, jsn:new([{<<"foo">>, <<"bar">>}], [{format, eep18}])), + ?_assertEqual(Object1Struct, jsn:new({foo, <<"bar">>}, [{format, struct}])), + ?_assertEqual(Object1Struct, jsn:new([{<<"foo">>, <<"bar">>}], [{format, struct}])), + ?_assertEqual(Object2, jsn:new({'foo.bar', <<"hoge">>}, [])), + ?_assertEqual(Object2, jsn:new({'foo.bar', <<"hoge">>}, [{format, proplist}])), + ?_assertEqual(Object2Eep18, jsn:new({<<"foo.bar">>, <<"hoge">>}, [{format, eep18}])), + ?_assertEqual(Object2Struct, jsn:new({'foo.bar', <<"hoge">>}, [{format, struct}])), + ?_assertError(badarg, jsn:new([], [{format, random}]))]. + +-endif. get_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, @@ -158,6 +191,26 @@ get_test_() -> ?_assertEqual(undefined, jsn:get({<<"baz">>, 99}, Src)), ?_assertEqual(undefined, jsn:get(<<"bar">>, Src))]. +-ifdef(maps_support). + +get_map_test_() -> + Src = #{<<"foo">> => <<"bar">>, + <<"qux">> => #{<<"lux">> => 99}, + <<"baz">> => [0, 10, 20]}, + [?_assertEqual(<<"bar">>, jsn:get(<<"foo">>, Src)), + ?_assertEqual(<<"bar">>, jsn:get(foo, Src)), + ?_assertEqual(99, jsn:get('qux.lux', Src)), + ?_assertEqual(99, jsn:get([<<"qux">>, lux], Src)), + ?_assertEqual(99, jsn:get({<<"qux">>, <<"lux">>}, Src)), + ?_assertEqual(0, jsn:get({<<"baz">>, 1}, Src)), + ?_assertEqual(0, jsn:get({<<"baz">>, first}, Src)), + ?_assertEqual(10, jsn:get({<<"baz">>, 2}, Src)), + ?_assertEqual(20, jsn:get({<<"baz">>, 3}, Src)), + ?_assertEqual(20, jsn:get({<<"baz">>, last}, Src)), + ?_assertEqual(undefined, jsn:get({<<"baz">>, 99}, Src)), + ?_assertEqual(undefined, jsn:get(<<"bar">>, Src))]. + +-endif. get_list_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, @@ -173,13 +226,33 @@ get_list_test_() -> ?_assertEqual(R3, jsn:get_list([<<"qux">>, 'a.b.c'], Src)), ?_assertEqual(R3, jsn:get_list([qux, [<<"a">>,b,<<"c">>]], Src))]. +-ifdef(maps_support). -find_test_() -> - Src0 = jsn:new([ - {<<"foo">>, <<"bar">>}, - {<<"qux">>, 99}, - {<<"baz">>, <<"hoge">>} - ]), +find_map_test_() -> + Src0 = jsn:new([{<<"foo">>, <<"bar">>}, + {<<"qux">>, 99}, + {<<"baz">>, <<"hoge">>}], + [{format, map}]), + Src1 = jsn:set(<<"moo">>, Src0, <<"cow">>), + Src2 = jsn:set(<<"foo">>, Src0, <<"kaboom">>), + Objects = [Src0, Src1, Src2], + Haystack = jsn:new({<<"weeble">>, Objects}, [{format, map}]), + Needle = <<"bar">>, + [?_assertEqual([Src0, Src1], jsn:find(foo, Needle, Objects)), + ?_assertEqual([], jsn:find(mop, Needle, Objects)), + ?_assertEqual([Src1], jsn:find(moo, <<"cow">>, Objects)), + ?_assertEqual([], jsn:find(moo, cow, Objects)), + ?_assertEqual([Src0, Src1], jsn:find(weeble, foo, Needle, Haystack)), + ?_assertEqual([], jsn:find(weeble, mop, Needle, Haystack)), + ?_assertEqual([Src1], jsn:find(weeble, moo, <<"cow">>, Haystack)), + ?_assertEqual([], jsn:find(weeble, moo, cow, Haystack))]. + +-endif. + +find_proplist_test_() -> + Src0 = jsn:new([{<<"foo">>, <<"bar">>}, + {<<"qux">>, 99}, + {<<"baz">>, <<"hoge">>}]), Src1 = jsn:set(<<"moo">>, Src0, <<"cow">>), Src2 = jsn:set(<<"foo">>, Src0, <<"kaboom">>), Objects = [Src0, Src1, Src2], @@ -194,6 +267,40 @@ find_test_() -> ?_assertEqual([Src1], jsn:find(weeble, moo, <<"cow">>, Haystack)), ?_assertEqual([], jsn:find(weeble, moo, cow, Haystack))]. +-ifdef(maps_support). + +set_test_() -> + Path = <<"foo.bar">>, + Object1 = jsn:new({Path, <<"baz">>}), + Object1Map = jsn:new({Path, <<"baz">>}, [{format, map}]), + Object2 = jsn:new({Path, [1, 2, 3]}), + Object2Map = jsn:new({Path, [1, 2, 3]}, [{format, map}]), + [?_assertEqual([{<<"foo">>, <<"bar">>}], jsn:set(<<"foo">>, jsn:new(), <<"bar">>)), + ?_assertEqual(#{<<"foo">> => <<"bar">>}, jsn:set(<<"foo">>, #{}, <<"bar">>)), + ?_assertEqual(Object1, jsn:set(Path, Object1, <<"baz">>)), + ?_assertEqual(Object1Map, jsn:set(Path, Object1Map, <<"baz">>)), + ?_assertEqual(Object2, jsn:set(Path, Object1, [1, 2, 3])), + ?_assertEqual(Object2Map, jsn:set(Path, Object1Map, [1, 2, 3])), + ?_assertEqual([99, 2, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, first}, Object2, 99))), + ?_assertEqual([99, 2, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, first}, Object2Map, 99))), + ?_assertEqual([99, 2, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 1}, Object2, 99))), + ?_assertEqual([99, 2, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 1}, Object2Map, 99))), + ?_assertEqual([1, 99, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 2}, Object2, 99))), + ?_assertEqual([1, 99, 3], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 2}, Object2Map, 99))), + ?_assertEqual([1, 2, 99], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 3}, Object2, 99))), + ?_assertEqual([1, 2, 99], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, 3}, Object2Map, 99))), + ?_assertEqual([1, 2, 99], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, last}, Object2, 99))), + ?_assertEqual([1, 2, 99], jsn:get(Path, jsn:set({<<"foo">>, <<"bar">>, last}, Object2Map, 99))), + ?_assertThrow({error, {not_an_object, _}}, jsn:set(<<"k">>, [1,2,3], <<"v">>)), + ?_assertThrow({error, {not_an_object, _}}, jsn:set(<<"k">>, <<"a">>, <<"v">>)), + ?_assertThrow({error, {not_an_object, _}}, jsn:set(<<"k">>, 0, <<"v">>)), + ?_assertThrow({error, {not_an_object, _}}, jsn:set(<<"k">>, {[0]}, <<"v">>)), + ?_assertThrow({error, {not_an_array, _}}, jsn:set({1}, [{<<"k">>, 1}], <<"v">>)), + ?_assertThrow({error, {not_an_array, _}}, jsn:set({1}, #{<<"k">>=> 1}, <<"v">>)), + ?_assertThrow({error, {not_an_array, _}}, jsn:set({<<"k">>, 1}, [{<<"k">>, 1}], <<"v">>)), + ?_assertThrow({error, {not_an_array, _}}, jsn:set({<<"k">>, 1}, #{<<"k">> => 1}, <<"v">>))]. + +-else. set_test_() -> Path = <<"foo.bar">>, @@ -214,6 +321,7 @@ set_test_() -> ?_assertThrow({error, {not_an_array, _}}, jsn:set({1}, [{<<"k">>, 1}], <<"v">>)), ?_assertThrow({error, {not_an_array, _}}, jsn:set({<<"k">>, 1}, [{<<"k">>, 1}], <<"v">>))]. +-endif. set_list_test_() -> EmptyObject = jsn:new(), @@ -228,6 +336,37 @@ set_list_test_() -> ?_assertEqual(EndObject2, jsn:set_list([{<<"hoge">>, <<"qux">>}], EndObject1)), ?_assertEqual(<<"qux">>, jsn:get(<<"hoge">>, jsn:set_list([{<<"hoge">>, <<"qux">>}], EndObject1)))]. +-ifdef(maps_support). + +delete_test_() -> + Path1 = <<"foo.bar">>, + Path2 = <<"qux.lux">>, + Object1 = jsn:new([{Path1, <<"baz">>}, {Path2, [1,2,3]}]), + Object1Map = jsn:new([{Path1, <<"baz">>}, {Path2, [1,2,3]}], [{format, map}]), + [?_assertEqual([], jsn:delete(<<"foo">>, [{<<"foo">>, <<"bar">>}])), + ?_assertEqual(#{}, jsn:delete(<<"foo">>, #{<<"foo">> => <<"bar">>})), + ?_assertEqual(jsn:new({Path2, [1,2,3]}), jsn:delete(foo, Object1)), + ?_assertEqual(jsn:new({Path2, [1,2,3]}, [{format, map}]), jsn:delete(foo, Object1Map)), + ?_assertEqual(jsn:new({Path1, <<"baz">>}), jsn:delete(qux, Object1)), + ?_assertEqual(jsn:new({Path1, <<"baz">>}, [{format, map}]), jsn:delete(qux, Object1Map)), + ?_assertEqual([2, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, first}, Object1))), + ?_assertEqual([2, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, first}, Object1Map))), + ?_assertEqual([2, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 1}, Object1))), + ?_assertEqual([2, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 1}, Object1Map))), + ?_assertEqual([1, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 2}, Object1))), + ?_assertEqual([1, 3], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 2}, Object1Map))), + ?_assertEqual([1, 2], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 3}, Object1))), + ?_assertEqual([1, 2], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, 3}, Object1Map))), + ?_assertEqual([1, 2], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, last}, Object1))), + ?_assertEqual([1, 2], jsn:get(Path2, jsn:delete({<<"qux">>, <<"lux">>, last}, Object1Map))), + ?_assertThrow({error, {not_an_object, _}}, jsn:delete(<<"k">>, [1,2,3])), + ?_assertThrow({error, {not_an_object, _}}, jsn:delete(<<"k">>, <<"a">>)), + ?_assertThrow({error, {not_an_object, _}}, jsn:delete(<<"k">>, 0)), + ?_assertThrow({error, {not_an_object, _}}, jsn:delete(<<"k">>, {[0]})), + ?_assertThrow({error, {not_an_array, _}}, jsn:delete({1}, #{<<"k">> => 1})), + ?_assertThrow({error, {not_an_array, _}}, jsn:delete({<<"k">>, 1}, #{<<"k">> => 1}))]. + +-else. delete_test_() -> Path1 = <<"foo.bar">>, @@ -248,6 +387,8 @@ delete_test_() -> ?_assertThrow({error, {not_an_array, _}}, jsn:delete({1}, [{<<"k">>, 1}])), ?_assertThrow({error, {not_an_array, _}}, jsn:delete({<<"k">>, 1}, [{<<"k">>, 1}]))]. +-endif. + delete_list_test_() -> Base = jsn:new([{<<"foo">>, <<"bar">>}, {<<"qux">>, 99}, @@ -265,6 +406,7 @@ delete_if_equal_test_() -> jsn:delete_if_equal(<<"qux">>, [null, 99], Base)), ?_assertEqual(Base, jsn:delete_if_equal(<<"baz">>, null, Base))]. + copy_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, {<<"qux">>, 99}, @@ -283,6 +425,7 @@ copy_test_() -> ?_assertEqual(R3, jsn:copy([<<"foo">>, <<"mop">>], Src, [jsn:new([], [{format, struct}])])), ?_assertError(badarg, jsn:copy([<<"foo">>, <<"mop">>], Src, jsn:new(), gg))]. + transform_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, {<<"qux">>, 99}]), @@ -302,6 +445,7 @@ transform_test_() -> [?_assertEqual(R1, jsn:transform(T1, Src)), ?_assertEqual(R2, jsn:transform(T2, Src))]. + path_transform_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, {<<"qux">>, 99}]), @@ -314,6 +458,7 @@ path_transform_test_() -> [?_assertEqual(R1, jsn:path_transform(T1, Src)), ?_assertEqual(R2, jsn:path_transform(T2, Src))]. + copy_mutate_test_() -> Src = jsn:new([{<<"foo">>, <<"bar">>}, {<<"qux">>, 99}, @@ -475,6 +620,112 @@ path_equal_test_() -> ?_assertEqual({error, jsn:to_binary(Field2)}, jsn:path_equal(Field2, Params, MisMatchedParams, soft)), ?_assertEqual({error, jsn:to_binary(Field2)}, jsn:path_equal(Field2, Params, MisMatchedParams, soft))]. +-ifdef(maps_support). + +is_equal_test_() -> + TestParams = [{'foo.bar.baz', null}, + {<<"foo.fum">>, <<"ok">>}, + {<<"foo.q.e.d">>, true}, + {<<"abc">>, [1,2,3]}], + Extra = [{'foo.extra', <<"extra">>}], + Proplist = jsn:new(TestParams, [{format, proplist}]), + Eep18 = jsn:new(TestParams, [{format, eep18}]), + Struct = jsn:new(TestParams, [{format, struct}]), + Map = jsn:new(TestParams, [{format, map}]), + ExtraProplist = jsn:new(Extra ++ TestParams, [{format, proplist}]), + ExtraEep18 = jsn:new(TestParams ++ Extra, [{format, eep18}]), + ExtraStruct = jsn:new(TestParams ++ Extra, [{format, struct}]), + ExtraMap = jsn:new(TestParams ++ Extra, [{format, map}]), + [?_assert(jsn:is_equal(<<"a">>, <<"a">>)), + ?_assert(jsn:is_equal([1,2], [1,2])), + ?_assert(jsn:is_equal(jsn:new(), jsn:new())), + ?_assert(jsn:is_equal(#{}, #{})), + ?_assert(jsn:is_equal(Proplist, Proplist)), + ?_assert(jsn:is_equal(Proplist, Struct)), + ?_assert(jsn:is_equal(Proplist, Eep18)), + ?_assert(jsn:is_equal(Proplist, Map)), + ?_assert(jsn:is_equal(Eep18, Proplist)), + ?_assert(jsn:is_equal(Eep18, Eep18)), + ?_assert(jsn:is_equal(Eep18, Struct)), + ?_assert(jsn:is_equal(Eep18, Map)), + ?_assert(jsn:is_equal(Struct, Proplist)), + ?_assert(jsn:is_equal(Struct, Eep18)), + ?_assert(jsn:is_equal(Struct, Struct)), + ?_assert(jsn:is_equal(Struct, Map)), + ?_assert(jsn:is_equal(Map, Proplist)), + ?_assert(jsn:is_equal(Map, Struct)), + ?_assert(jsn:is_equal(Map, Eep18)), + ?_assert(jsn:is_equal(Map, Map)), + ?_assert(jsn:is_equal(null, null)), + ?_assertNot(jsn:is_equal(true, false)), + ?_assertNot(jsn:is_equal(Proplist, ExtraProplist)), + ?_assertNot(jsn:is_equal(Proplist, ExtraEep18)), + ?_assertNot(jsn:is_equal(Proplist, ExtraStruct)), + ?_assertNot(jsn:is_equal(Proplist, ExtraMap)), + ?_assertNot(jsn:is_equal(Eep18, ExtraProplist)), + ?_assertNot(jsn:is_equal(Eep18, ExtraEep18)), + ?_assertNot(jsn:is_equal(Eep18, ExtraStruct)), + ?_assertNot(jsn:is_equal(Eep18, ExtraMap)), + ?_assertNot(jsn:is_equal(Struct, ExtraProplist)), + ?_assertNot(jsn:is_equal(Struct, ExtraEep18)), + ?_assertNot(jsn:is_equal(Struct, ExtraStruct)), + ?_assertNot(jsn:is_equal(Struct, ExtraMap)), + ?_assertNot(jsn:is_equal(Map, ExtraProplist)), + ?_assertNot(jsn:is_equal(Map, ExtraEep18)), + ?_assertNot(jsn:is_equal(Map, ExtraStruct)), + ?_assertNot(jsn:is_equal(Map, ExtraMap)), + ?_assertNot(jsn:is_equal(undefined, undefined)), + ?_assertNot(jsn:is_equal([1,undefined], [1,undefined]))]. + + +is_subset_test_() -> + [?_assert(jsn:is_subset(<<"a">>, <<"a">>)), + ?_assert(jsn:is_subset([1,2], [3,4,2,1])), + ?_assert(jsn:is_subset(jsn:new(), jsn:new({a, 99}))), + ?_assert(jsn:is_subset(#{}, #{a => 99})), + ?_assert(jsn:is_subset({[]}, #{a => 99})), + ?_assert(jsn:is_subset(null, null)), + ?_assertNot(jsn:is_subset(undefined, undefined)), + ?_assertNot(jsn:is_subset([1,undefined], [1,undefined])), + ?_assertNot(jsn:is_subset(true, false))]. + + +is_equal() -> + Map = generate_json_object(7, [{format, map}]), + Proplist = jsn:as_proplist(Map), + Eep18 = jsn:from_proplist(Proplist, [{format, eep18}]), + Struct = jsn:from_proplist(Proplist, [{format, struct}]), + NotEqualProplist = [{qqqqqqqqqqqq, 9} | Proplist], + ?assert(jsn:is_equal(Map, Map)), + ?assert(jsn:is_equal(Map, Proplist)), + ?assert(jsn:is_equal(Proplist, Proplist)), + ?assert(jsn:is_equal(Proplist, Map)), + ?assert(jsn:is_equal(Proplist, Eep18)), + ?assert(jsn:is_equal(Proplist, Struct)), + ?assert(jsn:is_equal([Proplist, Struct, 3], [Proplist, Eep18, 3])), + ?assertNot(jsn:is_equal(NotEqualProplist, Proplist)), + ?assertNot(jsn:is_equal(NotEqualProplist, Struct)), + ?assertNot(jsn:is_equal(NotEqualProplist, Eep18)). + + +is_subset() -> + Map = generate_json_object(7, [{format, map}]), + Proplist = jsn:as_proplist(Map), + Eep18 = jsn:from_proplist(Proplist, [{format, eep18}]), + Struct = jsn:from_proplist(Proplist, [{format, struct}]), + SuperProplist = [{qqqqqqqqqqqq, 9} | Proplist], + ?assert(jsn:is_subset(Map, SuperProplist)), + ?assert(jsn:is_subset(Proplist, SuperProplist)), + ?assert(jsn:is_subset(Eep18, SuperProplist)), + ?assert(jsn:is_subset(Struct, SuperProplist)), + ?assert(jsn:is_subset([Struct], [6, SuperProplist])), + ?assertNot(jsn:is_subset(SuperProplist, Map)), + ?assertNot(jsn:is_subset(SuperProplist, Proplist)), + ?assertNot(jsn:is_subset(SuperProplist, Eep18)), + ?assertNot(jsn:is_subset(SuperProplist, Struct)), + ?assertNot(jsn:is_subset([SuperProplist, 4], [Struct, 4])). + +-else. is_equal_test_() -> TestParams = [{'foo.bar.baz', null}, @@ -554,6 +805,8 @@ is_subset() -> ?assertNot(jsn:is_subset(SuperProplist, Struct)), ?assertNot(jsn:is_subset([SuperProplist, 4], [Struct, 4])). +-endif. + %% run the above tests several times is_equal_subset_test_() -> {setup, @@ -588,20 +841,38 @@ encode_decode_test_() -> ?_assertThrow({error, _}, jsn:decode("{ ", StructOpt)), ?_assertThrow({error, _}, jsn:encode({error, bogus, object}))]. +-ifdef(maps_support). from_as_proplist() -> - Eep18 = generate_json_object(7, [{format, eep18}]), - Proplist = jsn:as_proplist(Eep18), - Proplist = jsn:from_proplist(Proplist), - Proplist = jsn:from_proplist(Proplist, [{format, proplist}]), + Proplist = jsn:sort_keys(generate_json_object(7, [{format, proplist}])), Eep18 = jsn:from_proplist(Proplist, [{format, eep18}]), Struct = jsn:from_proplist(Proplist, [{format, struct}]), + Map = jsn:from_proplist(Proplist, [{format, map}]), + ?assertEqual(Proplist, jsn:from_proplist(Proplist)), + ?assertEqual(Proplist, jsn:from_proplist(Proplist, [{format, proplist}])), ?assertEqual(Eep18, jsn:from_proplist(Proplist, [{format, eep18}])), + ?assertEqual(Struct, jsn:from_proplist(Proplist, [{format, struct}])), + ?assertEqual(Map, jsn:from_proplist(Proplist, [{format, map}])), ?assertEqual(Proplist, jsn:as_proplist(Proplist)), ?assertEqual(Proplist, jsn:as_proplist(Eep18)), ?assertEqual(Proplist, jsn:as_proplist(Struct)), - ?assert(jsn:is_equal(Proplist, Eep18)), - ?assert(jsn:is_equal(Proplist, Struct)). + ?assertEqual(Proplist, jsn:sort_keys(jsn:as_proplist(Map))). + +-else. + +from_as_proplist() -> + Eep18 = generate_json_object(7, [{format, eep18}]), + Proplist = jsn:as_proplist(Eep18), + Struct = jsn:from_proplist(Proplist, [{format, struct}]), + ?assertEqual(Proplist, jsn:from_proplist(Proplist)), + ?assertEqual(Proplist, jsn:from_proplist(Proplist, [{format, proplist}])), + ?assertEqual(Eep18, jsn:from_proplist(Proplist, [{format, eep18}])), + ?assertEqual(Struct, jsn:from_proplist(Proplist, [{format, struct}])), + ?assertEqual(Proplist, jsn:as_proplist(Proplist)), + ?assertEqual(Proplist, jsn:as_proplist(Eep18)), + ?assertEqual(Proplist, jsn:as_proplist(Struct)). + +-endif. %% run the above test several times from_as_proplist_test_() ->