@ -1,3 +1,6 @@ | |||||
{erl_opts, [{i, "include"}]}. | {erl_opts, [{i, "include"}]}. | ||||
{edoc_opts, [{preprocess, true}]}. | {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. |