diff --git a/ebin/jsx.app b/ebin/jsx.app index 51784f6..3018493 100644 --- a/ebin/jsx.app +++ b/ebin/jsx.app @@ -1,14 +1,18 @@ {application, jsx, [ {description, "a streaming, evented json parsing toolkit"}, - {vsn, "0.0.1"}, + {vsn, "0.8"}, {modules, [ jsx, jsx_utf8, jsx_utf16, jsx_utf16le, jsx_utf32, - jsx_utf32le + jsx_utf32le, + jsx_eep0018, + jsx_encoder, + jsx_format, + jsx_verify ]}, {registered, []}, {applications, [ diff --git a/include/jsx_types.hrl b/include/jsx_types.hrl index f6a629f..a1021ca 100644 --- a/include/jsx_types.hrl +++ b/include/jsx_types.hrl @@ -24,8 +24,6 @@ %% unsure of how to specify a binary with a complex structure like utfx encoded %% binaries. this should be further limited somehow probably. --type json() :: binary(). - -type jsx_opts() :: [jsx_opt()]. -type jsx_opt() :: {comments, true | false} @@ -55,9 +53,61 @@ %% this probably doesn't work properly --type jsx_parser() :: fun((json()) -> jsx_parser_result()). +-type jsx_parser() :: fun((binary()) -> jsx_parser_result()). -type jsx_parser_result() :: {event, jsx_event(), fun(() -> jsx_parser_result())} | {incomplete, jsx_parser()} | {error, badjson} - | ok. \ No newline at end of file + | ok. + + +-type json() :: json_object() | json_array(). + +-type json_array() :: [json_term()]. +-type json_object() :: [{json_key(), json_term()}]. + +-type json_key() :: binary() | atom(). + +-type json_term() :: json_array() | json_object() | json_string() | json_number() | true | false | null. + +-type json_string() :: binary(). + +-type json_number() :: float() | integer(). + + +-type supported_utf() :: utf8 | utf16 | {utf16, little} | utf32 | {utf32, little}. + + +-type encoder_opts() :: [encoder_opt()]. +-type encoder_opt() :: {strict, true | false} + | {encoding, auto | supported_utf()} + | {space, integer()} + | space + | {indent, integer()} + | indent. + + + +-type decoder_opts() :: [decoder_opt()]. +-type decoder_opt() :: {strict, true | false} + | {comments, true | false} + | {encoding, supported_utf()} + | {label, atom | binary | existing_atom} + | {float, true | false}. + + +-type verify_opts() :: [verify_opt()]. +-type verify_opt() :: {strict, true | false} + | {encoding, auto | supported_utf()} + | {comments, true | false}. + + +-type format_opts() :: [format_opt()]. +-type format_opt() :: {strict, true | false} + | {encoding, auto | supported_utf()} + | {comments, true | false} + | {space, integer()} + | space + | {indent, integer()} + | indent + | {output_encoding, supported_utf()}. \ No newline at end of file diff --git a/makefile b/makefile index 016e34c..0da1433 100644 --- a/makefile +++ b/makefile @@ -15,8 +15,5 @@ clean: ./rebar clean ./priv/backends.escript clean -package: compile - ./rebar install target=. - install: compile ./rebar install \ No newline at end of file diff --git a/src/jsx.erl b/src/jsx.erl index 0ba19ea..7e80643 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -26,6 +26,10 @@ %% the core parser api -export([parser/0, parser/1]). +-export([term_to_json/1, term_to_json/2]). +-export([json_to_term/1, json_to_term/2]). +-export([is_json/1, is_json/2]). +-export([format/1, format/2]). %% types for function specifications -include("./include/jsx_types.hrl"). @@ -38,6 +42,7 @@ encoding = auto }). + -spec parser() -> jsx_parser(). -spec parser(Opts::jsx_opts()) -> jsx_parser(). @@ -53,12 +58,55 @@ parser(OptsList) -> ; {utf32, little} -> fun jsx_utf32le:parse/2 ; auto -> fun detect_encoding/2 end, - start(F, OptsList). + case parse_opts(OptsList) of + {error, badopt} -> {error, badopt} + ; Opts -> fun(Stream) -> F(Stream, Opts) end + end. -start(F, OptsList) -> - Opts = parse_opts(OptsList), - fun(Stream) -> F(Stream, Opts) end. + +-spec term_to_json(JSON::json()) -> binary(). +-spec term_to_json(JSON::json(), Opts::encoder_opts()) -> binary(). +term_to_json(JSON) -> + term_to_json(JSON, []). + +term_to_json(JSON, Opts) -> + json_encoder:term_to_json(JSON, Opts). + + +-spec json_to_term(JSON::binary()) -> json(). +-spec json_to_term(JSON::binary(), Opts::decoder_opts()) -> json(). + +json_to_term(JSON) -> + json_to_term(JSON, []). + +json_to_term(JSON, Opts) -> + json_decoder:json_to_term(JSON, Opts). + + +-spec is_json(JSON::binary()) -> true | false. +-spec is_json(JSON::binary(), Opts::verify_opts()) -> true | false. + +is_json(JSON) -> + is_json(JSON, []). + +is_json(JSON, Opts) -> + json_verify:is_json(JSON, Opts). + + +-spec format(JSON::binary()) -> binary() | iolist(). +-spec format(JSON::binary(), Opts::format_opts()) -> binary() | iolist(). + +format(JSON) -> + format(JSON, []). + +format(JSON, Opts) -> + json_pp:pp(JSON, Opts). + + +%% ---------------------------------------------------------------------------- +%% internal functions +%% ---------------------------------------------------------------------------- %% option parsing @@ -78,7 +126,9 @@ parse_opts([{multi_term, Value}|Rest], Opts) -> true = lists:member(Value, [true, false]), parse_opts(Rest, Opts#opts{multi_term = Value}); parse_opts([{encoding, _}|Rest], Opts) -> - parse_opts(Rest, Opts). + parse_opts(Rest, Opts); +parse_opts(_, _) -> + {error, badopt}. %% encoding detection diff --git a/src/jsx_eep0018.erl b/src/jsx_eep0018.erl new file mode 100644 index 0000000..7e24281 --- /dev/null +++ b/src/jsx_eep0018.erl @@ -0,0 +1,145 @@ +%% The MIT License + +%% Copyright (c) 2010 Alisdair Sullivan + +%% 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_eep0018). +-author("alisdairsullivan@yahoo.ca"). + +-export([json_to_term/2]). + +-include("./include/jsx_types.hrl"). + + + +-spec json_to_term(JSON::binary(), Opts::decoder_opts()) -> json(). + +json_to_term(JSON, Opts) -> + P = jsx:parser(opts_to_jsx_opts(Opts)), + case proplists:get_value(strict, Opts, true) of + true -> collect_strict(P(JSON), [[]], Opts) + ; false -> collect(P(JSON), [[]], Opts) + end. + + +opts_to_jsx_opts(Opts) -> + opts_to_jsx_opts(Opts, []). + +opts_to_jsx_opts([{encoding, Val}|Rest], Acc) -> + case lists:member(Val, [auto, utf8, utf16, {utf16, little}, utf32, {utf32, little}]) of + true -> opts_to_jsx_opts(Rest, [{encoding, Val}] ++ Acc) + ; false -> opts_to_jsx_opts(Rest, Acc) + end; +opts_to_jsx_opts([{comments, Val}|Rest], Acc) -> + case Val of + true -> opts_to_jsx_opts(Rest, [{comments, true}] ++ Acc) + ; false -> opts_to_jsx_opts(Rest, [{comments, false}] ++ Acc) + ; _ -> opts_to_jsx_opts(Rest, Acc) + end; +opts_to_jsx_opts([_|Rest], Acc) -> + opts_to_jsx_opts(Rest, Acc); +opts_to_jsx_opts([], Acc) -> + Acc. + + +collect_strict({event, Start, Next}, Acc, Opts) when Start =:= start_object; Start =:= start_array -> + collect(Next(), [[]|Acc], Opts); +collect_strict(_, _, _) -> + erlang:error(badarg). + + +collect({event, Start, Next}, Acc, Opts) when Start =:= start_object; Start =:= start_array -> + collect(Next(), [[]|Acc], Opts); + +%% special case for empty object +collect({event, end_object, Next}, [[], Parent|Rest], Opts) -> + collect(Next(), [[[{}]] ++ Parent] ++ Rest, Opts); +%% reverse the array/object accumulator before prepending it to it's parent +collect({event, end_object, Next}, [Current, Parent|Rest], Opts) when is_list(Parent) -> + collect(Next(), [[lists:reverse(Current)] ++ Parent] ++ Rest, Opts); +collect({event, end_array, Next}, [Current, Parent|Rest], Opts) when is_list(Parent) -> + collect(Next(), [[lists:reverse(Current)] ++ Parent] ++ Rest, Opts); +collect({event, Start, Next}, [Current, Key, Parent|Rest], Opts) + when Start =:= end_object; Start =:= end_array -> + collect(Next(), [[{Key, lists:reverse(Current)}] ++ Parent] ++ Rest, Opts); + +%% end of json is emitted asap (at close of array/object), calling Next() until {incomplete, More} +%% and then More(end_stream) ensures the tail of the json binary is clean (whitespace only) +collect({event, end_json, Next}, [[Acc]], _Opts) -> + case Next() of + {incomplete, More} -> case More(end_stream) of + ok -> Acc + ; _ -> erlang:error(badarg) + end + ; _ -> erlang:error(badarg) + end; + +%% key can only be emitted inside of a json object, so just insert it directly into +%% the head of the accumulator and deal with it when we receive it's paired value +collect({event, {key, _} = PreKey, Next}, [Current|_] = Acc, Opts) -> + Key = event(PreKey, Opts), + case key_repeats(Key, Current) of + true -> erlang:error(badarg) + ; false -> collect(Next(), [Key] ++ Acc, Opts) + end; + +%% check acc to see if we're inside an object or an array. because inside an object +%% context the events that fall this far are always preceded by a key (which are +%% binaries or atoms), if Current is a list, we're inside an array, else, an +%% object +collect({event, Event, Next}, [Current|Rest], Opts) when is_list(Current) -> + collect(Next(), [[event(Event, Opts)] ++ Current] ++ Rest, Opts); +collect({event, Event, Next}, [Key, Current|Rest], Opts) -> + collect(Next(), [[{Key, event(Event, Opts)}] ++ Current] ++ Rest, Opts); + +%% any other event is an error +collect(_, _, _) -> erlang:error(badarg). + + +event({string, String}, _Opts) -> + unicode:characters_to_binary(String); +event({key, Key}, Opts) -> + case proplists:get_value(label, Opts, binary) of + binary -> unicode:characters_to_binary(Key) + ; atom -> + try list_to_atom(Key) + catch error:badarg -> unicode:characters_to_binary(Key) end + ; existing_atom -> + try list_to_existing_atom(Key) + catch error:badarg -> unicode:characters_to_binary(Key) end + end; +%% special case for negative zero +event({integer, "-0"}, _Opts) -> + erlang:float(erlang:list_to_integer("-0")); +event({integer, Integer}, Opts) -> + case proplists:get_value(float, Opts, false) of + true -> erlang:float(erlang:list_to_integer(Integer)) + ; false -> erlang:list_to_integer(Integer) + end; +event({float, Float}, _Opts) -> + erlang:list_to_float(Float); +event({literal, Literal}, _Opts) -> + Literal. + + +key_repeats(Key, [{Key, _Value}|_Rest]) -> true; +key_repeats(Key, [_|Rest]) -> key_repeats(Key, Rest); +key_repeats(_Key, []) -> false. \ No newline at end of file diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl new file mode 100644 index 0000000..2daf873 --- /dev/null +++ b/src/jsx_encoder.erl @@ -0,0 +1,220 @@ +%% The MIT License + +%% Copyright (c) 2010 Alisdair Sullivan + +%% 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). +-author("alisdairsullivan@yahoo.ca"). + +-export([term_to_json/2]). + +-include("./include/jsx_types.hrl"). + + + +-spec term_to_json(JSON::json(), Opts::encoder_opts()) -> binary(). + +term_to_json(List, Opts) -> + case proplists:get_value(strict, Opts, true) of + true when is_list(List) -> continue + ; false -> continue + ; true -> erlang:error(badarg) + end, + Encoding = proplists:get_value(encoding, Opts, utf8), + json:format(event_generator(term_to_events(List)), [{output_encoding, Encoding}] ++ Opts). + +event_generator([]) -> + fun() -> {event, end_json, fun() -> {incomplete, fun(end_stream) -> ok end} end} end; +event_generator([Next|Rest]) -> + fun() -> {event, Next, event_generator(Rest)} end. + + +term_to_events([{}]) -> + [start_object, end_object]; +term_to_events([First|_] = List) when is_tuple(First) -> + proplist_to_events(List, [start_object]); +term_to_events(List) when is_list(List) -> + list_to_events(List, [start_array]); +term_to_events(Term) -> + term_to_event(Term). + + +proplist_to_events([{Key, Term}|Rest], Acc) -> + Event = term_to_event(Term), + EncodedKey = key_to_event(Key), + case key_repeats(EncodedKey, Acc) of + false -> proplist_to_events(Rest, Event ++ EncodedKey ++ Acc) + ; true -> erlang:error(badarg) + end; +proplist_to_events([], Acc) -> + lists:reverse([end_object] ++ Acc); +proplist_to_events(_, _) -> + erlang:throw(badarg). + + +list_to_events([Term|Rest], Acc) -> + list_to_events(Rest, term_to_event(Term) ++ Acc); +list_to_events([], Acc) -> + lists:reverse([end_array] ++ Acc). + + +term_to_event(List) when is_list(List) -> + term_to_events(List); +term_to_event(Float) when is_float(Float) -> + [{float, float_to_decimal(Float)}]; +term_to_event(Integer) when is_integer(Integer) -> + [{integer, erlang:integer_to_list(Integer)}]; +term_to_event(String) when is_binary(String) -> + [{string, String}]; +term_to_event(true) -> [{literal, true}]; +term_to_event(false) -> [{literal, false}]; +term_to_event(null) -> [{literal, null}]; +term_to_event(_) -> erlang:error(badarg). + + +key_to_event(Key) when is_atom(Key) -> + [{key, erlang:atom_to_list(Key)}]; +key_to_event(Key) when is_binary(Key) -> + [{key, unicode:characters_to_list(Key, utf8)}]. + + +key_repeats([Key], [Key|_]) -> true; +key_repeats(Key, [_|Rest]) -> key_repeats(Key, Rest); +key_repeats(_, []) -> false. + + +%% conversion of floats to 'nice' decimal output. erlang's float implementation is almost +%% but not quite ieee 754. it converts negative zero to plain zero silently, and throws +%% exceptions for any operations that would produce NaN or infinity. as far as I can +%% tell that is. trying to match against NaN or infinity binary patterns produces nomatch +%% exceptions, and arithmetic operations produce badarg exceptions. with that in mind, this +%% function makes no attempt to handle special values (except for zero) + +%% algorithm from "Printing FLoating-Point Numbers Quickly and Accurately" by Burger & Dybvig +float_to_decimal(0.0) -> "0.0"; +float_to_decimal(Num) when is_float(Num) -> + {F, E} = extract(<>), + {R, S, MP, MM} = initial_vals(F, E), + K = ceiling(math:log10(abs(Num)) - 1.0e-10), + Round = F band 1 =:= 1, + {Dpoint, Digits} = scale(R, S, MP, MM, K, Round), + if Num >= 0 -> format(Dpoint, Digits) + ; Num < 0 -> "-" ++ format(Dpoint, Digits) + end. + + +extract(<<_:1, 0:11, Frac:52>>) -> {Frac, -1074}; +extract(<<_:1, Exp:11, Frac:52>>) -> {Frac + (1 bsl 52), Exp - 1075}. + + +ceiling(X) -> + Y = trunc(X), + case X - Y of + Z when Z > 0 -> Y + 1 + ; _ -> Y + end. + + +initial_vals(F, E) when E >= 0, F /= 1 bsl 52 -> + BE = 1 bsl E, + {F * BE * 2, 2, BE, BE}; +initial_vals(F, E) when E >= 0 -> + BE = 1 bsl E, + {F * BE * 4, 4, BE * 2, BE}; +initial_vals(F, E) when E == -1074; F /= 1 bsl 52 -> + {F * 2, 1 bsl (-E + 1), 1, 1}; +initial_vals(F, E) -> + {F * 4, 1 bsl (-E + 2), 2, 1}. + + +scale(R, S, MP, MM, K, Round) -> + case K >= 0 of + true -> fixup(R, S * pow(10, K), MP, MM, K, Round) + ; false -> + Scale = pow(10, -1 * K), + fixup(R * Scale, S, MP * Scale, MM * Scale, K, Round) + end. + + +fixup(R, S, MP, MM, K, true) -> + case (R + MP >= S) of + true -> {K + 1, generate(R, S, MP, MM, true)} + ; false -> {K, generate(R * 10, S, MP * 10, MM * 10, true)} + end; +fixup(R, S, MP, MM, K, false) -> + case (R + MP > S) of + true -> {K + 1, generate(R, S, MP, MM, true)} + ; false -> {K, generate(R * 10, S, MP * 10, MM * 10, true)} + end. + + +generate(RT, S, MP, MM, Round) -> + D = RT div S, + R = RT rem S, + TC1 = case Round of true -> (R =< MM); false -> (R < MM) end, + TC2 = case Round of true -> (R + MP >= S); false -> (R + MP > S) end, + case TC1 of + false -> case TC2 of + false -> [D | generate(R * 10, S, MP * 10, MM * 10, Round)] + ; true -> [D + 1] + end + ; true -> case TC2 of + false -> [D] + ; true -> case R * 2 < S of + true -> [D] + ; false -> [D + 1] + end + end + end. + + +%% this is not efficient at all and should be replaced with a lookup table probably +pow(_B, 0) -> 1; +pow(B, E) when E > 0 -> pow(B, E, 1). + +pow(B, E, Acc) when E < 2 -> B * Acc; +pow(B, E, Acc) when E band 1 == 1 -> pow(B * B, E bsr 1, B * Acc); +pow(B, E, Acc) -> pow(B * B, E bsr 1, Acc). + + +format(Dpoint, Digits) when Dpoint =< length(Digits), Dpoint > 0 -> + format(Digits, Dpoint, []); +format(Dpoint, Digits) when Dpoint > 0 -> + Pad = Dpoint - length(Digits), + case Pad of + X when X > 6 -> format(Digits, 1, []) ++ "e" ++ integer_to_list(Dpoint - 1) + ; _ -> format(Digits ++ [ 0 || _ <- lists:seq(1, Pad)], Dpoint, []) + end; +format(Dpoint, Digits) when Dpoint < 0 -> + format(Digits, 1, []) ++ "e" ++ integer_to_list(Dpoint - 1). + + +format([], 0, Acc) -> + lists:reverse("0." ++ Acc); +format([], ignore, Acc) -> + lists:reverse(Acc); +format(Digits, 0, Acc) -> + format(Digits, ignore, "." ++ Acc); +format([Digit|Digits], Dpoint, Acc) -> + format(Digits, case Dpoint of ignore -> ignore; X -> X - 1 end, to_ascii(Digit) ++ Acc). + + +to_ascii(X) -> [X + 48]. %% ascii "1" is [49], "2" is [50], etc... \ No newline at end of file diff --git a/src/jsx_format.erl b/src/jsx_format.erl new file mode 100644 index 0000000..09469bf --- /dev/null +++ b/src/jsx_format.erl @@ -0,0 +1,190 @@ +%% The MIT License + +%% Copyright (c) 2010 Alisdair Sullivan + +%% 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_format). +-author("alisdairsullivan@yahoo.ca"). + +-export([pp/2]). + +-include("./include/jsx_types.hrl"). + + + +-record(opts, { + space = 0, + indent = 0, + output_encoding = iolist, + strict = true +}). + + + +-define(newline, $\n). +-define(space, 16#20). %% ascii code for space +-define(quote, $\"). +-define(comma, $,). +-define(colon, $:). +-define(start_object, ${). +-define(end_object, $}). +-define(start_array, $[). +-define(end_array, $]). + + + +-spec format(JSON::binary(), Opts::format_opts()) -> binary() | iolist(). + +pp(F, Opts) when is_function(F) -> + prettify(F(), [], parse_opts(Opts, #opts{}), 0, start); + +pp(JSON, Opts) when is_binary(JSON) -> + P = jsx:parser(extract_parser_opts(Opts)), + prettify(P(JSON), [], parse_opts(Opts, #opts{}), 0, start). + + +parse_opts([{indent, Val}|Rest], Opts) -> + parse_opts(Rest, Opts#opts{indent = Val}); +parse_opts([indent|Rest], Opts) -> + parse_opts(Rest, Opts#opts{indent = 1}); +parse_opts([{space, Val}|Rest], Opts) -> + parse_opts(Rest, Opts#opts{space = Val}); +parse_opts([space|Rest], Opts) -> + parse_opts(Rest, Opts#opts{space = 1}); +parse_opts([{output_encoding, Val}|Rest], Opts) -> + parse_opts(Rest, Opts#opts{output_encoding = Val}); +parse_opts([], Opts) -> + Opts. + + +extract_parser_opts(Opts) -> + [ {K, V} || {K, V} <- Opts, lists:member(K, [comments, encoding]) ]. + + +prettify({event, start_object, Next}, Acc, Opts, Level, start) -> + prettify(Next(), [Acc, ?start_object], Opts, Level + 1, new); +prettify({event, start_object, Next}, Acc, Opts, Level, _) -> + prettify(Next(), + [Acc, ?comma, space(Opts), indent(Opts, Level), ?start_object], + Opts, + Level + 1, + new); + +prettify({event, start_array, Next}, Acc, Opts, Level, start) -> + prettify(Next(), [Acc, ?start_array], Opts, Level + 1, new); +prettify({event, start_array, Next}, Acc, Opts, Level, _) -> + prettify(Next(), + [Acc, ?comma, space(Opts), indent(Opts, Level), ?start_array], + Opts, + Level + 1, + new); + +prettify({event, end_object, Next}, Acc, Opts, Level, value) -> + DeLevel = Level - 1, + prettify(Next(), [Acc, indent(Opts, DeLevel), ?end_object], Opts, DeLevel, value); +prettify({event, end_object, Next}, Acc, Opts, Level, new) -> + prettify(Next(), [Acc, ?end_object], Opts, Level - 1, value); + +prettify({event, end_array, Next}, Acc, Opts, Level, value) -> + DeLevel = Level - 1, + prettify(Next(), [Acc, indent(Opts, DeLevel), ?end_array], Opts, DeLevel, value); +prettify({event, end_array, Next}, Acc, Opts, Level, new) -> + prettify(Next(), [Acc, ?end_array], Opts, Level - 1, value); + +prettify({event, {key, Key}, Next}, Acc, Opts, Level, value) -> + prettify(Next(), + [Acc, ?comma, space(Opts), indent(Opts, Level), format(string, Key), ?colon, space(Opts)], + Opts, + Level, + key); +prettify({event, {key, Key}, Next}, Acc, Opts, Level, _) -> + prettify(Next(), + [Acc, indent(Opts, Level), format(string, Key), ?colon, space(Opts)], + Opts, + Level, + key); + +prettify({event, {Type, Value}, Next}, Acc, Opts, Level, value) -> + prettify(Next(), + [Acc, ?comma, space(Opts), indent(Opts, Level), format(Type, Value)], + Opts, + Level, + value); +prettify({event, {Type, Value}, Next}, Acc, Opts, Level, new) -> + prettify(Next(), [Acc, indent(Opts, Level), format(Type, Value)], Opts, Level, value); +prettify({event, {Type, Value}, Next}, Acc, Opts, Level, key) -> + prettify(Next(), [Acc, format(Type, Value)], Opts, Level, value); +prettify({event, {Type, Value}, Next}, _Acc, Opts, Level, start) -> + case Opts#opts.strict of + true -> erlang:throw(badarg) + ; false -> prettify(Next(), [format(Type, Value)], Opts, Level, error) + end; + +prettify({event, end_json, Next}, Acc, Opts, _, _) -> + case Next() of + {incomplete, More} -> case More(end_stream) of + ok -> encode(Acc, Opts) + ; _ -> erlang:throw(badarg) + end + ; _ -> erlang:throw(badarg) + end; + +prettify(_, _, _, _, error) -> erlang:throw(badarg). + +format(string, String) -> + [?quote, String, ?quote]; +format(literal, Literal) -> + erlang:atom_to_list(Literal); +format(_, Number) -> + Number. + + +indent(Opts, Level) -> + case Opts#opts.indent of + 0 -> [] + ; X when X > 0 -> + Indent = [ ?space || _ <- lists:seq(1, X) ], + indent(Indent, Level, [?newline]) + end. + +indent(_Indent, 0, Acc) -> + Acc; +indent(Indent, N, Acc) -> + indent(Indent, N - 1, [Acc, Indent]). + + +space(Opts) -> + case Opts#opts.space of + 0 -> [] + ; X when X > 0 -> [ ?space || _ <- lists:seq(1, X) ] + end. + + +-define(is_utf_encoding(X), + X == utf8; X == utf16; X == utf32; X == {utf16, little}; X == {utf32, little} +). + +encode(Acc, Opts) -> + case Opts#opts.output_encoding of + iolist -> Acc + ; UTF when ?is_utf_encoding(UTF) -> unicode:characters_to_binary(Acc, utf8, UTF) + ; _ -> erlang:throw(badarg) + end. \ No newline at end of file diff --git a/src/jsx_verify.erl b/src/jsx_verify.erl new file mode 100644 index 0000000..49198c1 --- /dev/null +++ b/src/jsx_verify.erl @@ -0,0 +1,77 @@ +%% The MIT License + +%% Copyright (c) 2010 Alisdair Sullivan + +%% 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). +-author("alisdairsullivan@yahoo.ca"). + +-export([is_json/2]). + +-include("./include/jsx_types.hrl"). + + + +-spec is_json(JSON::binary(), Opts::verify_opts()) -> true | false. + +is_json(JSON, Opts) -> + Encoding = proplists:get_value(encoding, Opts, utf8), + P = jsx:parser([{encoding, Encoding}]), + case proplists:get_value(strict, Opts, true) of + true -> collect_strict(P(JSON), Opts) + ; false -> collect(P(JSON), Opts) + end. + + +%% enforce only arrays and objects at top level +collect_strict({event, start_object, Next}, Keys) -> + collect(Next(), Keys); +collect_strict({event, start_array, Next}, Keys) -> + collect(Next(), Keys); +collect_strict(_, _) -> + false. + +%% make sure to ensure tail is clean +collect({event, end_json, Next}, _Keys) -> + case Next() of + {incomplete, More} -> case More(end_stream) of + ok -> true + ; _ -> false + end + ; _ -> false + end; + + +%% check to see if key has already been encountered, if not add it to the key accumulator +%% and continue, else return false +collect({event, {key, Key}, Next}, Keys) -> + case lists:member(Key, Keys) of + true -> false + ; false -> collect(Next(), [Key] ++ Keys) + end; + +collect({event, _, Next}, Keys) -> + collect(Next(), Keys); +collect(_, _) -> + false. + + +