@ -1,3 +1,6 @@ | |||
{erl_opts, [{i, "include"}]}. | |||
{edoc_opts, [{preprocess, true}]}. | |||
{deps,[ | |||
{jiffy, {git, "https://github.com/davisp/jiffy.git", {tag, "1.0.4"}}} | |||
]}. | |||
@ -1,15 +0,0 @@ | |||
Def0 = case erlang:is_builtin(erlang, binary_to_integer, 1) andalso | |||
erlang:is_builtin(erlang, binary_to_float, 1) of | |||
true -> []; | |||
false -> [{d, no_binary_to_whatever}] | |||
end, | |||
Def1 = case erlang:is_builtin(erlang, is_map, 1) of | |||
true -> [{d, maps_support}|Def0]; | |||
false -> Def0 | |||
end, | |||
Defs = case os:getenv("JSX_FORCE_MAPS") of | |||
false -> Def1; | |||
_ -> [{d, maps_always}|Def1] | |||
end, | |||
lists:keystore(erl_opts, 1, CONFIG, | |||
{erl_opts, proplists:get_value(erl_opts, CONFIG, []) ++ Defs}). |
@ -1,527 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 alisdair sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx). | |||
-export([encode/1, encode/2, decode/1, decode/2]). | |||
-export([is_json/1, is_json/2, is_term/1, is_term/2]). | |||
-export([format/1, format/2, minify/1, prettify/1]). | |||
-export([consult/1, consult/2]). | |||
-export([encoder/3, decoder/3, parser/3]). | |||
-export([resume/3]). | |||
-export([maps_support/0]). | |||
-export_type([json_term/0, json_text/0, token/0]). | |||
-export_type([encoder/0, decoder/0, parser/0, internal_state/0]). | |||
-export_type([config/0]). | |||
-ifdef(TEST). | |||
%% data and helper functions for tests | |||
-export([test_cases/0, special_test_cases/0]). | |||
-export([init/1, handle_event/2]). | |||
-endif. | |||
-ifndef(maps_support). | |||
-type json_term() :: [{binary() | atom(), json_term()}] | [{},...] | |||
| [json_term()] | [] | |||
| true | false | null | |||
| integer() | float() | |||
| binary() | atom() | |||
| calendar:datetime(). | |||
-endif. | |||
-ifdef(maps_support). | |||
-type json_term() :: [{binary() | atom(), json_term()}] | [{},...] | |||
| [json_term()] | [] | |||
| map() | |||
| true | false | null | |||
| integer() | float() | |||
| binary() | atom() | |||
| calendar:datetime(). | |||
-endif. | |||
-type json_text() :: binary(). | |||
-type config() :: jsx_config:config(). | |||
-spec encode(Source::json_term()) -> json_text(). | |||
encode(Source) -> encode(Source, []). | |||
-spec encode(Source::json_term(), Config::jsx_to_json:config()) -> json_text() | {incomplete, encoder()}. | |||
encode(Source, Config) -> jsx_to_json:to_json(Source, Config). | |||
-spec decode(Source::json_text()) -> json_term(). | |||
decode(Source) -> decode(Source, []). | |||
-spec decode(Source::json_text(), Config::jsx_to_term:config()) -> json_term() | {incomplete, decoder()}. | |||
decode(Source, Config) -> jsx_to_term:to_term(Source, Config). | |||
-spec format(Source::json_text()) -> json_text(). | |||
format(Source) -> format(Source, []). | |||
-spec format(Source::json_text(), Config::jsx_to_json:config()) -> json_text() | {incomplete, decoder()}. | |||
format(Source, Config) -> jsx_to_json:format(Source, Config). | |||
-spec minify(Source::json_text()) -> json_text(). | |||
minify(Source) -> format(Source, []). | |||
-spec prettify(Source::json_text()) -> json_text(). | |||
prettify(Source) -> format(Source, [space, {indent, 2}]). | |||
-spec is_json(Source::any()) -> boolean(). | |||
is_json(Source) -> is_json(Source, []). | |||
-spec is_json(Source::any(), Config::jsx_verify:config()) -> boolean() | {incomplete, decoder()}. | |||
is_json(Source, Config) -> jsx_verify:is_json(Source, Config). | |||
-spec is_term(Source::any()) -> boolean(). | |||
is_term(Source) -> is_term(Source, []). | |||
-spec is_term(Source::any(), Config::jsx_verify:config()) -> boolean() | {incomplete, encoder()}. | |||
is_term(Source, Config) -> jsx_verify:is_term(Source, Config). | |||
-spec consult(File::file:name_all()) -> list(json_term()). | |||
consult(File) -> consult(File, []). | |||
-spec consult(File::file:name_all(), Config::jsx_to_term:config()) -> list(json_term()). | |||
consult(File, Config) -> jsx_consult:consult(File, Config). | |||
-type decoder() :: fun((json_text() | end_stream | end_json) -> any()). | |||
-spec decoder(Handler::module(), State::any(), Config::list()) -> decoder(). | |||
decoder(Handler, State, Config) -> jsx_decoder:decoder(Handler, State, Config). | |||
-type encoder() :: fun((json_term() | end_stream | end_json) -> any()). | |||
-spec encoder(Handler::module(), State::any(), Config::list()) -> encoder(). | |||
encoder(Handler, State, Config) -> jsx_encoder:encoder(Handler, State, Config). | |||
-type token() :: [token()] | |||
| start_object | |||
| end_object | |||
| start_array | |||
| end_array | |||
| {key, binary()} | |||
| {string, binary()} | |||
| binary() | |||
| {number, integer() | float()} | |||
| {integer, integer()} | |||
| {float, float()} | |||
| integer() | |||
| float() | |||
| {literal, true} | |||
| {literal, false} | |||
| {literal, null} | |||
| true | |||
| false | |||
| null | |||
| end_json. | |||
-type parser() :: fun((token() | end_stream) -> any()). | |||
-spec parser(Handler::module(), State::any(), Config::list()) -> parser(). | |||
parser(Handler, State, Config) -> jsx_parser:parser(Handler, State, Config). | |||
-opaque internal_state() :: tuple(). | |||
-spec resume(Term::json_text() | token(), InternalState::internal_state(), Config::list()) -> any(). | |||
resume(Term, {decoder, State, Handler, Acc, Stack}, Config) -> | |||
jsx_decoder:resume(Term, State, Handler, Acc, Stack, jsx_config:parse_config(Config)); | |||
resume(Term, {parser, State, Handler, Stack}, Config) -> | |||
jsx_parser:resume(Term, State, Handler, Stack, jsx_config:parse_config(Config)). | |||
-spec maps_support() -> boolean(). | |||
-ifndef(maps_support). | |||
maps_support() -> false. | |||
-endif. | |||
-ifdef(maps_support). | |||
maps_support() -> true. | |||
-endif. | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
%% test handler | |||
init([]) -> []. | |||
handle_event(end_json, State) -> lists:reverse([end_json] ++ State); | |||
handle_event(Event, State) -> [Event] ++ State. | |||
test_cases() -> | |||
empty_array() | |||
++ nested_array() | |||
++ empty_object() | |||
++ nested_object() | |||
++ strings() | |||
++ literals() | |||
++ integers() | |||
++ floats() | |||
++ compound_object(). | |||
%% segregate these so we can skip them in `jsx_to_term` | |||
special_test_cases() -> special_objects() ++ special_array(). | |||
empty_array() -> [{"[]", <<"[]">>, [], [start_array, end_array]}]. | |||
nested_array() -> | |||
[{ | |||
"[[[]]]", | |||
<<"[[[]]]">>, | |||
[[[]]], | |||
[start_array, start_array, start_array, end_array, end_array, end_array] | |||
}]. | |||
empty_object() -> [{"{}", <<"{}">>, [{}], [start_object, end_object]}]. | |||
nested_object() -> | |||
[{ | |||
"{\"key\":{\"key\":{}}}", | |||
<<"{\"key\":{\"key\":{}}}">>, | |||
[{<<"key">>, [{<<"key">>, [{}]}]}], | |||
[ | |||
start_object, | |||
{key, <<"key">>}, | |||
start_object, | |||
{key, <<"key">>}, | |||
start_object, | |||
end_object, | |||
end_object, | |||
end_object | |||
] | |||
}]. | |||
naked_strings() -> | |||
Raw = [ | |||
"", | |||
"hello world" | |||
], | |||
[ | |||
{ | |||
String, | |||
<<"\"", (list_to_binary(String))/binary, "\"">>, | |||
list_to_binary(String), | |||
[{string, list_to_binary(String)}] | |||
} | |||
|| String <- Raw | |||
]. | |||
strings() -> | |||
naked_strings() | |||
++ [ wrap_with_array(Test) || Test <- naked_strings() ] | |||
++ [ wrap_with_object(Test) || Test <- naked_strings() ]. | |||
naked_integers() -> | |||
Raw = [ | |||
1, 2, 3, | |||
127, 128, 129, | |||
255, 256, 257, | |||
65534, 65535, 65536, | |||
18446744073709551616, | |||
18446744073709551617 | |||
], | |||
[ | |||
{ | |||
integer_to_list(X), | |||
list_to_binary(integer_to_list(X)), | |||
X, | |||
[{integer, X}] | |||
} | |||
|| X <- Raw ++ [ -1 * Y || Y <- Raw ] ++ [0] | |||
]. | |||
integers() -> | |||
naked_integers() | |||
++ [ wrap_with_array(Test) || Test <- naked_integers() ] | |||
++ [ wrap_with_object(Test) || Test <- naked_integers() ]. | |||
naked_floats() -> | |||
Raw = [ | |||
0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, | |||
1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, | |||
1234567890.0987654321, | |||
0.0e0, | |||
1234567890.0987654321e16, | |||
0.1e0, 0.1e1, 0.1e2, 0.1e4, 0.1e8, 0.1e16, 0.1e308, | |||
1.0e0, 1.0e1, 1.0e2, 1.0e4, 1.0e8, 1.0e16, 1.0e308, | |||
2.2250738585072014e-308, %% min normalized float | |||
1.7976931348623157e308, %% max normalized float | |||
5.0e-324, %% min denormalized float | |||
2.225073858507201e-308 %% max denormalized float | |||
], | |||
[ | |||
{ | |||
sane_float_to_list(X), | |||
list_to_binary(sane_float_to_list(X)), | |||
X, | |||
[{float, X}] | |||
} | |||
|| X <- Raw ++ [ -1 * Y || Y <- Raw ] | |||
]. | |||
floats() -> | |||
naked_floats() | |||
++ [ wrap_with_array(Test) || Test <- naked_floats() ] | |||
++ [ wrap_with_object(Test) || Test <- naked_floats() ]. | |||
naked_literals() -> | |||
[ | |||
{ | |||
atom_to_list(Literal), | |||
atom_to_binary(Literal, unicode), | |||
Literal, | |||
[{literal, Literal}] | |||
} | |||
|| Literal <- [true, false, null] | |||
]. | |||
literals() -> | |||
naked_literals() | |||
++ [ wrap_with_array(Test) || Test <- naked_literals() ] | |||
++ [ wrap_with_object(Test) || Test <- naked_literals() ]. | |||
compound_object() -> | |||
[{ | |||
"[{\"alpha\":[1,2,3],\"beta\":{\"alpha\":[1.0,2.0,3.0],\"beta\":[true,false]}},[{}]]", | |||
<<"[{\"alpha\":[1,2,3],\"beta\":{\"alpha\":[1.0,2.0,3.0],\"beta\":[true,false]}},[{}]]">>, | |||
[[{<<"alpha">>, [1, 2, 3]}, {<<"beta">>, [{<<"alpha">>, [1.0, 2.0, 3.0]}, {<<"beta">>, [true, false]}]}], [[{}]]], | |||
[ | |||
start_array, | |||
start_object, | |||
{key, <<"alpha">>}, | |||
start_array, | |||
{integer, 1}, | |||
{integer, 2}, | |||
{integer, 3}, | |||
end_array, | |||
{key, <<"beta">>}, | |||
start_object, | |||
{key, <<"alpha">>}, | |||
start_array, | |||
{float, 1.0}, | |||
{float, 2.0}, | |||
{float, 3.0}, | |||
end_array, | |||
{key, <<"beta">>}, | |||
start_array, | |||
{literal, true}, | |||
{literal, false}, | |||
end_array, | |||
end_object, | |||
end_object, | |||
start_array, | |||
start_object, | |||
end_object, | |||
end_array, | |||
end_array | |||
] | |||
}]. | |||
special_objects() -> | |||
[ | |||
{ | |||
"[{key, atom}]", | |||
<<"{\"key\":\"atom\"}">>, | |||
[{key, atom}], | |||
[start_object, {key, <<"key">>}, {string, <<"atom">>}, end_object] | |||
}, | |||
{ | |||
"[{1, true}]", | |||
<<"{\"1\":true}">>, | |||
[{1, true}], | |||
[start_object, {key, <<"1">>}, {literal, true}, end_object] | |||
} | |||
]. | |||
special_array() -> | |||
[ | |||
{ | |||
"[foo, bar]", | |||
<<"[\"foo\",\"bar\"]">>, | |||
[foo, bar], | |||
[start_array, {string, <<"foo">>}, {string, <<"bar">>}, end_array] | |||
} | |||
]. | |||
wrap_with_array({Title, JSON, Term, Events}) -> | |||
{ | |||
"[" ++ Title ++ "]", | |||
<<"[", JSON/binary, "]">>, | |||
[Term], | |||
[start_array] ++ Events ++ [end_array] | |||
}. | |||
wrap_with_object({Title, JSON, Term, Events}) -> | |||
{ | |||
"{\"key\":" ++ Title ++ "}", | |||
<<"{\"key\":", JSON/binary, "}">>, | |||
[{<<"key">>, Term}], | |||
[start_object, {key, <<"key">>}] ++ Events ++ [end_object] | |||
}. | |||
sane_float_to_list(X) -> | |||
[Output] = io_lib:format("~p", [X]), | |||
Output. | |||
incremental_decode(JSON) -> | |||
Final = lists:foldl( | |||
fun(Byte, Decoder) -> {incomplete, F} = Decoder(Byte), F end, | |||
decoder(jsx, [], [stream]), | |||
json_to_bytes(JSON) | |||
), | |||
Final(end_stream). | |||
incremental_parse(Events) -> | |||
Final = lists:foldl( | |||
fun(Event, Parser) -> {incomplete, F} = Parser(Event), F end, | |||
parser(?MODULE, [], [stream]), | |||
lists:map(fun(X) -> [X] end, Events) | |||
), | |||
Final(end_stream). | |||
%% used to convert a json text into a list of codepoints to be incrementally | |||
%% parsed | |||
json_to_bytes(JSON) -> json_to_bytes(JSON, []). | |||
json_to_bytes(<<>>, Acc) -> [<<>>] ++ lists:reverse(Acc); | |||
json_to_bytes(<<X, Rest/binary>>, Acc) -> json_to_bytes(Rest, [<<X>>] ++ Acc). | |||
%% actual tests! | |||
decode_test_() -> | |||
Data = test_cases(), | |||
[{Title, ?_assertEqual(Events ++ [end_json], (decoder(?MODULE, [], []))(JSON))} | |||
|| {Title, JSON, _, Events} <- Data | |||
] ++ | |||
[{Title ++ " (incremental)", ?_assertEqual(Events ++ [end_json], incremental_decode(JSON))} | |||
|| {Title, JSON, _, Events} <- Data | |||
]. | |||
parse_test_() -> | |||
Data = test_cases(), | |||
[{Title, ?_assertEqual(Events ++ [end_json], (parser(?MODULE, [], []))(Events ++ [end_json]))} | |||
|| {Title, _, _, Events} <- Data | |||
] ++ | |||
[{Title ++ " (incremental)", ?_assertEqual(Events ++ [end_json], incremental_parse(Events))} | |||
|| {Title, _, _, Events} <- Data | |||
]. | |||
encode_test_() -> | |||
Data = test_cases(), | |||
[ | |||
{ | |||
Title, ?_assertEqual( | |||
Events ++ [end_json], | |||
(jsx:encoder(jsx, [], []))(Term) | |||
) | |||
} || {Title, _, Term, Events} <- Data | |||
]. | |||
end_stream_test_() -> | |||
Tokens = [start_object, end_object, end_json], | |||
[ | |||
{"encoder end_stream", ?_assertEqual( | |||
Tokens, | |||
begin | |||
{incomplete, F} = (jsx:parser(jsx, [], [stream]))([start_object, end_object]), | |||
F(end_stream) | |||
end | |||
)}, | |||
{"encoder end_json", ?_assertEqual( | |||
Tokens, | |||
begin | |||
{incomplete, F} = (jsx:parser(jsx, [], [stream]))([start_object, end_object]), | |||
F(end_json) | |||
end | |||
)}, | |||
{"decoder end_stream", ?_assertEqual( | |||
Tokens, | |||
begin {incomplete, F} = (jsx:decoder(jsx, [], [stream]))(<<"{}">>), F(end_stream) end | |||
)}, | |||
{"decoder end_json", ?_assertEqual( | |||
Tokens, | |||
begin {incomplete, F} = (jsx:decoder(jsx, [], [stream]))(<<"{}">>), F(end_json) end | |||
)} | |||
]. | |||
-endif. |
@ -1,346 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 alisdair sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_config). | |||
-export([parse_config/1]). | |||
-export([config_to_list/1]). | |||
-export([extract_config/1, valid_flags/0]). | |||
-ifdef(TEST). | |||
-export([fake_error_handler/3]). | |||
-endif. | |||
-include("jsx_config.hrl"). | |||
-type handler_type(Handler) :: | |||
fun((jsx:json_text() | end_stream | | |||
jsx:json_term(), | |||
{decoder, any(), module(), null | list(), list()} | | |||
{parser, any(), module(), list()} | | |||
{encoder, any(), module()}, | |||
list({pre_encode, fun((any()) -> any())} | | |||
{error_handler, Handler} | | |||
{incomplete_handler, Handler} | | |||
atom())) -> any()). | |||
-type handler() :: handler_type(handler()). | |||
-export_type([handler/0]). | |||
-type config() :: #config{}. | |||
-export_type([config/0]). | |||
%% parsing of jsx config | |||
-spec parse_config(Config::proplists:proplist()) -> config(). | |||
parse_config(Config) -> parse_config(Config, #config{}). | |||
parse_config([], Config) -> Config; | |||
parse_config([escaped_forward_slashes|Rest], Config) -> | |||
parse_config(Rest, Config#config{escaped_forward_slashes=true}); | |||
parse_config([escaped_strings|Rest], Config) -> | |||
parse_config(Rest, Config#config{escaped_strings=true}); | |||
parse_config([unescaped_jsonp|Rest], Config) -> | |||
parse_config(Rest, Config#config{unescaped_jsonp=true}); | |||
parse_config([dirty_strings|Rest], Config) -> | |||
parse_config(Rest, Config#config{dirty_strings=true}); | |||
parse_config([multi_term|Rest], Config) -> | |||
parse_config(Rest, Config#config{multi_term=true}); | |||
parse_config([return_tail|Rest], Config) -> | |||
parse_config(Rest, Config#config{return_tail=true}); | |||
%% retained for backwards compat, now does nothing however | |||
parse_config([repeat_keys|Rest], Config) -> | |||
parse_config(Rest, Config); | |||
parse_config([uescape|Rest], Config) -> | |||
parse_config(Rest, Config#config{uescape=true}); | |||
parse_config([strict|Rest], Config) -> | |||
parse_config(Rest, Config#config{ | |||
strict_comments=true, | |||
strict_commas=true, | |||
strict_utf8=true, | |||
strict_single_quotes=true, | |||
strict_escapes=true, | |||
strict_control_codes=true | |||
}); | |||
parse_config([{strict, Strict}|Rest], Config) -> | |||
parse_strict(Strict, Rest, Config); | |||
parse_config([stream|Rest], Config) -> | |||
parse_config(Rest, Config#config{stream=true}); | |||
parse_config([{error_handler, ErrorHandler}|Rest] = Options, Config) when is_function(ErrorHandler, 3) -> | |||
case Config#config.error_handler of | |||
false -> parse_config(Rest, Config#config{error_handler=ErrorHandler}) | |||
; _ -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([{incomplete_handler, IncompleteHandler}|Rest] = Options, Config) when is_function(IncompleteHandler, 3) -> | |||
case Config#config.incomplete_handler of | |||
false -> parse_config(Rest, Config#config{incomplete_handler=IncompleteHandler}) | |||
; _ -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config(_Options, _Config) -> erlang:error(badarg). | |||
parse_strict([], Rest, Config) -> parse_config(Rest, Config); | |||
parse_strict([comments|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_comments=true}); | |||
parse_strict([trailing_commas|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_commas=true}); | |||
parse_strict([utf8|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_utf8=true}); | |||
parse_strict([single_quotes|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_single_quotes=true}); | |||
parse_strict([escapes|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_escapes=true}); | |||
parse_strict([control_codes|Strict], Rest, Config) -> | |||
parse_strict(Strict, Rest, Config#config{strict_control_codes=true}); | |||
parse_strict(_Strict, _Rest, _Config) -> | |||
erlang:error(badarg). | |||
-spec config_to_list(Config::config()) -> proplists:proplist(). | |||
config_to_list(Config) -> | |||
reduce_config(lists:map( | |||
fun ({error_handler, F}) -> {error_handler, F}; | |||
({incomplete_handler, F}) -> {incomplete_handler, F}; | |||
({Key, true}) -> Key | |||
end, | |||
lists:filter( | |||
fun({_, false}) -> false; (_) -> true end, | |||
lists:zip(record_info(fields, config), tl(tuple_to_list(Config))) | |||
) | |||
)). | |||
reduce_config(Input) -> reduce_config(Input, [], []). | |||
reduce_config([], Output, Strict) -> | |||
case length(Strict) of | |||
0 -> lists:reverse(Output); | |||
5 -> lists:reverse(Output) ++ [strict]; | |||
_ -> lists:reverse(Output) ++ [{strict, lists:reverse(Strict)}] | |||
end; | |||
reduce_config([strict_comments|Input], Output, Strict) -> | |||
reduce_config(Input, Output, [comments] ++ Strict); | |||
reduce_config([strict_utf8|Input], Output, Strict) -> | |||
reduce_config(Input, Output, [utf8] ++ Strict); | |||
reduce_config([strict_single_quotes|Input], Output, Strict) -> | |||
reduce_config(Input, Output, [single_quotes] ++ Strict); | |||
reduce_config([strict_escapes|Input], Output, Strict) -> | |||
reduce_config(Input, Output, [escapes] ++ Strict); | |||
reduce_config([strict_control_codes|Input], Output, Strict) -> | |||
reduce_config(Input, Output, [control_codes] ++ Strict); | |||
reduce_config([Else|Input], Output, Strict) -> | |||
reduce_config(Input, [Else] ++ Output, Strict). | |||
-spec valid_flags() -> [atom()]. | |||
valid_flags() -> | |||
[ | |||
escaped_forward_slashes, | |||
escaped_strings, | |||
unescaped_jsonp, | |||
dirty_strings, | |||
multi_term, | |||
return_tail, | |||
repeat_keys, | |||
strict, | |||
stream, | |||
uescape, | |||
error_handler, | |||
incomplete_handler | |||
]. | |||
-spec extract_config(Config::proplists:proplist()) -> proplists:proplist(). | |||
extract_config(Config) -> | |||
extract_parser_config(Config, []). | |||
extract_parser_config([], Acc) -> Acc; | |||
extract_parser_config([{K,V}|Rest], Acc) -> | |||
case lists:member(K, valid_flags()) of | |||
true -> extract_parser_config(Rest, [{K,V}] ++ Acc) | |||
; false -> extract_parser_config(Rest, Acc) | |||
end; | |||
extract_parser_config([K|Rest], Acc) -> | |||
case lists:member(K, valid_flags()) of | |||
true -> extract_parser_config(Rest, [K] ++ Acc) | |||
; false -> extract_parser_config(Rest, Acc) | |||
end. | |||
%% eunit tests | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
config_test_() -> | |||
[ | |||
{"all flags", | |||
?_assertEqual( | |||
#config{escaped_forward_slashes = true, | |||
escaped_strings = true, | |||
unescaped_jsonp = true, | |||
dirty_strings = true, | |||
multi_term = true, | |||
return_tail = true, | |||
strict_comments = true, | |||
strict_commas = true, | |||
strict_utf8 = true, | |||
strict_single_quotes = true, | |||
strict_escapes = true, | |||
strict_control_codes = true, | |||
stream = true, | |||
uescape = true | |||
}, | |||
parse_config([dirty_strings, | |||
escaped_forward_slashes, | |||
escaped_strings, | |||
unescaped_jsonp, | |||
multi_term, | |||
return_tail, | |||
repeat_keys, | |||
strict, | |||
stream, | |||
uescape | |||
]) | |||
) | |||
}, | |||
{"strict flag", | |||
?_assertEqual( | |||
#config{strict_comments = true, | |||
strict_commas = true, | |||
strict_utf8 = true, | |||
strict_single_quotes = true, | |||
strict_escapes = true, | |||
strict_control_codes = true | |||
}, | |||
parse_config([strict]) | |||
) | |||
}, | |||
{"strict selective", | |||
?_assertEqual( | |||
#config{strict_comments = true}, | |||
parse_config([{strict, [comments]}]) | |||
) | |||
}, | |||
{"strict expanded", | |||
?_assertEqual( | |||
#config{strict_comments = true, | |||
strict_utf8 = true, | |||
strict_single_quotes = true, | |||
strict_escapes = true | |||
}, | |||
parse_config([{strict, [comments, utf8, single_quotes, escapes]}]) | |||
) | |||
}, | |||
{"error_handler flag", ?_assertEqual( | |||
#config{error_handler=fun ?MODULE:fake_error_handler/3}, | |||
parse_config([{error_handler, fun ?MODULE:fake_error_handler/3}]) | |||
)}, | |||
{"two error_handlers defined", ?_assertError( | |||
badarg, | |||
parse_config([ | |||
{error_handler, fun(_, _, _) -> true end}, | |||
{error_handler, fun(_, _, _) -> false end} | |||
]) | |||
)}, | |||
{"incomplete_handler flag", ?_assertEqual( | |||
#config{incomplete_handler=fun ?MODULE:fake_error_handler/3}, | |||
parse_config([{incomplete_handler, fun ?MODULE:fake_error_handler/3}]) | |||
)}, | |||
{"two incomplete_handlers defined", ?_assertError( | |||
badarg, | |||
parse_config([ | |||
{incomplete_handler, fun(_, _, _) -> true end}, | |||
{incomplete_handler, fun(_, _, _) -> false end} | |||
]) | |||
)}, | |||
{"bad option flag", ?_assertError(badarg, parse_config([this_flag_does_not_exist]))} | |||
]. | |||
config_to_list_test_() -> | |||
[ | |||
{"empty config", ?_assertEqual( | |||
[], | |||
config_to_list(#config{}) | |||
)}, | |||
{"all flags", ?_assertEqual( | |||
[dirty_strings, | |||
escaped_forward_slashes, | |||
escaped_strings, | |||
multi_term, | |||
stream, | |||
uescape, | |||
unescaped_jsonp, | |||
strict | |||
], | |||
config_to_list( | |||
#config{escaped_forward_slashes = true, | |||
escaped_strings = true, | |||
unescaped_jsonp = true, | |||
dirty_strings = true, | |||
multi_term = true, | |||
strict_comments = true, | |||
strict_utf8 = true, | |||
strict_single_quotes = true, | |||
strict_escapes = true, | |||
strict_control_codes = true, | |||
stream = true, | |||
uescape = true | |||
} | |||
) | |||
)}, | |||
{"single strict", ?_assertEqual( | |||
[{strict, [comments]}], | |||
config_to_list(#config{strict_comments = true}) | |||
)}, | |||
{"multiple strict", ?_assertEqual( | |||
[{strict, [utf8, single_quotes, escapes]}], | |||
config_to_list(#config{strict_utf8 = true, strict_single_quotes = true, strict_escapes = true}) | |||
)}, | |||
{"all strict", ?_assertEqual( | |||
[strict], | |||
config_to_list(#config{strict_comments = true, | |||
strict_utf8 = true, | |||
strict_single_quotes = true, | |||
strict_escapes = true, | |||
strict_control_codes = true}) | |||
)}, | |||
{"error handler", ?_assertEqual( | |||
[{error_handler, fun ?MODULE:fake_error_handler/3}], | |||
config_to_list(#config{error_handler=fun ?MODULE:fake_error_handler/3}) | |||
)}, | |||
{"incomplete handler", ?_assertEqual( | |||
[{incomplete_handler, fun ?MODULE:fake_error_handler/3}], | |||
config_to_list(#config{incomplete_handler=fun ?MODULE:fake_error_handler/3}) | |||
)} | |||
]. | |||
fake_error_handler(_, _, _) -> ok. | |||
-endif. |
@ -1,18 +0,0 @@ | |||
-record(config, { | |||
dirty_strings = false :: boolean(), | |||
escaped_forward_slashes = false :: boolean(), | |||
escaped_strings = false :: boolean(), | |||
multi_term = false :: boolean(), | |||
strict_comments = false :: boolean(), | |||
strict_commas = false :: boolean(), | |||
strict_utf8 = false :: boolean(), | |||
strict_single_quotes = false :: boolean(), | |||
strict_escapes = false :: boolean(), | |||
strict_control_codes = false :: boolean(), | |||
stream = false :: boolean(), | |||
return_tail = false :: boolean(), | |||
uescape = false :: boolean(), | |||
unescaped_jsonp = false :: boolean(), | |||
error_handler = false :: false | jsx_config:handler(), | |||
incomplete_handler = false :: false | jsx_config:handler() | |||
}). |
@ -1,99 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2015 Alisdair Sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_consult). | |||
-export([consult/2]). | |||
-export([init/1, reset/1, handle_event/2]). | |||
-record(config, { | |||
labels = binary, | |||
return_maps = false | |||
}). | |||
-type config() :: list(). | |||
-export_type([config/0]). | |||
-ifndef(maps_support). | |||
-type json_value() :: list(json_value()) | |||
| list({binary() | atom(), json_value()}) | |||
| true | |||
| false | |||
| null | |||
| integer() | |||
| float() | |||
| binary(). | |||
-endif. | |||
-ifdef(maps_support). | |||
-type json_value() :: list(json_value()) | |||
| map() | |||
| true | |||
| false | |||
| null | |||
| integer() | |||
| float() | |||
| binary(). | |||
-endif. | |||
-ifdef(maps_always). | |||
opts(Opts) -> [return_maps, multi_term] ++ Opts. | |||
-endif. | |||
-ifndef(maps_always). | |||
opts(Opts) -> [multi_term] ++ Opts. | |||
-endif. | |||
-spec consult(File::file:name_all(), Config::config()) -> [json_value()]. | |||
consult(File, Config) when is_list(Config) -> | |||
case file:read_file(File) of | |||
{ok, Bin} -> | |||
{Final, _, _} = (jsx:decoder( | |||
?MODULE, | |||
opts(Config), | |||
jsx_config:extract_config(opts(Config)) | |||
))(Bin), | |||
lists:reverse(Final); | |||
{error, _} -> erlang:error(badarg) | |||
end. | |||
-type state() :: {[], proplists:proplist(), {list(), #config{}}}. | |||
-spec init(Config::proplists:proplist()) -> state(). | |||
init(Config) -> {[], Config, jsx_to_term:start_term(Config)}. | |||
-spec reset(State::state()) -> state(). | |||
reset({Acc, Config, _}) -> {Acc, Config, jsx_to_term:start_term(Config)}. | |||
-spec handle_event(Event::any(), State::state()) -> state(). | |||
handle_event(end_json, {Acc, Config, State}) -> | |||
{[jsx_to_term:get_value(State)] ++ Acc, Config, State}; | |||
handle_event(Event, {Acc, Config, State}) -> | |||
{Acc, Config, jsx_to_term:handle_event(Event, State)}. |
@ -1,127 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 Alisdair Sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_encoder). | |||
-export([encoder/3, encode/1, encode/2]). | |||
-spec encoder(Handler::module(), State::any(), Config::list()) -> jsx:encoder(). | |||
encoder(Handler, State, Config) -> | |||
Parser = jsx:parser(Handler, State, Config), | |||
fun(Term) -> Parser(encode(Term) ++ [end_json]) end. | |||
-spec encode(Term::any()) -> any(). | |||
encode(Term) -> encode(Term, ?MODULE). | |||
-spec encode(Term::any(), EntryPoint::module()) -> any(). | |||
-ifndef(maps_support). | |||
encode(Term, EntryPoint) -> encode_(Term, EntryPoint). | |||
-endif. | |||
-ifdef(maps_support). | |||
encode(Map, _EntryPoint) when is_map(Map), map_size(Map) < 1 -> | |||
[start_object, end_object]; | |||
encode(Term, EntryPoint) when is_map(Term) -> | |||
[start_object] ++ unpack(Term, EntryPoint); | |||
encode(Term, EntryPoint) -> encode_(Term, EntryPoint). | |||
-endif. | |||
encode_([], _EntryPoint) -> [start_array, end_array]; | |||
encode_([{}], _EntryPoint) -> [start_object, end_object]; | |||
%% datetime special case | |||
encode_([{{_,_,_},{_,_,_}} = DateTime|Rest], EntryPoint) -> | |||
[start_array] ++ [DateTime] ++ unhitch(Rest, EntryPoint); | |||
encode_([{_, _}|_] = Term, EntryPoint) -> | |||
[start_object] ++ unzip(Term, EntryPoint); | |||
encode_(Term, EntryPoint) when is_list(Term) -> | |||
[start_array] ++ unhitch(Term, EntryPoint); | |||
encode_(Else, _EntryPoint) -> [Else]. | |||
unzip([{K, V}|Rest], EntryPoint) when is_integer(K); is_binary(K); is_atom(K) -> | |||
[K] ++ EntryPoint:encode(V, EntryPoint) ++ unzip(Rest, EntryPoint); | |||
unzip([], _) -> [end_object]; | |||
unzip(_, _) -> erlang:error(badarg). | |||
unhitch([V|Rest], EntryPoint) -> | |||
EntryPoint:encode(V, EntryPoint) ++ unhitch(Rest, EntryPoint); | |||
unhitch([], _) -> [end_array]. | |||
-ifdef(maps_support). | |||
unpack(Map, EntryPoint) -> unpack(Map, maps:keys(Map), EntryPoint). | |||
unpack(Map, [K|Rest], EntryPoint) when is_integer(K); is_binary(K); is_atom(K) -> | |||
[K] ++ EntryPoint:encode(maps:get(K, Map), EntryPoint) ++ unpack(Map, Rest, EntryPoint); | |||
unpack(_, [], _) -> [end_object]. | |||
-endif. | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
parser(Term, Opts) -> (jsx:parser(jsx, [], Opts))(Term). | |||
error_test_() -> | |||
[ | |||
{"value error", ?_assertError(badarg, parser(self(), []))}, | |||
{"string error", ?_assertError(badarg, parser(<<239, 191, 191>>, [strict]))} | |||
]. | |||
custom_error_handler_test_() -> | |||
Error = fun(Term, {_, State, _, _}, _) -> {State, Term} end, | |||
[ | |||
{"value error", ?_assertEqual( | |||
{value, [self()]}, | |||
parser(self(), [{error_handler, Error}]) | |||
)}, | |||
{"string error", ?_assertEqual( | |||
{value, [{string, <<237, 160, 128>>}]}, | |||
parser(<<237, 160, 128>>, [{error_handler, Error}, strict]) | |||
)} | |||
]. | |||
improper_lists_test_() -> | |||
[ | |||
{"improper proplist", ?_assertError( | |||
badarg, | |||
encode([{<<"key">>, <<"value">>}, false]) | |||
)}, | |||
{"improper list", ?_assertError( | |||
badarg, | |||
encode([{literal, true}, false, null]) | |||
)} | |||
]. | |||
-endif. |
@ -1,409 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 alisdair sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_to_json). | |||
-export([to_json/2, format/2]). | |||
-export([init/1, handle_event/2]). | |||
-export([start_json/0, start_json/1]). | |||
-export([start_object/1, start_array/1, finish/1, insert/2, get_key/1, get_value/1]). | |||
-record(config, { | |||
space = 0, | |||
indent = 0, | |||
depth = 0, | |||
newline = <<$\n>> | |||
}). | |||
-type config() :: list(). | |||
-export_type([config/0]). | |||
-spec to_json(Source::any(), Config::config()) -> binary(). | |||
to_json(Source, Config) when is_list(Config) -> | |||
(jsx:encoder(?MODULE, Config, jsx_config:extract_config(Config ++ [escaped_strings])))(Source). | |||
-spec format(Source::binary(), Config::config()) -> binary(). | |||
format(Source, Config) when is_binary(Source) andalso is_list(Config) -> | |||
(jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config ++ [escaped_strings])))(Source); | |||
format(_, _) -> erlang:error(badarg). | |||
parse_config(Config) -> parse_config(Config, #config{}). | |||
parse_config([{space, Val}|Rest], Config) when is_integer(Val), Val > 0 -> | |||
parse_config(Rest, Config#config{space = Val}); | |||
parse_config([space|Rest], Config) -> | |||
parse_config(Rest, Config#config{space = 1}); | |||
parse_config([{indent, Val}|Rest], Config) when is_integer(Val), Val > 0 -> | |||
parse_config(Rest, Config#config{indent = Val}); | |||
parse_config([indent|Rest], Config) -> | |||
parse_config(Rest, Config#config{indent = 1}); | |||
parse_config([{newline, Val}|Rest], Config) when is_binary(Val) -> | |||
parse_config(Rest, Config#config{newline = Val}); | |||
parse_config([{K, _}|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config) | |||
; false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([K|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config) | |||
; false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([], Config) -> | |||
Config. | |||
-define(start_object, <<"{">>). | |||
-define(start_array, <<"[">>). | |||
-define(end_object, <<"}">>). | |||
-define(end_array, <<"]">>). | |||
-define(colon, <<":">>). | |||
-define(comma, <<",">>). | |||
-define(quote, <<"\"">>). | |||
-define(space, <<" ">>). | |||
-define(newline, <<"\n">>). | |||
-type state() :: {unicode:charlist(), #config{}}. | |||
-spec init(Config::proplists:proplist()) -> state(). | |||
init(Config) -> {[], parse_config(Config)}. | |||
-spec handle_event(Event::any(), State::state()) -> state(). | |||
handle_event(end_json, State) -> get_value(State); | |||
handle_event(start_object, State) -> start_object(State); | |||
handle_event(end_object, State) -> finish(State); | |||
handle_event(start_array, State) -> start_array(State); | |||
handle_event(end_array, State) -> finish(State); | |||
handle_event({Type, Event}, {_, Config} = State) -> insert(encode(Type, Event, Config), State). | |||
encode(string, String, _Config) -> | |||
[?quote, String, ?quote]; | |||
encode(key, Key, _Config) -> | |||
[?quote, Key, ?quote]; | |||
encode(literal, Literal, _Config) -> | |||
erlang:atom_to_list(Literal); | |||
encode(integer, Integer, _Config) -> | |||
erlang:integer_to_list(Integer); | |||
encode(float, Float, _Config) -> | |||
io_lib:format("~p", [Float]). | |||
space(Config) -> | |||
case Config#config.space of | |||
0 -> <<>> | |||
; X when X > 0 -> binary:copy(?space, X) | |||
end. | |||
indent(Config) -> | |||
case Config#config.indent of | |||
0 -> <<>> | |||
; X when X > 0 -> <<(Config#config.newline)/binary, (binary:copy(?space, X * Config#config.depth))/binary>> | |||
end. | |||
indent_or_space(Config) -> | |||
case Config#config.indent > 0 of | |||
true -> indent(Config) | |||
; false -> space(Config) | |||
end. | |||
%% internal state is a stack and a config object | |||
%% `{Stack, Config}` | |||
%% the stack is a list of in progress objects/arrays | |||
%% `[Current, Parent, Grandparent,...OriginalAncestor]` | |||
%% an object has the representation on the stack of | |||
%% `{object, Object}` | |||
%% of if there's a key with a yet to be matched value | |||
%% `{object, Key, Object}` | |||
%% an array looks like | |||
%% `{array, Array}` | |||
%% `Object` and `Array` are utf8 encoded binaries | |||
start_json() -> {[], #config{}}. | |||
start_json(Config) when is_list(Config) -> {[], parse_config(Config)}. | |||
%% allocate a new object on top of the stack | |||
start_object({Stack, Config = #config{depth = Depth}}) -> | |||
{[{object, ?start_object}] ++ Stack, Config#config{depth = Depth + 1}}. | |||
%% allocate a new array on top of the stack | |||
start_array({Stack, Config = #config{depth = Depth}}) -> | |||
{[{array, ?start_array}] ++ Stack, Config#config{depth = Depth + 1}}. | |||
%% finish an object or array and insert it into the parent object if it exists | |||
finish({Stack, Config = #config{depth = Depth}}) -> | |||
NewConfig = Config#config{depth = Depth - 1}, | |||
finish_({Stack, NewConfig}). | |||
finish_({[{object, <<"{">>}], Config}) -> {<<"{}">>, Config}; | |||
finish_({[{array, <<"[">>}], Config}) -> {<<"[]">>, Config}; | |||
finish_({[{object, <<"{">>}|Rest], Config}) -> insert(<<"{}">>, {Rest, Config}); | |||
finish_({[{array, <<"[">>}|Rest], Config}) -> insert(<<"[]">>, {Rest, Config}); | |||
finish_({[{object, Object}], Config}) -> | |||
{[Object, indent(Config), ?end_object], Config}; | |||
finish_({[{object, Object}|Rest], Config}) -> | |||
insert([Object, indent(Config), ?end_object], {Rest, Config}); | |||
finish_({[{array, Array}], Config}) -> | |||
{[Array, indent(Config), ?end_array], Config}; | |||
finish_({[{array, Array}|Rest], Config}) -> | |||
insert([Array, indent(Config), ?end_array], {Rest, Config}); | |||
finish_(_) -> erlang:error(badarg). | |||
%% insert a value when there's no parent object or array | |||
insert(Value, {[], Config}) -> | |||
{Value, Config}; | |||
%% insert a key or value into an object or array, autodetects the 'right' thing | |||
insert(Key, {[{object, Object}|Rest], Config}) -> | |||
{[{object, Key, Object}] ++ Rest, Config}; | |||
insert(Value, {[{object, Key, ?start_object}|Rest], Config}) -> | |||
{ | |||
[{object, [ | |||
?start_object, | |||
indent(Config), | |||
Key, | |||
?colon, | |||
space(Config), | |||
Value | |||
]}] ++ Rest, | |||
Config | |||
}; | |||
insert(Value, {[{object, Key, Object}|Rest], Config}) -> | |||
{ | |||
[{object, [ | |||
Object, | |||
?comma, | |||
indent_or_space(Config), | |||
Key, | |||
?colon, | |||
space(Config), | |||
Value | |||
]}] ++ Rest, | |||
Config | |||
}; | |||
insert(Value, {[{array, ?start_array}|Rest], Config}) -> | |||
{[{array, [?start_array, indent(Config), Value]}] ++ Rest, Config}; | |||
insert(Value, {[{array, Array}|Rest], Config}) -> | |||
{ | |||
[{array, [Array, | |||
?comma, | |||
indent_or_space(Config), | |||
Value | |||
]}] ++ Rest, | |||
Config | |||
}; | |||
insert(_, _) -> erlang:error(badarg). | |||
get_key({[{object, Key, _}|_], _}) -> Key; | |||
get_key(_) -> erlang:error(badarg). | |||
get_value({Value, _Config}) -> | |||
try unicode:characters_to_binary(Value) | |||
catch error:_ -> erlang:error(badarg) | |||
end; | |||
get_value(_) -> erlang:error(badarg). | |||
%% eunit tests | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
config_test_() -> | |||
[ | |||
{"empty config", ?_assertEqual(#config{}, parse_config([]))}, | |||
{"unspecified indent/space", ?_assertEqual( | |||
#config{space=1, indent=1}, | |||
parse_config([space, indent]) | |||
)}, | |||
{"specific indent", ?_assertEqual( | |||
#config{indent=4}, | |||
parse_config([{indent, 4}]) | |||
)}, | |||
{"specific space", ?_assertEqual( | |||
#config{space=2}, | |||
parse_config([{space, 2}]) | |||
)}, | |||
{"specific space and indent", ?_assertEqual( | |||
#config{space=2, indent=2}, | |||
parse_config([{space, 2}, {indent, 2}]) | |||
)}, | |||
{"invalid opt flag", ?_assertError(badarg, parse_config([error]))}, | |||
{"invalid opt tuple", ?_assertError(badarg, parse_config([{error, true}]))} | |||
]. | |||
space_test_() -> | |||
[ | |||
{"no space", ?_assertEqual(<<>>, space(#config{space=0}))}, | |||
{"one space", ?_assertEqual(<<" ">>, space(#config{space=1}))}, | |||
{"four spaces", ?_assertEqual(<<" ">>, space(#config{space=4}))} | |||
]. | |||
indent_test_() -> | |||
[ | |||
{"no indent", ?_assertEqual(<<>>, indent(#config{indent=0, depth=1}))}, | |||
{"indent 1 depth 1", ?_assertEqual( | |||
<<?newline/binary, <<" ">>/binary>>, | |||
indent(#config{indent=1, depth=1}) | |||
)}, | |||
{"indent 1 depth 2", ?_assertEqual( | |||
<<?newline/binary, <<" ">>/binary>>, | |||
indent(#config{indent=1, depth=2}) | |||
)}, | |||
{"indent 4 depth 1", ?_assertEqual( | |||
<<?newline/binary, <<" ">>/binary>>, | |||
indent(#config{indent=4, depth=1}) | |||
)}, | |||
{"indent 4 depth 2", ?_assertEqual( | |||
<<?newline/binary, <<" ">>/binary, <<" ">>/binary>>, | |||
indent(#config{indent=4, depth=2}) | |||
)} | |||
]. | |||
indent_or_space_test_() -> | |||
[ | |||
{"no indent so space", ?_assertEqual( | |||
<<" ">>, | |||
indent_or_space(#config{space=1, indent=0, depth=1}) | |||
)}, | |||
{"indent so no space", ?_assertEqual( | |||
<<?newline/binary, <<" ">>/binary>>, | |||
indent_or_space(#config{space=1, indent=1, depth=1}) | |||
)} | |||
]. | |||
encode_test_() -> | |||
[ | |||
{"0.0", ?_assert(encode(float, 0.0, #config{}) =:= ["0.0"])}, | |||
{"1.0", ?_assert(encode(float, 1.0, #config{}) =:= ["1.0"])}, | |||
{"-1.0", ?_assert(encode(float, -1.0, #config{}) =:= ["-1.0"])}, | |||
{"3.1234567890987654321", | |||
?_assert( | |||
encode(float, 3.1234567890987654321, #config{}) =:= ["3.1234567890987655"]) | |||
}, | |||
{"1.0e23", ?_assert(encode(float, 1.0e23, #config{}) =:= ["1.0e23"])}, | |||
{"0.3", ?_assert(encode(float, 3.0/10.0, #config{}) =:= ["0.3"])}, | |||
{"0.0001", ?_assert(encode(float, 0.0001, #config{}) =:= ["0.0001"])}, | |||
{"0.00001", ?_assert(encode(float, 0.00001, #config{}) =:= ["1.0e-5"])}, | |||
{"0.00000001", ?_assert(encode(float, 0.00000001, #config{}) =:= ["1.0e-8"])}, | |||
{"1.0e-323", ?_assert(encode(float, 1.0e-323, #config{}) =:= ["1.0e-323"])}, | |||
{"1.0e308", ?_assert(encode(float, 1.0e308, #config{}) =:= ["1.0e308"])}, | |||
{"min normalized float", | |||
?_assert( | |||
encode(float, math:pow(2, -1022), #config{}) =:= ["2.2250738585072014e-308"] | |||
) | |||
}, | |||
{"max normalized float", | |||
?_assert( | |||
encode(float, (2 - math:pow(2, -52)) * math:pow(2, 1023), #config{}) | |||
=:= ["1.7976931348623157e308"] | |||
) | |||
}, | |||
{"min denormalized float", | |||
?_assert(encode(float, math:pow(2, -1074), #config{}) =:= ["5.0e-324"]) | |||
}, | |||
{"max denormalized float", | |||
?_assert( | |||
encode(float, (1 - math:pow(2, -52)) * math:pow(2, -1022), #config{}) | |||
=:= ["2.225073858507201e-308"] | |||
) | |||
}, | |||
{"hello world", ?_assert(encode(string, <<"hello world">>, #config{}) | |||
=:= [<<"\"">>, <<"hello world">>, <<"\"">>] | |||
)}, | |||
{"key", ?_assert(encode(key, <<"key">>, #config{}) =:= [<<"\"">>, <<"key">>, <<"\"">>])}, | |||
{"1", ?_assert(encode(integer, 1, #config{}) =:= "1")}, | |||
{"-1", ?_assert(encode(integer, -1, #config{}) =:= "-1")}, | |||
{"true", ?_assert(encode(literal, true, #config{}) =:= "true")}, | |||
{"false", ?_assert(encode(literal, false, #config{}) =:= "false")}, | |||
{"null", ?_assert(encode(literal, null, #config{}) =:= "null")} | |||
]. | |||
format_test_() -> | |||
% {minified version, pretty version} | |||
Cases = [ | |||
{"empty object", <<"{}">>, <<"{}">>}, | |||
{"empty array", <<"[]">>, <<"[]">>}, | |||
{"single key object", <<"{\"k\":\"v\"}">>, <<"{\n \"k\": \"v\"\n}">>}, | |||
{"single member array", <<"[true]">>, <<"[\n true\n]">>}, | |||
{"multiple key object", | |||
<<"{\"k\":\"v\",\"x\":\"y\"}">>, | |||
<<"{\n \"k\": \"v\",\n \"x\": \"y\"\n}">> | |||
}, | |||
{"multiple member array", | |||
<<"[1.0,2.0,3.0]">>, | |||
<<"[\n 1.0,\n 2.0,\n 3.0\n]">> | |||
}, | |||
{"nested structure", | |||
<<"[[{},[],true],{\"k\":\"v\",\"x\":\"y\"}]">>, | |||
<<"[\n [\n {},\n [],\n true\n ],\n {\n \"k\": \"v\",\n \"x\": \"y\"\n }\n]">> | |||
} | |||
], | |||
[{Title, ?_assertEqual(Min, jsx:minify(Pretty))} || {Title, Min, Pretty} <- Cases] ++ | |||
[{Title, ?_assertEqual(Pretty, jsx:prettify(Min))} || {Title, Min, Pretty} <- Cases]. | |||
custom_newline_test_() -> | |||
[ | |||
{"single key object", ?_assert( | |||
jsx:format(<<"{\"k\":\"v\"}">>, [space, {indent, 2}, {newline, <<$\r>>}]) | |||
=:= <<"{\r \"k\": \"v\"\r}">>) | |||
} | |||
]. | |||
handle_event_test_() -> | |||
Data = jsx:test_cases() ++ jsx:special_test_cases(), | |||
[ | |||
{ | |||
Title, ?_assertEqual( | |||
JSON, | |||
lists:foldl(fun handle_event/2, init([]), Events ++ [end_json]) | |||
) | |||
} || {Title, JSON, _, Events} <- Data | |||
]. | |||
-endif. |
@ -1,459 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 Alisdair Sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_to_term). | |||
-export([to_term/2]). | |||
-export([init/1, handle_event/2]). | |||
-export([ | |||
start_term/1, | |||
start_object/1, | |||
start_array/1, | |||
finish/1, | |||
insert/2, | |||
get_key/1, | |||
get_value/1 | |||
]). | |||
-record(config, { | |||
labels = binary, | |||
return_maps = false | |||
}). | |||
-type config() :: list(). | |||
-export_type([config/0]). | |||
-ifndef(maps_support). | |||
-type json_value() :: list(json_value()) | |||
| list({binary() | atom(), json_value()}) | [{},...] | |||
| true | |||
| false | |||
| null | |||
| integer() | |||
| float() | |||
| binary(). | |||
-endif. | |||
-ifdef(maps_support). | |||
-type json_value() :: list(json_value()) | |||
| list({binary() | atom(), json_value()}) | [{},...] | |||
| map() | |||
| true | |||
| false | |||
| null | |||
| integer() | |||
| float() | |||
| binary(). | |||
-endif. | |||
-spec to_term(Source::binary(), Config::config()) -> json_value(). | |||
-ifdef(maps_always). | |||
to_term(Source, Config) when is_list(Config) -> | |||
(jsx:decoder(?MODULE, [return_maps] ++ Config, jsx_config:extract_config(Config)))(Source). | |||
-endif. | |||
-ifndef(maps_always). | |||
to_term(Source, Config) when is_list(Config) -> | |||
(jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source). | |||
-endif. | |||
parse_config(Config) -> parse_config(Config, #config{}). | |||
parse_config([{labels, Val}|Rest], Config) | |||
when Val == binary; Val == atom; Val == existing_atom; Val == attempt_atom -> | |||
parse_config(Rest, Config#config{labels = Val}); | |||
parse_config([labels|Rest], Config) -> | |||
parse_config(Rest, Config#config{labels = binary}); | |||
parse_config([{return_maps, Val}|Rest], Config) | |||
when Val == true; Val == false -> | |||
parse_config(Rest, Config#config{return_maps = Val}); | |||
parse_config([return_maps|Rest], Config) -> | |||
parse_config(Rest, Config#config{return_maps = true}); | |||
parse_config([{K, _}|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config) | |||
; false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([K|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config) | |||
; false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([], Config) -> | |||
Config. | |||
-type state() :: {list(), #config{}}. | |||
-spec init(Config::proplists:proplist()) -> state(). | |||
init(Config) -> start_term(Config). | |||
-spec handle_event(Event::any(), State::state()) -> state(). | |||
handle_event(end_json, State) -> get_value(State); | |||
handle_event(start_object, State) -> start_object(State); | |||
handle_event(end_object, State) -> finish(State); | |||
handle_event(start_array, State) -> start_array(State); | |||
handle_event(end_array, State) -> finish(State); | |||
handle_event({key, Key}, {_, Config} = State) -> insert(format_key(Key, Config), State); | |||
handle_event({_, Event}, State) -> insert(Event, State). | |||
format_key(Key, Config) -> | |||
case Config#config.labels of | |||
binary -> Key | |||
; atom -> binary_to_atom(Key, utf8) | |||
; existing_atom -> binary_to_existing_atom(Key, utf8) | |||
; attempt_atom -> | |||
try binary_to_existing_atom(Key, utf8) of | |||
Result -> Result | |||
catch | |||
error:badarg -> Key | |||
end | |||
end. | |||
%% internal state is a stack and a config object | |||
%% `{Stack, Config}` | |||
%% the stack is a list of in progress objects/arrays | |||
%% `[Current, Parent, Grandparent,...OriginalAncestor]` | |||
%% an object has the representation on the stack of | |||
%% `{object, [ | |||
%% {NthKey, NthValue}, | |||
%% {NMinus1Key, NthMinus1Value}, | |||
%% ..., | |||
%% {FirstKey, FirstValue} | |||
%% ]}` | |||
%% or if returning maps | |||
%% `{object, #{ | |||
%% FirstKey => FirstValue, | |||
%% SecondKey => SecondValue, | |||
%% ..., | |||
%% NthKey => NthValue | |||
%% }}` | |||
%% or if there's a key with a yet to be matched value | |||
%% `{object, Key, ...}` | |||
%% an array looks like | |||
%% `{array, [NthValue, NthMinus1Value,...FirstValue]}` | |||
start_term(Config) when is_list(Config) -> {[], parse_config(Config)}. | |||
-ifndef(maps_support). | |||
%% allocate a new object on top of the stack | |||
start_object({Stack, Config}) -> {[{object, []}] ++ Stack, Config}. | |||
%% allocate a new array on top of the stack | |||
start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}. | |||
%% finish an object or array and insert it into the parent object if it exists or | |||
%% return it if it is the root object | |||
finish({[{object, []}], Config}) -> {[{}], Config}; | |||
finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config}); | |||
finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config}; | |||
finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config}); | |||
finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config}; | |||
finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config}); | |||
finish(_) -> erlang:error(badarg). | |||
%% insert a value when there's no parent object or array | |||
insert(Value, {[], Config}) -> {Value, Config}; | |||
%% insert a key or value into an object or array, autodetects the 'right' thing | |||
insert(Key, {[{object, Pairs}|Rest], Config}) -> | |||
{[{object, Key, Pairs}] ++ Rest, Config}; | |||
insert(Value, {[{object, Key, Pairs}|Rest], Config}) -> | |||
{[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config}; | |||
insert(Value, {[{array, Values}|Rest], Config}) -> | |||
{[{array, [Value] ++ Values}] ++ Rest, Config}; | |||
insert(_, _) -> erlang:error(badarg). | |||
-endif. | |||
-ifdef(maps_support). | |||
%% allocate a new object on top of the stack | |||
start_object({Stack, Config=#config{return_maps=true}}) -> | |||
{[{object, #{}}] ++ Stack, Config}; | |||
start_object({Stack, Config}) -> | |||
{[{object, []}] ++ Stack, Config}. | |||
%% allocate a new array on top of the stack | |||
start_array({Stack, Config}) -> {[{array, []}] ++ Stack, Config}. | |||
%% finish an object or array and insert it into the parent object if it exists or | |||
%% return it if it is the root object | |||
finish({[{object, Map}], Config=#config{return_maps=true}}) -> {Map, Config}; | |||
finish({[{object, Map}|Rest], Config=#config{return_maps=true}}) -> insert(Map, {Rest, Config}); | |||
finish({[{object, []}], Config}) -> {[{}], Config}; | |||
finish({[{object, []}|Rest], Config}) -> insert([{}], {Rest, Config}); | |||
finish({[{object, Pairs}], Config}) -> {lists:reverse(Pairs), Config}; | |||
finish({[{object, Pairs}|Rest], Config}) -> insert(lists:reverse(Pairs), {Rest, Config}); | |||
finish({[{array, Values}], Config}) -> {lists:reverse(Values), Config}; | |||
finish({[{array, Values}|Rest], Config}) -> insert(lists:reverse(Values), {Rest, Config}); | |||
finish(_) -> erlang:error(badarg). | |||
%% insert a value when there's no parent object or array | |||
insert(Value, {[], Config}) -> {Value, Config}; | |||
%% insert a key or value into an object or array, autodetects the 'right' thing | |||
insert(Key, {[{object, Map}|Rest], Config=#config{return_maps=true}}) -> | |||
{[{object, Key, Map}] ++ Rest, Config}; | |||
insert(Key, {[{object, Pairs}|Rest], Config}) -> | |||
{[{object, Key, Pairs}] ++ Rest, Config}; | |||
insert(Value, {[{object, Key, Map}|Rest], Config=#config{return_maps=true}}) -> | |||
{[{object, maps:put(Key, Value, Map)}] ++ Rest, Config}; | |||
insert(Value, {[{object, Key, Pairs}|Rest], Config}) -> | |||
{[{object, [{Key, Value}] ++ Pairs}] ++ Rest, Config}; | |||
insert(Value, {[{array, Values}|Rest], Config}) -> | |||
{[{array, [Value] ++ Values}] ++ Rest, Config}; | |||
insert(_, _) -> erlang:error(badarg). | |||
-endif. | |||
get_key({[{object, Key, _}|_], _}) -> Key; | |||
get_key(_) -> erlang:error(badarg). | |||
get_value({Value, _Config}) -> Value; | |||
get_value(_) -> erlang:error(badarg). | |||
%% eunit tests | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
config_test_() -> | |||
[ | |||
{"empty config", ?_assertEqual(#config{}, parse_config([]))}, | |||
{"implicit binary labels", ?_assertEqual(#config{}, parse_config([labels]))}, | |||
{"binary labels", ?_assertEqual(#config{}, parse_config([{labels, binary}]))}, | |||
{"atom labels", ?_assertEqual(#config{labels=atom}, parse_config([{labels, atom}]))}, | |||
{"existing atom labels", ?_assertEqual( | |||
#config{labels=existing_atom}, | |||
parse_config([{labels, existing_atom}]) | |||
)}, | |||
{"return_maps true", ?_assertEqual( | |||
#config{return_maps=true}, | |||
parse_config([return_maps]) | |||
)}, | |||
{"invalid opt flag", ?_assertError(badarg, parse_config([error]))}, | |||
{"invalid opt tuple", ?_assertError(badarg, parse_config([{error, true}]))} | |||
]. | |||
format_key_test_() -> | |||
[ | |||
{"binary key", ?_assertEqual(<<"key">>, format_key(<<"key">>, #config{labels=binary}))}, | |||
{"atom key", ?_assertEqual(key, format_key(<<"key">>, #config{labels=atom}))}, | |||
{"existing atom key", ?_assertEqual( | |||
key, | |||
format_key(<<"key">>, #config{labels=existing_atom}) | |||
)}, | |||
{"nonexisting atom key", ?_assertError( | |||
badarg, | |||
format_key(<<"nonexistentatom">>, #config{labels=existing_atom}) | |||
)}, | |||
{"sloppy existing atom key", ?_assertEqual( | |||
key, | |||
format_key(<<"key">>, #config{labels=attempt_atom}) | |||
)}, | |||
{"nonexisting atom key", ?_assertEqual( | |||
<<"nonexistentatom">>, | |||
format_key(<<"nonexistentatom">>, #config{labels=attempt_atom}) | |||
)} | |||
]. | |||
rep_manipulation_test_() -> | |||
[ | |||
{"allocate a new context with option", ?_assertEqual( | |||
{[], #config{labels=atom}}, | |||
start_term([{labels, atom}]) | |||
)}, | |||
{"allocate a new object on an empty stack", ?_assertEqual( | |||
{[{object, []}], #config{}}, | |||
start_object({[], #config{}}) | |||
)}, | |||
{"allocate a new object on a stack", ?_assertEqual( | |||
{[{object, []}, {object, []}], #config{}}, | |||
start_object({[{object, []}], #config{}}) | |||
)}, | |||
{"allocate a new array on an empty stack", ?_assertEqual( | |||
{[{array, []}], #config{}}, | |||
start_array({[], #config{}}) | |||
)}, | |||
{"allocate a new array on a stack", ?_assertEqual( | |||
{[{array, []}, {object, []}], #config{}}, | |||
start_array({[{object, []}], #config{}}) | |||
)}, | |||
{"insert a key into an object", ?_assertEqual( | |||
{[{object, key, []}, junk], #config{}}, | |||
insert(key, {[{object, []}, junk], #config{}}) | |||
)}, | |||
{"get current key", ?_assertEqual( | |||
key, | |||
get_key({[{object, key, []}], #config{}}) | |||
)}, | |||
{"try to get non-key from object", ?_assertError( | |||
badarg, | |||
get_key({[{object, []}], #config{}}) | |||
)}, | |||
{"try to get key from array", ?_assertError( | |||
badarg, | |||
get_key({[{array, []}], #config{}}) | |||
)}, | |||
{"insert a value into an object", ?_assertEqual( | |||
{[{object, [{key, value}]}, junk], #config{}}, | |||
insert(value, {[{object, key, []}, junk], #config{}}) | |||
)}, | |||
{"insert a value into an array", ?_assertEqual( | |||
{[{array, [value]}, junk], #config{}}, | |||
insert(value, {[{array, []}, junk], #config{}}) | |||
)}, | |||
{"finish an object with no ancestor", ?_assertEqual( | |||
{[{a, b}, {x, y}], #config{}}, | |||
finish({[{object, [{x, y}, {a, b}]}], #config{}}) | |||
)}, | |||
{"finish an empty object", ?_assertEqual( | |||
{[{}], #config{}}, | |||
finish({[{object, []}], #config{}}) | |||
)}, | |||
{"finish an object with an ancestor", ?_assertEqual( | |||
{[{object, [{key, [{a, b}, {x, y}]}, {foo, bar}]}], #config{}}, | |||
finish({[{object, [{x, y}, {a, b}]}, {object, key, [{foo, bar}]}], #config{}}) | |||
)}, | |||
{"finish an array with no ancestor", ?_assertEqual( | |||
{[a, b, c], #config{}}, | |||
finish({[{array, [c, b, a]}], #config{}}) | |||
)}, | |||
{"finish an array with an ancestor", ?_assertEqual( | |||
{[{array, [[a, b, c], d, e, f]}], #config{}}, | |||
finish({[{array, [c, b, a]}, {array, [d, e, f]}], #config{}}) | |||
)} | |||
]. | |||
-ifdef(maps_support). | |||
rep_manipulation_with_maps_test_() -> | |||
[ | |||
{"allocate a new object on an empty stack", ?_assertEqual( | |||
{[{object, #{}}], #config{return_maps=true}}, | |||
start_object({[], #config{return_maps=true}}) | |||
)}, | |||
{"allocate a new object on a stack", ?_assertEqual( | |||
{[{object, #{}}, {object, #{}}], #config{return_maps=true}}, | |||
start_object({[{object, #{}}], #config{return_maps=true}}) | |||
)}, | |||
{"insert a key into an object", ?_assertEqual( | |||
{[{object, key, #{}}, junk], #config{return_maps=true}}, | |||
insert(key, {[{object, #{}}, junk], #config{return_maps=true}}) | |||
)}, | |||
{"get current key", ?_assertEqual( | |||
key, | |||
get_key({[{object, key, #{}}], #config{return_maps=true}}) | |||
)}, | |||
{"try to get non-key from object", ?_assertError( | |||
badarg, | |||
get_key({[{object, #{}}], #config{return_maps=true}}) | |||
)}, | |||
{"insert a value into an object", ?_assertEqual( | |||
{[{object, #{key => value}}, junk], #config{return_maps=true}}, | |||
insert(value, {[{object, key, #{}}, junk], #config{return_maps=true}}) | |||
)}, | |||
{"finish an object with no ancestor", ?_assertEqual( | |||
{#{a => b, x => y}, #config{return_maps=true}}, | |||
finish({[{object, #{x => y, a => b}}], #config{return_maps=true}}) | |||
)}, | |||
{"finish an empty object", ?_assertEqual( | |||
{#{}, #config{return_maps=true}}, | |||
finish({[{object, #{}}], #config{return_maps=true}}) | |||
)}, | |||
{"finish an object with an ancestor", ?_assertEqual( | |||
{ | |||
[{object, #{key => #{a => b, x => y}, foo => bar}}], | |||
#config{return_maps=true} | |||
}, | |||
finish({ | |||
[{object, #{x => y, a => b}}, {object, key, #{foo => bar}}], | |||
#config{return_maps=true} | |||
}) | |||
)} | |||
]. | |||
return_maps_test_() -> | |||
[ | |||
{"an empty map", ?_assertEqual( | |||
#{}, | |||
jsx:decode(<<"{}">>, [return_maps]) | |||
)}, | |||
{"an empty map", ?_assertEqual( | |||
[{}], | |||
jsx:decode(<<"{}">>, []) | |||
)}, | |||
{"an empty map", ?_assertEqual( | |||
[{}], | |||
jsx:decode(<<"{}">>, [{return_maps, false}]) | |||
)}, | |||
{"a small map", ?_assertEqual( | |||
#{<<"awesome">> => true, <<"library">> => <<"jsx">>}, | |||
jsx:decode(<<"{\"library\": \"jsx\", \"awesome\": true}">>, [return_maps]) | |||
)}, | |||
{"a recursive map", ?_assertEqual( | |||
#{<<"key">> => #{<<"key">> => true}}, | |||
jsx:decode(<<"{\"key\": {\"key\": true}}">>, [return_maps]) | |||
)}, | |||
{"a map inside a list", ?_assertEqual( | |||
[#{}], | |||
jsx:decode(<<"[{}]">>, [return_maps]) | |||
)} | |||
]. | |||
-endif. | |||
handle_event_test_() -> | |||
Data = jsx:test_cases(), | |||
[ | |||
{ | |||
Title, ?_assertEqual( | |||
Term, | |||
lists:foldl(fun handle_event/2, init([]), Events ++ [end_json]) | |||
) | |||
} || {Title, _, Term, Events} <- Data | |||
]. | |||
-endif. |
@ -1,119 +0,0 @@ | |||
%% The MIT License | |||
%% Copyright (c) 2010-2013 alisdair sullivan <alisdairsullivan@yahoo.ca> | |||
%% Permission is hereby granted, free of charge, to any person obtaining a copy | |||
%% of this software and associated documentation files (the "Software"), to deal | |||
%% in the Software without restriction, including without limitation the rights | |||
%% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |||
%% copies of the Software, and to permit persons to whom the Software is | |||
%% furnished to do so, subject to the following conditions: | |||
%% The above copyright notice and this permission notice shall be included in | |||
%% all copies or substantial portions of the Software. | |||
%% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |||
%% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |||
%% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |||
%% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |||
%% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |||
%% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN | |||
%% THE SOFTWARE. | |||
-module(jsx_verify). | |||
-export([is_json/2, is_term/2]). | |||
-export([init/1, handle_event/2]). | |||
-spec is_json(Source::binary(), Config::proplists:proplist()) -> true | false | {incomplete, jsx:decoder()}. | |||
is_json(Source, Config) when is_list(Config) -> | |||
try (jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source) | |||
catch error:badarg -> false | |||
end. | |||
-spec is_term(Source::any(), Config::proplists:proplist()) -> true | false | {incomplete, jsx:encoder()}. | |||
is_term(Source, Config) when is_list(Config) -> | |||
try (jsx:encoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source) | |||
catch error:badarg -> false | |||
end. | |||
parse_config(Config) -> parse_config(Config, []). | |||
%% ignore deprecated flags | |||
parse_config([no_repeated_keys|Rest], Config) -> | |||
parse_config(Rest, Config); | |||
parse_config([{repeated_keys, Val}|Rest], Config) when Val == true; Val == false -> | |||
parse_config(Rest, Config); | |||
parse_config([repeated_keys|Rest], Config) -> | |||
parse_config(Rest, Config); | |||
parse_config([{K, _}|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config); | |||
false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([K|Rest] = Options, Config) -> | |||
case lists:member(K, jsx_config:valid_flags()) of | |||
true -> parse_config(Rest, Config); | |||
false -> erlang:error(badarg, [Options, Config]) | |||
end; | |||
parse_config([], Config) -> | |||
Config. | |||
%% we don't actually need any state for this | |||
-type state() :: []. | |||
-spec init(Config::proplists:proplist()) -> state(). | |||
init(Config) -> parse_config(Config). | |||
-spec handle_event(Event::any(), State::state()) -> state(). | |||
handle_event(end_json, _) -> true; | |||
handle_event(_, State) -> State. | |||
%% eunit tests | |||
-ifdef(TEST). | |||
-include_lib("eunit/include/eunit.hrl"). | |||
config_test_() -> | |||
[ | |||
{"empty config", ?_assertEqual([], parse_config([]))}, | |||
{"no repeat keys", ?_assertEqual([], parse_config([no_repeated_keys]))}, | |||
{"bare repeated keys", ?_assertEqual([], parse_config([repeated_keys]))}, | |||
{"repeated keys true", ?_assertEqual( | |||
[], | |||
parse_config([{repeated_keys, true}]) | |||
)}, | |||
{"repeated keys false", ?_assertEqual( | |||
[], | |||
parse_config([{repeated_keys, false}]) | |||
)}, | |||
{"invalid opt flag", ?_assertError(badarg, parse_config([error]))}, | |||
{"invalid opt tuple", ?_assertError(badarg, parse_config([{error, true}]))} | |||
]. | |||
handle_event_test_() -> | |||
Data = jsx:test_cases() ++ jsx:special_test_cases(), | |||
[ | |||
{ | |||
Title, ?_assertEqual( | |||
true, | |||
lists:foldl(fun handle_event/2, [], Events ++ [end_json]) | |||
) | |||
} || {Title, _, _, Events} <- Data | |||
]. | |||
-endif. |