Current section

Files

Jump to
jsxn src jsxn.erl
Raw

src/jsxn.erl

%% 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(jsxn).
-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([encoder/3, decoder/3, parser/3]).
-export([resume/3]).
-export([init/1, handle_event/2]).
-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]).
-type json_term() :: [{binary() | atom(), json_term()}] | [{}]
| [json_term()] | []
| #{}
| true | false | null
| integer() | float()
| binary() | atom().
-type json_text() :: binary().
-type config() :: jsx_config:config().
-spec encode(Source::json_term()) -> json_text() | {incomplete, encoder()}.
-spec encode(Source::json_term(), Config::jsx_to_json:config()) -> json_text() | {incomplete, encoder()}.
encode(Source) -> jsx:encode(Source, []).
encode(Source, Config) -> jsx:encode(Source, Config).
-spec decode(Source::json_text()) -> json_term() | {incomplete, decoder()}.
-spec decode(Source::json_text(), Config::jsx_to_term:config()) -> json_term() | {incomplete, decoder()}.
decode(Source) -> decode(Source, []).
decode(Source, Config) -> (jsx:decoder(?MODULE, Config, jsx_config:extract_config(Config)))(Source).
-spec format(Source::json_text()) -> json_text() | {incomplete, decoder()}.
-spec format(Source::json_text(), Config::jsx_to_json:config()) -> json_text() | {incomplete, decoder()}.
format(Source) -> jsx:format(Source, []).
format(Source, Config) -> jsx:format(Source, Config).
-spec minify(Source::json_text()) -> json_text() | {incomplete, decoder()}.
minify(Source) -> jsx:format(Source, []).
-spec prettify(Source::json_text()) -> json_text() | {incomplete, decoder()}.
prettify(Source) -> jsx:format(Source, [space, {indent, 2}]).
-spec is_json(Source::any()) -> true | false.
-spec is_json(Source::any(), Config::jsx_verify:config()) -> true | false.
is_json(Source) -> jsx:is_json(Source, []).
is_json(Source, Config) -> jsx:is_json(Source, Config).
-spec is_term(Source::any()) -> true | false.
-spec is_term(Source::any(), Config::jsx_verify:config()) -> true | false.
is_term(Source) -> jsx:is_term(Source, []).
is_term(Source, Config) -> jsx:is_term(Source, Config).
-type decoder() :: fun((json_text() | end_stream) -> any()).
-spec decoder(Handler::module(), State::any(), Config::list()) -> decoder().
decoder(Handler, State, Config) -> jsx:decoder(Handler, State, Config).
-type encoder() :: fun((json_term() | end_stream) -> any()).
-spec encoder(Handler::module(), State::any(), Config::list()) -> encoder().
encoder(Handler, State, Config) -> jsx: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(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)).
-record(config, {
labels = binary
}).
-type state() :: {[any()], #config{}}.
-spec init(Config::proplists:proplist()) -> state().
init(Config) -> jsx_to_term:init(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}}`
%% of if there's a key with a yet to be matched value
%% `{object, Key, #{NthKey => NthValue},...}}`
%% an array looks like
%% `{array, [NthValue, NthMinus1Value,...FirstValue]}`
%% 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, EmptyMap}], Config}) when is_map(EmptyMap), map_size(EmptyMap) < 1 ->
{#{}, Config};
finish({[{object, EmptyMap}|Rest], Config}) when is_map(EmptyMap), map_size(EmptyMap) < 1 ->
insert(#{}, {Rest, Config});
finish({[{object, Pairs}], Config}) -> {Pairs, Config};
finish({[{object, Pairs}|Rest], Config}) -> insert(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, maps:put(Key, Value, Pairs)}] ++ Rest, Config};
insert(Value, {[{array, Values}|Rest], Config}) ->
{[{array, [Value] ++ Values}] ++ Rest, Config};
insert(_, _) -> erlang:error(badarg).
get_value({Value, _Config}) -> Value;
get_value(_) -> erlang:error(badarg).
-ifdef(TEST).
-include_lib("eunit/include/eunit.hrl").
basic_decode_test_() ->
[
{"empty object", ?_assertEqual(#{}, decode(<<"{}">>))},
{"simple object", ?_assertEqual(
#{<<"key">> => <<"value">>},
decode(<<"{\"key\": \"value\"}">>)
)},
{"nested object", ?_assertEqual(
#{<<"key">> => #{<<"key">> => <<"value">>}},
decode(<<"{\"key\": {\"key\": \"value\"}}">>)
)},
{"complex object", ?_assertEqual(
#{<<"key">> => [
#{<<"key">> => <<"value">>},
#{<<"key">> => []},
#{<<"key">> => 1.0},
true,
false,
null
],
<<"another key">> => #{}
},
decode(<<"{\"key\": [
{\"key\": \"value\"},
{\"key\": []},
{\"key\": 1.0},
true,
false,
null
], \"another key\": {}
}">>)
)},
{"empty list", ?_assertEqual([], decode(<<"[]">>))},
{"raw value", ?_assertEqual(1.0, decode(<<"1.0">>))}
].
-endif.