diff --git a/benchmark/poison.patch b/benchmark/poison.patch
index c54ccf0..52e98c3 100644
--- a/benchmark/poison.patch
+++ b/benchmark/poison.patch
@@ -63,9 +63,10 @@ index c07cf0d..8426e22 100644
{:jason, "~> 1.1", only: [:test, :bench]},
{:exjsx, "~> 4.0", only: :bench},
{:tiny, "~> 1.0", only: :bench},
- {:jsone, "~> 1.4", only: :bench},
+- {:jsone, "~> 1.4", only: :bench},
- {:jiffy, "~> 0.15", only: :bench},
- {:json, "~> 1.2", only: :bench}
++ {:jsone, path: "../..", only: :bench},
+ {:jiffy, "~> 1.0", only: :bench},
+ {:json, "~> 1.4", only: :bench}
]
diff --git a/doc/jsone.md b/doc/jsone.md
index 1fefd61..f787f9c 100644
--- a/doc/jsone.md
+++ b/doc/jsone.md
@@ -120,7 +120,7 @@ the last such instance.
-encode_option() = native_utf8 | native_forward_slash | canonical_form | {float_format, [float_format_option()]} | {datetime_format, datetime_encode_format()} | {object_key_type, string | scalar | value} | {space, non_neg_integer()} | {indent, non_neg_integer()} | {map_unknown_value, fun((term()) -> {ok, json_value()} | error)} | common_option()
+encode_option() = native_utf8 | native_forward_slash | canonical_form | {float_format, [float_format_option()]} | {datetime_format, datetime_encode_format()} | {object_key_type, string | scalar | value} | {space, non_neg_integer()} | {indent, non_neg_integer()} | {map_unknown_value, undefined | fun((term()) -> {ok, json_value()} | error)} | skip_undefined | common_option()
`native_utf8`:
@@ -156,8 +156,13 @@ encode_option() = native_utf8 | native_forward_slash | canonical_form | {float_f
- Inserts a newline and `N` spaces for each level of indentation
- default: `0`
+`skip_undefined`:
+- If specified, each entry having `undefined` value in a object isn't included in the result JSON
+
`{map_unknown_value, Fun}`:
-- If specified, unknown values encountered during an encoding process are converted to `json_value()` by applying `Fun`.
+- If `Fun` is a function, unknown values encountered during an encoding process are converted to `json_value()` by applying `Fun`.
+- If `Fun` is `undefined`, the encoding results in an error if there are unknown values.
+- default: `term_to_json_string/1`
@@ -175,7 +180,7 @@ float_format_option() = {scientific, Decimals::0..249} | {decimals, Decimals::0.
- The encoded string will contain at most `Decimals` number of digits past the decimal point.
- If `compact` is provided the trailing zeros at the end of the string are truncated.
-For more details, see [erlang:float_to_list/2](http://erlang.org/doc/man/erlang.html#float_to_list-2).
+For more details, see [erlang:float_to_list/2](http://erlang.org/doc/man/erlang.md#float_to_list-2).
```
> jsone:encode(1.23).
@@ -192,6 +197,46 @@ For more details, see [erlang:float_to_list/2](http://erlang.org/doc/man/erlang.
+### incomplete() ###
+
+
+
+incomplete() = {incomplete, incomplete_fun()}
+
+
+
+
+
+### incomplete_fun() ###
+
+
+
+incomplete_fun() = fun((binary() | end_stream) -> incomplete() | json_value())
+
+
+
+
+
+### incomplete_try() ###
+
+
+
+incomplete_try() = {incomplete, incomplete_try_fun()}
+
+
+
+
+
+### incomplete_try_fun() ###
+
+
+
+incomplete_try_fun() = fun((binary() | end_stream) -> incomplete_try() | {ok, json_value(), Remainings::binary()} | {error, {Reason::term(), [stack_item()]}})
+
+
+
+
+
### json_array() ###
@@ -236,7 +281,7 @@ json_object() = json_object_format_tupl
-json_object_format_map() = #{}
+json_object_format_map() = map()
@@ -392,7 +437,7 @@ utc_offset_seconds() = -86399..86399
## Function Index ##
-
+
@@ -433,6 +478,43 @@ Raises an error exception if input is not valid json
in call from jsone:decode/1 (src/jsone.erl, line 71)
```
+
+
+### decode_stream/1 ###
+
+
+decode_stream(Json::binary()) -> incomplete()
+
+
+
+Equivalent to [`decode_stream(Json, [])`](#decode_stream-2).
+
+
+
+### decode_stream/2 ###
+
+
+decode_stream(Json::binary(), Options::[decode_option()]) -> incomplete()
+
+
+
+Decodes an Erlang from JSON text in chunks.
+
+Instead of returning a result, `{incomplete, fun()}` is returned. The
+returned fun takes a single argument and it should called to continue the
+decoding. When all the input has been provided, the fun should be called with
+`end_stream` to signal the end of input and then the fun returns a result or
+an error, as `decode/2` would do.
+
+```
+ 1> {incomplete, F1} = jsone:decode(<<"[1,2,">>, []).
+ {incomplete, #Fun}
+ 2> {incomplete, F2} = F1(<<"3]>>).
+ {incomplete, #Fun}
+ 3> F2(end_stream).
+ [1,2,3]
+```
+
### encode/1 ###
@@ -467,6 +549,47 @@ Raises an error exception if input is not an instance of type `json_value()`
in call from jsone:encode/1 (src/jsone.erl, line 97)
```
+
+
+### ip_address_to_json_string/1 ###
+
+
+ip_address_to_json_string(X::inet:ip_address() | any()) -> {ok, json_string()} | error
+
+
+
+Convert an IP address into a text representation.
+
+This function can be specified as the value of the `map_unknown_value` encoding option.
+
+This function formats IPv6 addresses by following the recommendation defined in RFC 5952.
+Note that the trailing 32 bytes of special IPv6 addresses such as IPv4-Compatible (::X.X.X.X),
+IPv4-Mapped (::ffff:X.X.X.X), IPv4-Translated (::ffff:0:X.X.X.X) and IPv4/IPv6 translation
+(64:ff9b::X.X.X.X and 64:ff9b:1::X.X.X.X ~ 64:ff9b:1:ffff:ffff:ffff:X.X.X.X) are formatted
+using the IPv4 format.
+
+```
+ > EncodeOpt = [{map_unknown_value, fun jsone:ip_address_to_json_string/1}].
+ > jsone:encode(#{ip => {127, 0, 0, 1}}, EncodeOpt).
+ <<"{\"ip\":\"127.0.0.1\"}">>
+ > {ok, Addr} = inet:parse_address("2001:DB8:0000:0000:0001:0000:0000:0001").
+ > jsone:encode(Addr, EncodeOpt).
+ <<"\"2001:db8::1:0:0:1\"">>
+ > jsone:encode([foo, {0, 0, 0, 0, 0, 16#FFFF, 16#7F00, 16#0001}], EncodeOpt).
+ <<"[\"foo\",\"::ffff:127.0.0.1\"]">>
+```
+
+
+
+### term_to_json_string/1 ###
+
+
+term_to_json_string(X::term()) -> {ok, json_string()} | error
+
+
+
+Converts the given term `X` to its string representation (i.e., the result of `io_lib:format("~p", [X])`).
+
### try_decode/1 ###
@@ -498,6 +621,37 @@ Decodes an erlang term from json text (a utf8 encoded binary)
[{line,208}]}]}}
```
+
+
+### try_decode_stream/1 ###
+
+
+try_decode_stream(Json::binary()) -> incomplete_try()
+
+
+
+Equivalent to [`try_decode_stream(Json, [])`](#try_decode_stream-2).
+
+
+
+### try_decode_stream/2 ###
+
+
+try_decode_stream(Json::binary(), Options::[decode_option()]) -> incomplete_try()
+
+
+
+Decodes an Erlang from JSON text in chunks.
+
+```
+ 1> {incomplete, F1} = jsone:try_decode(<<"[1,2,">>, []).
+ {incomplete, #Fun}
+ 2> {incomplete, F2} = F1(<<"3] \"next value\"">>).
+ {incomplete, #Fun}
+ 2> F2(end_stream).
+ {ok,[1,2,3],<<" \"next value\"">>}
+```
+
### try_encode/1 ###
@@ -528,3 +682,4 @@ Encodes an erlang term into json text (a utf8 encoded binary)
[hoge,[{array_values,[2]}],<<"[1,">>],
[{line,86}]}]}}
```
+
diff --git a/src/jsone.erl b/src/jsone.erl
index 725fc60..4e1d146 100644
--- a/src/jsone.erl
+++ b/src/jsone.erl
@@ -31,6 +31,8 @@
%%--------------------------------------------------------------------------------
-export([decode/1, decode/2,
try_decode/1, try_decode/2,
+ decode_stream/1, decode_stream/2,
+ try_decode_stream/1, try_decode_stream/2,
encode/1, encode/2,
try_encode/1, try_encode/2,
term_to_json_string/1,
@@ -49,6 +51,8 @@
json_object_format_map/0,
json_scalar/0,
+ incomplete/0,
+ incomplete_try/0,
encode_option/0,
decode_option/0,
float_format_option/0,
@@ -130,6 +134,17 @@
-type json_scalar() :: json_boolean() | json_number() | json_string().
+-type incomplete_fun() :: fun((binary() | end_stream) ->
+ incomplete() | json_value()).
+-type incomplete() :: {incomplete, incomplete_fun()}.
+
+-type incomplete_try_fun() :: fun((binary() | end_stream) ->
+ incomplete_try() |
+ {ok, json_value(), Remainings :: binary()} |
+ {error, {Reason :: term(), [stack_item()]}}).
+
+-type incomplete_try() :: {incomplete, incomplete_try_fun()}.
+
-type float_format_option() :: {scientific, Decimals :: 0 .. 249} | {decimals, Decimals :: 0 .. 253} | compact.
%% `scientific':
%% - The float will be formatted using scientific notation with `Decimals' digits of precision.
@@ -287,7 +302,6 @@
%% - If the value is `first' then the first duplicate key/value is returned.
%% - If the value is `last' then the last duplicate key/value is returned.
%% - default: `first'
-%%
-type stack_item() :: {Module :: module(),
Function :: atom(),
@@ -346,7 +360,9 @@ decode(Json, Options) ->
%% @equiv try_decode(Json, [])
--spec try_decode(binary()) -> {ok, json_value(), Remainings :: binary()} | {error, {Reason :: term(), [stack_item()]}}.
+-spec try_decode(binary()) ->
+ {ok, json_value(), Remainings :: binary()} |
+ {error, {Reason :: term(), [stack_item()]}}.
try_decode(Json) ->
try_decode(Json, []).
@@ -363,11 +379,67 @@ try_decode(Json) ->
%% [{line,208}]}]}}
%% '''
-spec try_decode(binary(), [decode_option()]) ->
- {ok, json_value(), Remainings :: binary()} | {error, {Reason :: term(), [stack_item()]}}.
+ {ok, json_value(), Remainings :: binary()} |
+ {error, {Reason :: term(), [stack_item()]}}.
try_decode(Json, Options) ->
jsone_decode:decode(Json, Options).
+%% @equiv decode_stream(Json, [])
+-spec decode_stream(binary()) -> incomplete().
+decode_stream(Json) ->
+ decode_stream(Json, []).
+
+
+%% @doc Decodes an Erlang from JSON text in chunks.
+%%
+%% Instead of returning a result, `{incomplete, fun()}' is returned. The
+%% returned fun takes a single argument and it should called to continue the
+%% decoding. When all the input has been provided, the fun should be called with
+%% `end_stream' to signal the end of input and then the fun returns a result or
+%% an error, as `decode/2' would do.
+%%
+%% ```
+%% 1> {incomplete, F1} = jsone:decode(<<"[1,2,">>, []).
+%% {incomplete, #Fun}
+%% 2> {incomplete, F2} = F1(<<"3]>>).
+%% {incomplete, #Fun}
+%% 3> F2(end_stream).
+%% [1,2,3]
+%% '''
+
+-spec decode_stream(binary(), [decode_option()]) -> incomplete().
+decode_stream(Json, Options) ->
+ try
+ {incomplete, ContinueFun} = try_decode_stream(Json, Options),
+ {incomplete, replace_incomplete_fun(ContinueFun)}
+ catch
+ error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE->
+ erlang:raise(error, Reason, [StackItem | ?GET_STACKTRACE])
+ end.
+
+
+%% @equiv try_decode_stream(Json, [])
+-spec try_decode_stream(binary()) -> incomplete_try().
+try_decode_stream(Json) ->
+ try_decode_stream(Json, []).
+
+
+%% @doc Decodes an Erlang from JSON text in chunks.
+%%
+%% ```
+%% 1> {incomplete, F1} = jsone:try_decode(<<"[1,2,">>, []).
+%% {incomplete, #Fun}
+%% 2> {incomplete, F2} = F1(<<"3] \"next value\"">>).
+%% {incomplete, #Fun}
+%% 2> F2(end_stream).
+%% {ok,[1,2,3],<<" \"next value\"">>}
+%% '''
+-spec try_decode_stream(binary(), [decode_option()]) -> incomplete_try().
+try_decode_stream(Json, Options) ->
+ jsone_decode:decode_stream(Json, Options).
+
+
%% @equiv encode(JsonValue, [])
-spec encode(json_value()) -> binary().
encode(JsonValue) ->
@@ -471,3 +543,25 @@ check_decode_remainings(<<$\n, Bin/binary>>) ->
check_decode_remainings(Bin);
check_decode_remainings(<>) ->
erlang:error(badarg, [Bin]).
+
+%% Replace the fun in `{incomplete, Fun}' from `jsone_decode:decode_steram/2'
+%% with a fun that returns the same result as `jsone:decode/1,2'.
+replace_incomplete_fun(Fun) ->
+ fun (end_stream) ->
+ try
+ {ok, Value, Remainings} = Fun(end_stream),
+ check_decode_remainings(Remainings),
+ Value
+ catch
+ error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE->
+ erlang:raise(error, Reason, [StackItem | ?GET_STACKTRACE])
+ end;
+ (Input) ->
+ try
+ {incomplete, ContinueFun} = Fun(Input),
+ {incomplete, replace_incomplete_fun(ContinueFun)}
+ catch
+ error:{badmatch, {error, {Reason, [StackItem]}}} ?CAPTURE_STACKTRACE->
+ erlang:raise(error, Reason, [StackItem | ?GET_STACKTRACE])
+ end
+ end.
diff --git a/src/jsone_decode.erl b/src/jsone_decode.erl
index 33160e0..d44aeb1 100644
--- a/src/jsone_decode.erl
+++ b/src/jsone_decode.erl
@@ -34,7 +34,7 @@
%%--------------------------------------------------------------------------------
%% Exported API
%%--------------------------------------------------------------------------------
--export([decode/1, decode/2]).
+-export([decode/1, decode/2, decode_stream/2]).
%%--------------------------------------------------------------------------------
%% Macros & Records & Types
@@ -61,37 +61,57 @@
{object_value, jsone:json_string(), jsone:json_object_members()} |
{object_next, jsone:json_object_members()}.
--type decode_result() :: {ok, jsone:json_value(), Rest :: binary()} | {error, {Reason :: term(), [jsone:stack_item()]}}.
+-type incomplete() :: jsone:incomplete_try().
--record(decode_opt_v2, {
+-type decode_result() :: {ok, jsone:json_value(), Rest :: binary()} |
+ incomplete() |
+ {error, {Reason :: term(), [jsone:stack_item()]}}.
+
+-record(decode_opt_v3, {
object_format = ?DEFAULT_OBJECT_FORMAT :: tuple | proplist | map,
allow_ctrl_chars = false :: boolean(),
reject_invalid_utf8 = false :: boolean(),
keys = binary :: 'binary' | 'atom' | 'existing_atom' | 'attempt_atom',
undefined_as_null = false :: boolean(),
- duplicate_map_keys = first :: first | last
+ duplicate_map_keys = first :: first | last,
+ stream = false :: boolean()
}).
--define(OPT, #decode_opt_v2).
--type opt() :: #decode_opt_v2{}.
+-define(OPT, #decode_opt_v3).
+
+-type opt() :: #decode_opt_v3{}.
%%--------------------------------------------------------------------------------
%% Exported Functions
%%--------------------------------------------------------------------------------
--spec decode(binary()) -> decode_result().
+-spec decode(binary()) ->
+ {ok, jsone:json_value(), Rest :: binary()} |
+ {error, {Reason :: term(), [jsone:stack_item()]}}.
decode(Json) ->
decode(Json, []).
--spec decode(binary(), [jsone:decode_option()]) -> decode_result().
+-spec decode(binary(), [jsone:decode_option()]) ->
+ {ok, jsone:json_value(), Rest :: binary()} |
+ {error, {Reason :: term(), [jsone:stack_item()]}}.
decode(<>, Options) ->
Opt = parse_options(Options),
whitespace(Json, value, [], <<"">>, Opt).
+-spec decode_stream(binary(), [jsone:decode_option()]) -> incomplete().
+decode_stream(<>, Options) ->
+ Opt0 = parse_options(Options),
+ Opt = Opt0?OPT{stream = true},
+ Result = whitespace(Json, value, [], <<"">>, Opt),
+ incomplete_result(Result).
%%--------------------------------------------------------------------------------
%% Internal Functions
%%--------------------------------------------------------------------------------
+
+%% The functions which return decode_result() can only return incomplete() if
+%% the stream option is set.
+
-spec next(binary(), jsone:json_value(), [next()], binary(), opt()) -> decode_result().
next(<>, Value, [], _Buf, _Opt) ->
{ok, Value, Bin};
@@ -115,6 +135,8 @@ whitespace(<<$\r, Bin/binary>>, Next, Nexts, Buf, Opt) ->
whitespace(Bin, Next, Nexts, Buf, Opt);
whitespace(<<$\n, Bin/binary>>, Next, Nexts, Buf, Opt) ->
whitespace(Bin, Next, Nexts, Buf, Opt);
+whitespace(<<>>, Next, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun whitespace/5, [<<>>, Next, Nexts, Buf, Opt]);
whitespace(<>, Next, Nexts, Buf, Opt) ->
case Next of
value ->
@@ -149,6 +171,12 @@ value(<<${, Bin/binary>>, Nexts, Buf, Opt) ->
whitespace(Bin, object, Nexts, Buf, Opt);
value(<<$", Bin/binary>>, Nexts, Buf, Opt) ->
string(Bin, byte_size(Buf), Nexts, Buf, Opt);
+value(<> = Bin, Nexts, Buf, Opt) when
+ Opt?OPT.stream,
+ (C =:= $f andalso byte_size(Bin) < 5) orelse % incomplete "false"
+ (C =:= $t andalso byte_size(Bin) < 4) orelse % incomplete "true"
+ (C =:= $n andalso byte_size(Bin) < 4) -> % incomplete "null"
+ incomplete(fun value/4, [Bin, Nexts, Buf, Opt]);
value(<>, Nexts, Buf, Opt) ->
number(Bin, Nexts, Buf, Opt).
@@ -156,6 +184,8 @@ value(<>, Nexts, Buf, Opt) ->
-spec array(binary(), [next()], binary(), opt()) -> decode_result().
array(<<$], Bin/binary>>, Nexts, Buf, Opt) ->
next(Bin, [], Nexts, Buf, Opt);
+array(<<>>, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun array/4, [<<>>, Nexts, Buf, Opt]);
array(<>, Nexts, Buf, Opt) ->
value(Bin, [{array_next, []} | Nexts], Buf, Opt).
@@ -165,6 +195,8 @@ array_next(<<$], Bin/binary>>, Values, Nexts, Buf, Opt) ->
next(Bin, lists:reverse(Values), Nexts, Buf, Opt);
array_next(<<$,, Bin/binary>>, Values, Nexts, Buf, Opt) ->
whitespace(Bin, value, [{array_next, Values} | Nexts], Buf, Opt);
+array_next(<<>>, Values, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun array_next/5, [<<>>, Values, Nexts, Buf, Opt]);
array_next(Bin, Values, Nexts, Buf, Opt) ->
?ERROR(array_next, [Bin, Values, Nexts, Buf, Opt]).
@@ -172,6 +204,8 @@ array_next(Bin, Values, Nexts, Buf, Opt) ->
-spec object(binary(), [next()], binary(), opt()) -> decode_result().
object(<<$}, Bin/binary>>, Nexts, Buf, Opt) ->
next(Bin, make_object([], Opt), Nexts, Buf, Opt);
+object(<<>>, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun object/4, [<<>>, Nexts, Buf, Opt]);
object(<>, Nexts, Buf, Opt) ->
object_key(Bin, [], Nexts, Buf, Opt).
@@ -179,6 +213,8 @@ object(<>, Nexts, Buf, Opt) ->
-spec object_key(binary(), jsone:json_object_members(), [next()], binary(), opt()) -> decode_result().
object_key(<<$", Bin/binary>>, Members, Nexts, Buf, Opt) ->
string(Bin, byte_size(Buf), [{object_value, Members} | Nexts], Buf, Opt);
+object_key(<<>>, Members, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun object_key/5, [<<>>, Members, Nexts, Buf, Opt]);
object_key(<>, Members, Nexts, Buf, Opt) ->
?ERROR(object_key, [Bin, Members, Nexts, Buf, Opt]).
@@ -187,6 +223,8 @@ object_key(<>, Members, Nexts, Buf, Opt) ->
decode_result().
object_value(<<$:, Bin/binary>>, Key, Members, Nexts, Buf, Opt) ->
whitespace(Bin, value, [{object_next, object_key(Key, Opt), Members} | Nexts], Buf, Opt);
+object_value(<<>>, Key, Members, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun object_value/6, [<<>>, Key, Members, Nexts, Buf, Opt]);
object_value(Bin, Key, Members, Nexts, Buf, Opt) ->
?ERROR(object_value, [Bin, Key, Members, Nexts, Buf, Opt]).
@@ -213,6 +251,8 @@ object_next(<<$}, Bin/binary>>, Members, Nexts, Buf, Opt) ->
next(Bin, make_object(Members, Opt), Nexts, Buf, Opt);
object_next(<<$,, Bin/binary>>, Members, Nexts, Buf, Opt) ->
whitespace(Bin, {object_key, Members}, Nexts, Buf, Opt);
+object_next(<<>>, Members, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun object_next/5, [<<>>, Members, Nexts, Buf, Opt]);
object_next(Bin, Members, Nexts, Buf, Opt) ->
?ERROR(object_next, [Bin, Members, Nexts, Buf, Opt]).
@@ -232,6 +272,8 @@ string(<<$", Bin/binary>>, Base, Start, Nexts, Buf, Opt) ->
Buf2 = <>,
next(Bin, binary:part(Buf2, Start, byte_size(Buf2) - Start), Nexts, Buf2, Opt)
end;
+string(<<$\\>> = Bin, Base, Start, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete_string(Bin, Base, Start, Nexts, Buf, Opt);
string(<<$\\, B/binary>>, Base, Start, Nexts, Buf, Opt) ->
Prefix = binary:part(Base, 0, byte_size(Base) - byte_size(B) - 1),
case B of
@@ -265,12 +307,20 @@ string(<<_/utf8, Bin/binary>>, Base, Start, Nexts, Buf, Opt) when Opt?OPT.allow_
string(Bin, Base, Start, Nexts, Buf, Opt);
string(<>, Base, Start, Nexts, Buf, Opt) when 16#20 =< C ->
string(Bin, Base, Start, Nexts, Buf, Opt);
+string(<>, Base, Start, Nexts, Buf, Opt = ?OPT{stream = true})
+ when C >= 16#c0, C =< 16#df, byte_size(Bin) < 1; % 110xxxxx
+ C >= 16#e0, C =< 16#ef, byte_size(Bin) < 2; % 1110xxxx
+ C >= 16#f0, C =< 16#f7, byte_size(Bin) < 3 -> % 11110xxx
+ %% Partial UTF-8 character. The first byte determines the number of following bytes.
+ incomplete_string(<>, Base, Start, Nexts, Buf, Opt);
+string(<<>>, Base, Start, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete_string(<<>>, Base, Start, Nexts, Buf, Opt);
string(Bin, Base, Start, Nexts, Buf, Opt) ->
?ERROR(string, [Bin, Base, Start, Nexts, Buf, Opt]).
-spec unicode_string(binary(), non_neg_integer(), [next()], binary(), opt()) -> decode_result().
-unicode_string(<>, Start, Nexts, Buf, Opt) ->
+unicode_string(<> = Bin0, Start, Nexts, Buf, Opt) ->
try
binary_to_integer(N, 16)
of
@@ -290,6 +340,10 @@ unicode_string(<>, Start, Nexts, Buf, Opt) ->
error:badarg ->
?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt])
end;
+ <<$\\, $u, N2/binary>> when byte_size(N2) < 4, Opt?OPT.stream ->
+ incomplete(fun unicode_string/5, [Bin0, Start, Nexts, Buf, Opt]);
+ _ when Bin =:= <<$\\>> orelse Bin =:= <<>>, Opt?OPT.stream ->
+ incomplete(fun unicode_string/5, [Bin0, Start, Nexts, Buf, Opt]);
_ ->
?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt])
end;
@@ -304,6 +358,8 @@ unicode_string(<>, Start, Nexts, Buf, Opt) ->
error:badarg ->
?ERROR(unicode_string, [<>, Start, Nexts, Buf, Opt])
end;
+unicode_string(Bin, Start, Nexts, Buf, Opt) when byte_size(Bin) < 4, Opt?OPT.stream ->
+ incomplete(fun unicode_string/5, [Bin, Start, Nexts, Buf, Opt]);
unicode_string(Bin, Start, Nexts, Buf, Opt) ->
?ERROR(unicode_string, [Bin, Start, Nexts, Buf, Opt]).
@@ -316,10 +372,14 @@ number(<>, Nexts, Buf, Opt) ->
-spec number_integer_part(binary(), 1 | -1, [next()], binary(), opt()) -> decode_result().
+number_integer_part(<<$0>>, Sign, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_fraction_part/6, [<<>>, Sign, 0, Nexts, Buf, Opt]);
number_integer_part(<<$0, Bin/binary>>, Sign, Nexts, Buf, Opt) ->
number_fraction_part(Bin, Sign, 0, Nexts, Buf, Opt);
number_integer_part(<>, Sign, Nexts, Buf, Opt) when $1 =< C, C =< $9 ->
number_integer_part_rest(Bin, C - $0, Sign, Nexts, Buf, Opt);
+number_integer_part(<<>>, Sign, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_integer_part/5, [<<>>, Sign, Nexts, Buf, Opt]);
number_integer_part(Bin, Sign, Nexts, Buf, Opt) ->
?ERROR(number_integer_part, [Bin, Sign, Nexts, Buf, Opt]).
@@ -327,6 +387,8 @@ number_integer_part(Bin, Sign, Nexts, Buf, Opt) ->
-spec number_integer_part_rest(binary(), non_neg_integer(), 1 | -1, [next()], binary(), opt()) -> decode_result().
number_integer_part_rest(<>, N, Sign, Nexts, Buf, Opt) when $0 =< C, C =< $9 ->
number_integer_part_rest(Bin, N * 10 + C - $0, Sign, Nexts, Buf, Opt);
+number_integer_part_rest(<<>>, N, Sign, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_integer_part_rest/6, [<<>>, N, Sign, Nexts, Buf, Opt]);
number_integer_part_rest(<>, N, Sign, Nexts, Buf, Opt) ->
number_fraction_part(Bin, Sign, N, Nexts, Buf, Opt).
@@ -342,6 +404,8 @@ number_fraction_part(<>, Sign, Int, Nexts, Buf, Opt) ->
decode_result().
number_fraction_part_rest(<>, Sign, N, DecimalOffset, Nexts, Buf, Opt) when $0 =< C, C =< $9 ->
number_fraction_part_rest(Bin, Sign, N * 10 + C - $0, DecimalOffset + 1, Nexts, Buf, Opt);
+number_fraction_part_rest(<<>>, Sign, N, DecimalOffset, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_fraction_part_rest/7, [<<>>, Sign, N, DecimalOffset, Nexts, Buf, Opt]);
number_fraction_part_rest(<>, Sign, N, DecimalOffset, Nexts, Buf, Opt) when DecimalOffset > 0 ->
number_exponation_part(Bin, Sign * N, DecimalOffset, Nexts, Buf, Opt);
number_fraction_part_rest(Bin, Sign, N, DecimalOffset, Nexts, Buf, Opt) ->
@@ -357,10 +421,15 @@ number_exponation_part(<<$e, $-, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt
number_exponation_part(Bin, N, DecimalOffset, -1, 0, true, Nexts, Buf, Opt);
number_exponation_part(<<$E, $-, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) ->
number_exponation_part(Bin, N, DecimalOffset, -1, 0, true, Nexts, Buf, Opt);
+number_exponation_part(<>, N, DecimalOffset, Nexts, Buf, Opt) when E == $e orelse E == $E,
+ Opt?OPT.stream ->
+ incomplete(fun number_exponation_part/6, [<>, N, DecimalOffset, Nexts, Buf, Opt]);
number_exponation_part(<<$e, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) ->
number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt);
number_exponation_part(<<$E, Bin/binary>>, N, DecimalOffset, Nexts, Buf, Opt) ->
number_exponation_part(Bin, N, DecimalOffset, 1, 0, true, Nexts, Buf, Opt);
+number_exponation_part(<<>>, N, DecimalOffset, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_exponation_part/6, [<<>>, N, DecimalOffset, Nexts, Buf, Opt]);
number_exponation_part(<>, N, DecimalOffset, Nexts, Buf, Opt) ->
case DecimalOffset of
0 ->
@@ -381,6 +450,8 @@ number_exponation_part(<>, N, DecimalOffset, Nexts, Buf, Opt) ->
opt()) -> decode_result().
number_exponation_part(<>, N, DecimalOffset, ExpSign, Exp, _, Nexts, Buf, Opt) when $0 =< C, C =< $9 ->
number_exponation_part(Bin, N, DecimalOffset, ExpSign, Exp * 10 + C - $0, false, Nexts, Buf, Opt);
+number_exponation_part(<<>>, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt) when Opt?OPT.stream ->
+ incomplete(fun number_exponation_part/9, [<<>>, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt]);
number_exponation_part(<>, N, DecimalOffset, ExpSign, Exp, false, Nexts, Buf, Opt) ->
Pos = ExpSign * Exp - DecimalOffset,
try
@@ -400,6 +471,52 @@ number_exponation_part(<>, N, DecimalOffset, ExpSign, Exp, false, Ne
number_exponation_part(Bin, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt) ->
?ERROR(number_exponation_part, [Bin, N, DecimalOffset, ExpSign, Exp, IsFirst, Nexts, Buf, Opt]).
+incomplete_result({ok, Value, Rest} = Result) ->
+ %% The user needs to call Fun(end_stream) to get the ok tuple.
+ {incomplete,
+ fun (end_stream) ->
+ Result;
+ (<>) ->
+ incomplete_result({ok, Value, <>})
+ end};
+incomplete_result({error, _} = Error) ->
+ {incomplete,
+ fun (end_stream) ->
+ Error;
+ (<<_More/binary>>) ->
+ incomplete_result(Error)
+ end};
+incomplete_result({incomplete, _} = Incomplete) ->
+ Incomplete.
+
+-spec incomplete(fun(), list()) -> jsone:incomplete_try().
+incomplete(Fun, [Remains | Args]) ->
+ {incomplete,
+ fun F(end_stream) ->
+ %% The last arg is always Opts
+ [Opt | RevArgs] = lists:reverse(Args),
+ Args1 = lists:reverse([Opt?OPT{stream = false} | RevArgs]),
+ apply(Fun, [Remains | Args1]);
+ F(<<>>) ->
+ {incomplete, F};
+ F(More) when is_binary(More) ->
+ Bin = <>,
+ incomplete_result(apply(Fun, [Bin | Args]))
+ end}.
+
+-spec incomplete_string(binary(), binary(), non_neg_integer(), [next()], binary(), opt()) ->
+ jsone:incomplete().
+incomplete_string(Remains, Base, Start, Nexts, Buf, Opt) ->
+ {incomplete,
+ fun F(end_stream) ->
+ string(Remains, Base, Start, Nexts, Buf, Opt?OPT{stream = false});
+ F(<<>>) ->
+ {incomplete, F};
+ F(More) when is_binary(More) ->
+ incomplete_result(string(<>,
+ <>,
+ Start, Nexts, Buf, Opt))
+ end}.
-spec make_object(jsone:json_object_members(), opt()) -> jsone:json_object().
make_object(Members, ?OPT{object_format = tuple}) ->
@@ -434,5 +551,7 @@ parse_option([undefined_as_null | T], Opt) ->
parse_option(T, Opt?OPT{undefined_as_null = true});
parse_option([{duplicate_map_keys, V} | T], Opt) when V =:= first; V =:= last ->
parse_option(T, Opt?OPT{duplicate_map_keys = V});
+parse_option([stream | T], Opt) ->
+ parse_option(T, Opt?OPT{stream = true});
parse_option(List, Opt) ->
error(badarg, [List, Opt]).
diff --git a/test/jsone_decode_tests.erl b/test/jsone_decode_tests.erl
index 81c153e..087494f 100644
--- a/test/jsone_decode_tests.erl
+++ b/test/jsone_decode_tests.erl
@@ -6,7 +6,7 @@
-ifdef('NO_MAP_TYPE').
-define(MAP_OBJECT_TYPE, tuple).
--define(OBJ0, {[]}).
+-define(OBJ0, {[]}).
-define(OBJ1(K, V), {[{K, V}]}).
-define(OBJ2(K1, V1, K2, V2), {[{K1, V1}, {K2, V2}]}).
-define(OBJ2_DUP_KEY(K1, V1, K2, V2), ?OBJ2(K1, V1, K2, V2)).
@@ -19,36 +19,35 @@
-define(OBJ2_DUP_KEY_LAST(_K1, _V1, K2, V2), #{K2 => V2}). % the last value is used
-endif.
-
decode_test_() ->
[
%% Symbols
- {"false", fun() -> ?assertEqual({ok, false, <<"">>}, jsone_decode:decode(<<"false">>)) end},
- {"true", fun() -> ?assertEqual({ok, true, <<"">>}, jsone_decode:decode(<<"true">>)) end},
- {"null", fun() -> ?assertEqual({ok, null, <<"">>}, jsone_decode:decode(<<"null">>)) end},
+ {"false", fun () -> ?assertEqual({ok, false, <<"">>}, jsone_decode:decode(<<"false">>)) end},
+ {"true", fun () -> ?assertEqual({ok, true, <<"">>}, jsone_decode:decode(<<"true">>)) end},
+ {"null", fun () -> ?assertEqual({ok, null, <<"">>}, jsone_decode:decode(<<"null">>)) end},
%% Numbers: Integer
- {"positive integer", fun() -> ?assertEqual({ok, 1, <<"">>}, jsone_decode:decode(<<"1">>)) end},
- {"zero", fun() -> ?assertEqual({ok, 0, <<"">>}, jsone_decode:decode(<<"0">>)) end},
- {"negative integer", fun() -> ?assertEqual({ok, -1, <<"">>}, jsone_decode:decode(<<"-1">>)) end},
+ {"positive integer", fun () -> ?assertEqual({ok, 1, <<"">>}, jsone_decode:decode(<<"1">>)) end},
+ {"zero", fun () -> ?assertEqual({ok, 0, <<"">>}, jsone_decode:decode(<<"0">>)) end},
+ {"negative integer", fun () -> ?assertEqual({ok, -1, <<"">>}, jsone_decode:decode(<<"-1">>)) end},
{"large integer (no limit on size)",
- fun() ->
- ?assertEqual({ok, 111111111111111111111111111111111111111111111111111111111111111111111111111111, <<"">>},
- jsone_decode:decode(<<"111111111111111111111111111111111111111111111111111111111111111111111111111111">>))
+ fun () ->
+ ?assertEqual({ok, 111111111111111111111111111111111111111111111111111111111111111111111111111111, <<"">>},
+ jsone_decode:decode(<<"111111111111111111111111111111111111111111111111111111111111111111111111111111">>))
end},
{"integer with leading zero (interpreted as zero and remaining binary)",
- fun() ->
+ fun () ->
?assertEqual({ok, 0, <<"0">>}, jsone_decode:decode(<<"00">>)),
?assertEqual({ok, 0, <<"1">>}, jsone_decode:decode(<<"01">>)),
?assertEqual({ok, 0, <<"1">>}, jsone_decode:decode(<<"-01">>))
end},
{"integer can't begin with an explicit plus sign",
- fun() -> ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"+1">>)) end},
+ fun () -> ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"+1">>)) end},
%% Numbers: Floats
- {"float: decimal notation", fun() -> ?assertEqual({ok, 1.23, <<"">>}, jsone_decode:decode(<<"1.23">>)) end},
+ {"float: decimal notation", fun () -> ?assertEqual({ok, 1.23, <<"">>}, jsone_decode:decode(<<"1.23">>)) end},
{"float: exponential notation",
- fun() ->
+ fun () ->
?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345e-3">>)), % lower case 'e'
?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345E-3">>)), % upper case 'E'
?assertEqual({ok, 12.345, <<"">>}, jsone_decode:decode(<<"12345.0e-3">>)),
@@ -59,7 +58,7 @@ decode_test_() ->
?assertEqual({ok, 123.0, <<"">>}, jsone_decode:decode(<<"1.23000000000000000000e+02">>))
end},
{"float: invalid format",
- fun() ->
+ fun () ->
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<".123">>)), % omitted integer part
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.">>)), % omitted fraction part: EOS
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"0.e+3">>)), % omitted fraction part: with exponent part
@@ -74,15 +73,15 @@ decode_test_() ->
end},
%% Strings
- {"simple string", fun() -> ?assertEqual({ok, <<"abc">>, <<"">>}, jsone_decode:decode(<<"\"abc\"">>)) end},
+ {"simple string", fun () -> ?assertEqual({ok, <<"abc">>, <<"">>}, jsone_decode:decode(<<"\"abc\"">>)) end},
{"string: escaped characters",
- fun() ->
+ fun () ->
Input = list_to_binary([$", [[$\\, C] || C <- [$", $/, $\\, $b, $f, $n, $r, $t]], $"]),
Expected = <<"\"\/\\\b\f\n\r\t">>,
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
end},
{"string: escaped Unicode characters",
- fun() ->
+ fun () ->
%% japanese
Input1 = <<"\"\\u3042\\u3044\\u3046\\u3048\\u304A\"">>,
Expected1 = <<"あいうえお">>, % assumed that the encoding of this file is UTF-8
@@ -104,28 +103,28 @@ decode_test_() ->
?assertEqual({ok, Expected4, <<"">>}, jsone_decode:decode(Input4))
end},
{"string: surrogate pairs",
- fun() ->
+ fun () ->
Input = <<"\"\\ud848\\udc49\\ud848\\udc9a\\ud848\\udcfc\"">>,
Expected = <<"𢁉𢂚𢃼">>,
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
end},
{"string: control characters",
- fun() ->
+ fun () ->
Ctrls = lists:seq(0, 16#1f),
- lists:foreach(fun(C) ->
- %% Control characters are unacceptable
- ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<$", C, $">>))
+ lists:foreach(fun (C) ->
+ %% Control characters are unacceptable
+ ?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<$", C, $">>))
end,
Ctrls),
- lists:foreach(fun(C) ->
- %% `allow_ctrl_chars' option allows strings which contain unescaped control characters
- ?assertEqual({ok, <>, <<"">>},
- jsone_decode:decode(<<$", C, $">>, [{allow_ctrl_chars, true}]))
+ lists:foreach(fun (C) ->
+ %% `allow_ctrl_chars' option allows strings which contain unescaped control characters
+ ?assertEqual({ok, <>, <<"">>},
+ jsone_decode:decode(<<$", C, $">>, [{allow_ctrl_chars, true}]))
end,
Ctrls)
end},
{"string: invalid escape characters",
- fun() ->
+ fun () ->
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\z\"">>)), % '\z' is undefined
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\uab\"">>)), % too few hex characters
?assertMatch({error, {badarg, _}}, jsone_decode:decode(<<"\"\\ud848\"">>)), % high(first) surrogate only
@@ -138,117 +137,117 @@ decode_test_() ->
%% Arrays
{"simple array",
- fun() ->
+ fun () ->
Input = <<"[1,2,\"abc\",null]">>,
Expected = [1, 2, <<"abc">>, null],
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
end},
{"array: contains whitespaces",
- fun() ->
+ fun () ->
Input = <<"[ 1,\t2, \n \"abc\",\r null]">>,
Expected = [1, 2, <<"abc">>, null],
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
end},
{"empty array",
- fun() ->
+ fun () ->
?assertEqual({ok, [], <<"">>}, jsone_decode:decode(<<"[]">>)),
?assertEqual({ok, [], <<"">>}, jsone_decode:decode(<<"[ \t\r\n]">>))
end},
{"array: trailing comma is disallowed",
- fun() ->
+ fun () ->
Input = <<"[1, 2, \"abc\", null, ]">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"array: missing comma",
- fun() ->
+ fun () ->
Input = <<"[1 2, \"abc\", null]">>, % a missing comma between '1' and '2'
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"array: missing closing bracket",
- fun() ->
+ fun () ->
Input = <<"[1, 2, \"abc\", null">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
%% Objects
{"simple object",
- fun() ->
+ fun () ->
Input = <<"{\"1\":2,\"key\":\"value\"}">>,
Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>),
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input)), % `map' is the default format
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]))
end},
{"simple object: tuple or proplist",
- fun() ->
+ fun () ->
Input = <<"{\"1\":2,\"key\":\"value\"}">>,
Expected = {[{<<"1">>, 2}, {<<"key">>, <<"value">>}]},
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, tuple}])),
?assertEqual({ok, element(1, Expected), <<"">>}, jsone_decode:decode(Input, [{object_format, proplist}]))
end},
{"object: contains whitespaces",
- fun() ->
+ fun () ->
Input = <<"{ \"1\" :\t 2,\n\r\"key\" : \n \"value\"}">>,
Expected = ?OBJ2(<<"1">>, 2, <<"key">>, <<"value">>),
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input))
end},
{"empty object",
- fun() ->
+ fun () ->
?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>)),
?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{ \t\r\n}">>)),
?assertEqual({ok, {[]}, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, tuple}])),
?assertEqual({ok, [{}], <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, proplist}]))
end},
{"empty object: map",
- fun() ->
- ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, ?MAP_OBJECT_TYPE}]))
+ fun () ->
+ ?assertEqual({ok, ?OBJ0, <<"">>}, jsone_decode:decode(<<"{}">>, [{object_format, ?MAP_OBJECT_TYPE}]))
end},
{"duplicated members: map",
- fun() ->
+ fun () ->
Input = <<"{\"1\":\"first\",\"1\":\"second\"}">>,
Expected = ?OBJ2_DUP_KEY(<<"1">>, <<"first">>, <<"1">>, <<"second">>),
?assertEqual({ok, Expected, <<"">>}, jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}]))
end},
{"duplicated members last: map",
- fun() ->
+ fun () ->
Input = <<"{\"1\":\"first\",\"1\":\"second\"}">>,
Expected = ?OBJ2_DUP_KEY_LAST(<<"1">>, <<"first">>, <<"1">>, <<"second">>),
?assertEqual({ok, Expected, <<"">>},
jsone_decode:decode(Input, [{object_format, ?MAP_OBJECT_TYPE}, {duplicate_map_keys, last}]))
end},
{"object: trailing comma is disallowed",
- fun() ->
+ fun () ->
Input = <<"{\"1\":2, \"key\":\"value\", }">>,
io:format("~p\n", [catch jsone_decode:decode(Input)]),
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input, [{object_format, tuple}]))
end},
{"object: missing comma",
- fun() ->
+ fun () ->
Input = <<"{\"1\":2 \"key\":\"value\"}">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"object: missing field key",
- fun() ->
+ fun () ->
Input = <<"{:2, \"key\":\"value\"}">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"object: non string key",
- fun() ->
+ fun () ->
Input = <<"{1:2, \"key\":\"value\"}">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"object: missing field value",
- fun() ->
+ fun () ->
Input = <<"{\"1\", \"key\":\"value\"}">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"object: missing closing brace",
- fun() ->
+ fun () ->
Input = <<"{\"1\":2 \"key\":\"value\"">>,
?assertMatch({error, {badarg, _}}, jsone_decode:decode(Input))
end},
{"atom keys",
- fun() ->
- KeyOpt = fun(Keys) -> [{keys, Keys}, {object_format, proplist}] end,
+ fun () ->
+ KeyOpt = fun (Keys) -> [{keys, Keys}, {object_format, proplist}] end,
Input = <<"{\"foo\":\"ok\"}">>,
?assertEqual([{<<"foo">>, <<"ok">>}], jsone:decode(Input, KeyOpt(binary))),
?assertEqual([{foo, <<"ok">>}], jsone:decode(Input, KeyOpt(atom))),
@@ -262,27 +261,134 @@ decode_test_() ->
?assertEqual(Value, atom_to_binary(Atom, latin1))
end},
{"garbage remainings chars",
- fun() ->
+ fun () ->
?assertError(badarg, jsone:decode(<<"1@">>)),
?assertEqual(1, jsone:decode(<<"1 \n\t\r ">>)) % Whitespaces are OK
end},
%% Others
{"compound data",
- fun() ->
+ fun () ->
Input = <<" [true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null] ">>,
Expected = [true, ?OBJ2(<<"1">>, 2, <<"array">>, [[[[1]]], ?OBJ1(<<"ab">>, <<"cd">>), false]), null],
?assertEqual({ok, Expected, <<" ">>}, jsone_decode:decode(Input))
end},
{"undefined_as_null option",
- fun() ->
+ fun () ->
?assertEqual({ok, undefined, <<>>}, jsone_decode:decode(<<"null">>, [undefined_as_null])), % OK
?assertEqual({ok, null, <<>>}, jsone_decode:decode(<<"null">>, [])) % OK
end},
{"Invalid UTF-8 characters",
- fun() ->
- Input = <<123, 34, 105, 100, 34, 58, 34, 190, 72, 94, 90, 253, 121, 94, 71, 73, 68, 91, 122, 211, 253, 32,
- 94, 86, 67, 163, 253, 230, 34, 125>>,
+ fun () ->
+ Input =
+ <<123, 34, 105, 100, 34, 58, 34, 190, 72, 94, 90, 253, 121, 94, 71, 73, 68, 91, 122, 211, 253, 32, 94,
+ 86, 67, 163, 253, 230, 34, 125>>,
?assertMatch({ok, _, _}, jsone:try_decode(Input)),
?assertMatch({error, {badarg, _}}, jsone:try_decode(Input, [reject_invalid_utf8]))
end}].
+
+stream_decode_full_test_() ->
+ [{"stream one chunk",
+ fun () ->
+ {incomplete, Fun} = jsone_decode:decode_stream(<<"null">>, []),
+ ?assertEqual({ok, null, <<>>}, Fun(end_stream))
+ end},
+ {"stream two chunks",
+ fun () ->
+ {incomplete, Fun1} = jsone_decode:decode_stream(<<"nul">>, []),
+ {incomplete, Fun2} = Fun1(<<"l">>),
+ ?assertEqual({ok, null, <<>>}, Fun2(end_stream))
+ end}].
+
+stream_decode_byte_by_byte_test_() ->
+ %% A complex JSON document without errors.
+ %% Escaped strings include japanese, ascii, surrugate pairs and mixed.
+ Jsons =
+ [<<"true">>,
+ <<"false">>,
+ <<"null">>, % symbols
+ <<"1">>,
+ <<"0">>,
+ <<"-1">>,
+ <<"-0">>, % integers
+ %% large integer
+ <<"111111111111111111111111111111111111111111111111111111111111111111111111111111">>,
+ %% floats
+ <<"1.23">>,
+ <<"12345e-3">>,
+ <<"12345E-3">>,
+ <<"12345.0e-3">>,
+ <<"0.12345E2">>,
+ <<"0.12345e+2">>,
+ <<"0.12345E+2">>,
+ <<"-0.012345e3">>,
+ <<"1.23000000000000000000e+02">>,
+ %% strings
+ <<"\"abc\"">>,
+ <<"\"\\\"\\/\\\\\\b\\f\\n\\r\\t\"">>,
+ <<"\"\\u3042\\u3044\\u3046\\u3048\\u304A\"">>,
+ <<"\"\\u0061\\u0062\\u0063\"">>,
+ <<"\"\\u06DD\\u06DE\\u10AE\\u10AF\"">>,
+ <<"\"a\\u30421\\u3044bb\\u304622\\u3048ccc\\u304A333\"">>,
+ <<"\"\\ud848\\udc49\\ud848\\udc9a\\ud848\\udcfc\"">>,
+ <<"[1,2,\"abc\",null]">>, % simple array
+ <<"[ 1,\t2, \n \"abc\",\r null]">>, % array: contains whitespaces
+ <<"[]">>,
+ <<"[ \t\r\n]">>, % empty array
+ <<"{\"1\":2,\"key\":\"value\"}">>, % simple object
+ %% object: contains whitespaces
+ <<"{ \"1\" :\t 2,\n\r\"key\" : \n \"value\"}">>,
+ <<"{}">>,
+ <<"{ \t\r\n}">>, % empty object
+ <<"{\"1\":\"first\",\"1\":\"second\"}">>, % duplicated members
+ %% compound data
+ <<"[true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null]">>],
+ %% Test cases
+ [{"'" ++ binary_to_list(Json) ++ "'", fun () -> byte_by_byte(Json) end} || Json <- Jsons].
+
+%% Parse one byte at a time, interleaved with parsing an empty binary
+%% between every chunk.
+byte_by_byte(Bin) ->
+ Result =
+ lists:foldl(fun (Byte, {incomplete, Continue1}) ->
+ {incomplete, Continue2} = Continue1(<>),
+ Continue2(<<>>)
+ end,
+ jsone_decode:decode_stream(<<>>, []),
+ binary_to_list(Bin)),
+ {incomplete, Fun} = Result,
+ {ok, Expected, <<>>} = jsone_decode:decode(Bin),
+ ?assertEqual({ok, Expected, <<>>}, Fun(end_stream)).
+
+stream_decode_byte_by_byte_using_jsone_test_() ->
+ Cases =
+ [{"trailing whitespace", <<"[true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null] ">>},
+ {"trailing garbage", <<"[true, {\"1\" : 2, \"array\":[[[[1]]], {\"ab\":\"cd\"}, false]}, null] bla bla">>}],
+ [{Name, fun () -> byte_by_byte_using_jsone(Bin) end} || {Name, Bin} <- Cases].
+
+%% Using jsone:decode_stream/2 as opposed to jsone_decode:decode_stream/2.
+byte_by_byte_using_jsone(Bin) ->
+ %% Parse one byte at a time, interleaved with parsing an empty binary
+ %% between every chunk.
+ {incomplete, Fun} =
+ lists:foldl(fun (Byte, {incomplete, Continue1}) ->
+ {incomplete, Continue2} = Continue1(<>),
+ Continue2(<<>>)
+ end,
+ jsone:decode_stream(<<>>, []),
+ binary_to_list(Bin)),
+ Expected =
+ try
+ jsone:decode(Bin)
+ catch
+ C1:E1 ->
+ {C1, E1}
+ end,
+ Actual =
+ try
+ Fun(end_stream)
+ catch
+ C2:E2 ->
+ {C2, E2}
+ end,
+ ?assertEqual(Expected, Actual).