diff --git a/src/jsx.erl b/src/jsx.erl index 63c7b8a..09249a6 100644 --- a/src/jsx.erl +++ b/src/jsx.erl @@ -27,6 +27,7 @@ -export([encoder/0, encoder/1]). -export([decoder/0, decoder/1]). -export([fold/3, fold/4]). +-export([format/1, format/2]). %% various semi-useful types with nowhere else to hang out @@ -61,8 +62,7 @@ scanner() -> scanner([]). scanner(OptsList) when is_list(OptsList) -> fun(JSON) when is_binary(JSON) -> (decoder(OptsList))(JSON) - ; (Terms) when is_list(Terms); is_tuple(Terms) -> - (encoder(OptsList))(Terms) + ; (Terms) when is_list(Terms) -> (encoder(OptsList))(Terms) end. @@ -76,7 +76,7 @@ decoder() -> decoder([]). decoder(OptsList) when is_list(OptsList) -> jsx_decoder:decoder(OptsList). --type encoder() :: fun((list() | tuple()) -> +-type encoder() :: fun((list()) -> {ok, events()} | {incomplete, decoder()}). -spec encoder() -> encoder(). @@ -115,6 +115,14 @@ fold(Fun, Acc, Source, Opts) -> end. +-spec format(Source::binary()) -> binary(). +-spec format(Source::binary() | list(), Opts::jsx_format:opts()) -> binary(). + +format(Source) -> format(Source, []). + +format(Source, Opts) -> jsx_format:format(Source, Opts). + + -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). diff --git a/src/jsx_encoder.erl b/src/jsx_encoder.erl index e544795..04d1d72 100644 --- a/src/jsx_encoder.erl +++ b/src/jsx_encoder.erl @@ -61,22 +61,22 @@ encoder(OptsList) -> -start({string, String}, [], [], Opts) when is_binary(String); is_list(String) -> +start([{string, String}], [], [], Opts) when is_binary(String); is_list(String) -> {ok, [{string, unicode:characters_to_list(jsx_utils:json_escape(String, Opts))}, end_json ] }; -start({float, Float}, [], [], _Opts) when is_float(Float) -> +start([{float, Float}], [], [], _Opts) when is_float(Float) -> {ok, [{float, Float}, end_json] }; -start({integer, Int}, [], [], _Opts) when is_integer(Int) -> +start([{integer, Int}], [], [], _Opts) when is_integer(Int) -> {ok, [{integer, Int}, end_json] }; -start({literal, Atom}, [], [], _Opts) +start([{literal, Atom}], [], [], _Opts) when Atom == true; Atom == false; Atom == null -> {ok, [{literal, Atom}, end_json] @@ -84,8 +84,8 @@ start({literal, Atom}, [], [], _Opts) %% third parameter is a stack to match end_foos to start_foos start(Forms, [], [], Opts) when is_list(Forms) -> list_or_object(Forms, [], [], Opts); -start(Raw, [], [], Opts) -> - start(term_to_event(Raw), [], [], Opts). +start(Forms, T, Stack, Opts) -> + ?error([Forms, T, Stack, Opts]). list_or_object([start_object|Forms], T, Stack, Opts) -> @@ -147,25 +147,13 @@ done([], T, [], _Opts) -> done(Forms, T, Stack, Opts) -> ?error([Forms, T, Stack, Opts]). -term_to_event(X) when is_integer(X) -> {integer, X}; -term_to_event(X) when is_float(X) -> {float, X}; -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}. - - -ifdef(TEST). -include_lib("eunit/include/eunit.hrl"). encode(Terms) -> try case (jsx:encoder([]))(Terms) of - {ok, Terms} -> - true - %% matches [foo, end_json], aka naked terms - ; {ok, [Terms, end_json]} -> - true + {ok, Terms} -> true end catch error:badarg -> false @@ -220,10 +208,18 @@ encode_test_() -> end_array, end_json ]))}, - {"naked string", ?_assert(encode({string, "hello"}))}, - {"naked literal", ?_assert(encode({literal, true}))}, - {"naked integer", ?_assert(encode({integer, 1}))}, - {"naked float", ?_assert(encode({float, 1.0}))} + {"naked string", ?_assert((jsx:scanner())([{string, "hello"}]) + =:= {ok, [{string, "hello"}, end_json]} + )}, + {"naked literal", ?_assert((jsx:scanner())([{literal, true}]) + =:= {ok, [{literal, true}, end_json]} + )}, + {"naked integer", ?_assert((jsx:scanner())([{integer, 1}]) + =:= {ok, [{integer, 1}, end_json]} + )}, + {"naked string", ?_assert((jsx:scanner())([{float, 1.0}]) + =:= {ok, [{float, 1.0}, end_json]} + )} ]. -endif. \ No newline at end of file diff --git a/src/jsx_format.erl b/src/jsx_format.erl new file mode 100644 index 0000000..8634772 --- /dev/null +++ b/src/jsx_format.erl @@ -0,0 +1,194 @@ +%% 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). + +-export([format/2]). + + +-record(opts, { + output_encoding = utf8 +}). + +-type opts() :: []. + + +-spec format(Source::(binary() | list()), Opts::opts()) -> binary(). + +format(Source, Opts) when (is_binary(Source) andalso is_list(Opts)) + orelse (is_list(Source) andalso is_list(Opts)) -> + jsx:fold(fun fold/2, + {start, [], parse_opts(Opts)}, + Source, + extract_opts(Opts) + ). + + +parse_opts(Opts) -> parse_opts(Opts, #opts{}). + +parse_opts([{output_encoding, Val}|Rest], Opts) when Val == utf8 -> + parse_opts(Rest, Opts#opts{output_encoding = Val}); +parse_opts([_|Rest], Opts) -> + parse_opts(Rest, Opts); +parse_opts([], Opts) -> + Opts. + + +extract_opts(Opts) -> + extract_parser_opts(Opts, []). + +extract_parser_opts([], Acc) -> Acc; +extract_parser_opts([{K,V}|Rest], Acc) -> + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + true -> [{K,V}] ++ Acc + ; false -> extract_parser_opts(Rest, Acc) + end; +extract_parser_opts([K|Rest], Acc) -> + case lists:member(K, [loose_unicode, escape_forward_slash, explicit_end]) of + true -> [K] ++ Acc + ; false -> extract_parser_opts(Rest, Acc) + end. + + +-define(start_object, <<"{">>). +-define(start_array, <<"[">>). +-define(end_object, <<"}">>). +-define(end_array, <<"]">>). +-define(colon, <<":">>). +-define(comma, <<",">>). +-define(quote, <<"\"">>). +-define(space, <<" ">>). +-define(newline, <<"\n">>). + + +fold(Event, {start, Acc, Opts}) -> + case Event of + start_object -> {[object_start], [Acc, ?start_object], Opts} + ; start_array -> {[array_start], [Acc, ?start_array], Opts} + end; +fold(Event, {[object_start|Stack], Acc, Opts}) -> + case Event of + {key, Key} -> + {[object_value|Stack], [Acc, encode(string, Key), ?colon], Opts} + ; end_object -> + {Stack, [Acc, ?end_object], Opts} + end; +fold(Event, {[object_value|Stack], Acc, Opts}) -> + case Event of + {Type, Value} when Type == string; Type == literal; + Type == integer; Type == float -> + {[key|Stack], [Acc, encode(Type, Value)], Opts} + ; start_object -> {[object_start, key|Stack], [Acc, ?start_object], Opts} + ; start_array -> {[array_start, key|Stack], [Acc, ?start_array], Opts} + end; +fold(Event, {[key|Stack], Acc, Opts}) -> + case Event of + {key, Key} -> + {[object_value|Stack], [Acc, ?comma, encode(string, Key), ?colon], Opts} + ; end_object -> + {Stack, [Acc, ?end_object], Opts} + end; +fold(Event, {[array_start|Stack], Acc, Opts}) -> + case Event of + {Type, Value} when Type == string; Type == literal; + Type == integer; Type == float -> + {[array|Stack], [Acc, encode(Type, Value)], Opts} + ; start_object -> {[object_start, array|Stack], [Acc, ?start_object], Opts} + ; start_array -> {[array_start, array|Stack], [Acc, ?start_array], Opts} + ; end_array -> {Stack, [Acc, ?end_array], Opts} + end; +fold(Event, {[array|Stack], Acc, Opts}) -> + case Event of + {Type, Value} when Type == string; Type == literal; + Type == integer; Type == float -> + {[array|Stack], [Acc, ?comma, encode(Type, Value)], Opts} + ; end_array -> {Stack, [Acc, ?end_array], Opts} + ; start_object -> {[object_start, array|Stack], [Acc, ?comma, ?start_object], Opts} + ; start_array -> {[array_start, array|Stack], [Acc, ?comma, ?start_array], Opts} + end; +fold(end_json, {[], Acc, Opts}) -> encode(Acc, Opts). + + +encode(Acc, Opts) when is_list(Acc) -> + case Opts#opts.output_encoding of + iolist -> Acc + ; utf8 -> unicode:characters_to_binary(Acc, utf8) + ; _ -> erlang:error(badarg) + end; +encode(string, String) -> + [?quote, String, ?quote]; +encode(literal, Literal) -> + erlang:atom_to_list(Literal); +encode(integer, Integer) -> + erlang:integer_to_list(Integer); +encode(float, Float) -> + jsx_utils:nice_decimal(Float). + + +%% eunit tests + +-ifdef(TEST). +-include_lib("eunit/include/eunit.hrl"). + +basic_test_() -> + [ + {"empty object", ?_assert(format(<<"{}">>, []) =:= <<"{}">>)}, + {"empty array", ?_assert(format(<<"[]">>, []) =:= <<"[]">>)}, + {"simple object", + ?_assert(format(<<" { \"key\" :\n\t \"value\"\r\r\r\n } ">>, + [] + ) =:= <<"{\"key\":\"value\"}">> + ) + }, + {"simple array", + ?_assert(format(<<" [\n\ttrue,\n\tfalse , \n \tnull\n] ">>, + [] + ) =:= <<"[true,false,null]">> + ) + }, + {"nested structures", + ?_assert(format( + <<"[{\"key\":\"value\", + \"another key\": \"another value\", + \"a list\": [true, false] + }, + [[{}]] + ]">>, [] + ) =:= <<"[{\"key\":\"value\",\"another key\":\"another value\",\"a list\":[true,false]},[[{}]]]">> + ) + } + ]. + +terms_test_() -> + [ + {"terms", + ?_assert(format([start_object, + {key, <<"key">>}, + {string, <<"value">>}, + end_object, + end_json + ], []) =:= <<"{\"key\":\"value\"}">> + )} + ]. + +-endif. \ No newline at end of file