Browse Source

Support basic pretty-print functionality

Takeru Ohta 10 years ago
parent
commit
d0c1aaa408
6 changed files with 132 additions and 25 deletions
  1. 2 3
      Makefile
  2. 26 1
      README.md
  3. 15 2
      doc/jsone.md
  4. 13 2
      src/jsone.erl
  5. 45 16
      src/jsone_encode.erl
  6. 31 1
      test/jsone_encode_tests.erl

+ 2 - 3
Makefile

@@ -1,12 +1,11 @@
 APP=jsone
-NODE=$(APP)@localhost
 
 DIALYZER_OPTS=-Werror_handling -Wrace_conditions -Wunmatched_returns
 
 all: compile xref eunit dialyze
 
 init:
-	@./rebar get-deps compile 
+	@./rebar get-deps compile
 
 compile:
 	@./rebar compile skip_deps=true
@@ -24,7 +23,7 @@ edoc:
 	@./rebar doc skip_deps=true
 
 start: compile
-	erl -sname $(NODE) -pz ebin deps/*/ebin \
+	erl -pz ebin deps/*/ebin \
       -eval 'erlang:display({start_app, $(APP), application:start($(APP))}).'
 
 .dialyzer.plt:

+ 26 - 1
README.md

@@ -1,4 +1,4 @@
-jsone (0.3.1)
+jsone (0.3.2)
 =============
 
 An Erlang library for encoding, decoding [JSON](http://json.org/index.html) data.
@@ -95,6 +95,31 @@ Usage Example
 {error,{badarg,[{jsone_encode,object_members,
                               [[{123,<<"value">>}],[],<<"{">>],
                               [{line,138}]}]}}
+
+%% Pretty Print
+> Data = [true, {[{<<"1">>, 2}, {<<"array">>, [[[[1]]], {[{<<"ab">>, <<"cd">>}]}, false]}]}, null],
+> io:format("~s\n", jsone:encode(Data, [{indent, 1}, {space, 2}])).
+[
+  true,
+  {
+    "1": 2,
+    "array": [
+      [
+        [
+          [
+            1
+          ]
+        ]
+      ],
+      {
+        "ab": "cd"
+      },
+      false
+    ]
+  },
+  null
+]
+ok
 ```
 
 

+ 15 - 2
doc/jsone.md

@@ -41,12 +41,25 @@ decode_option() = {object_format, tuple | proplist | map}
 
 
 <pre><code>
-encode_option() = native_utf8
+encode_option() = native_utf8 | {space, non_neg_integer()} | {indent, non_neg_integer()}
 </code></pre>
 
 
 
-  native_utf8: Encodes UTF-8 characters as a human-readable(non-escaped) string
+
+`native_utf8`: <br />
+- Encodes UTF-8 characters as a human-readable(non-escaped) string <br />
+
+
+
+`{space, N}`: <br />
+- Inserts `N` spaces after every commna and colon <br />
+- default: `0` <br />
+
+
+`{indent, N}`: <br />
+- Inserts a newline and `N` spaces for each level of indentation <br />
+- default: `0` <br />
 
 
 

+ 13 - 2
src/jsone.erl

@@ -69,8 +69,19 @@
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_proplist() :: [{}] | json_object_members().
 
--type encode_option() :: native_utf8.
-%% native_utf8: Encodes UTF-8 characters as a human-readable(non-escaped) string
+-type encode_option() :: native_utf8
+                       | {space, non_neg_integer()}
+                       | {indent, non_neg_integer()}.
+%% `native_utf8': <br />
+%% - Encodes UTF-8 characters as a human-readable(non-escaped) string <br />
+%%
+%% `{space, N}': <br />
+%% - Inserts `N' spaces after every commna and colon <br />
+%% - default: `0' <br />
+%%
+%% `{indent, N}': <br />
+%% - Inserts a newline and `N' spaces for each level of indentation <br />
+%% - default: `0' <br />
 
 -type decode_option() :: {object_format, tuple | proplist | map}.
 %% object_format: <br />

+ 45 - 16
src/jsone_encode.erl

@@ -46,7 +46,11 @@
               | {object_value, jsone:json_value(), jsone:json_object_members()}
               | {object_members, jsone:json_object_members()}.
 
--record(encode_opt_v1, { native_utf8 = false :: boolean() }).
+-record(encode_opt_v1, {
+          native_utf8 = false :: boolean(),
+          space = 0 :: non_neg_integer(),
+          indent = 0 :: non_neg_integer()
+         }).
 -define(OPT, #encode_opt_v1).
 -type opt() :: #encode_opt_v1{}.
 
@@ -66,20 +70,20 @@ encode(Value, Options) ->
 %% Internal Functions
 %%--------------------------------------------------------------------------------
 -spec next([next()], binary(), opt()) -> encode_result().
-next([], Buf, _)             -> {ok, Buf};
-next([Next | Nexts], Buf, Opt) ->
+next([], Buf, _)                       -> {ok, Buf};
+next(Level = [Next | Nexts], Buf, Opt) ->
     case Next of
         {array_values, Values} ->
             case Values of
                 [] -> array_values(Values, Nexts, Buf, Opt);
-                _  -> array_values(Values, Nexts, <<Buf/binary, $,>>, Opt)
+                _  -> array_values(Values, Nexts, pp_newline_or_space(<<Buf/binary, $,>>, Level, Opt), Opt)
             end;
         {object_value, Value, Members} ->
-            object_value(Value, Members, Nexts, Buf, Opt);
+            object_value(Value, Members, Nexts, pp_space(<<Buf/binary, $:>>, Opt), Opt);
         {object_members, Members} ->
             case Members of
                 [] -> object_members(Members, Nexts, Buf, Opt);
-                _  -> object_members(Members, Nexts, <<Buf/binary, $,>>, Opt)
+                _  -> object_members(Members, Nexts, pp_newline_or_space(<<Buf/binary, $,>>, Level, Opt), Opt)
             end
     end.
 
@@ -117,10 +121,10 @@ escape_string(<<0:1, C:7, Str/binary>>, Nexts, Buf, Opt) -> escape_string(Str, N
 escape_string(<<2#110:3, B1:5, 2#10:2, B2:6, Str/binary>>, Nexts, Buf, Opt) when not ?IS_REDUNDANT_UTF8(B1, B2, 5) ->
     case Opt?OPT.native_utf8 of
         false ->
-            Unicode = (B1 bsl 6) + B2,
-            escape_unicode_char(Str, Unicode, Nexts, Buf, Opt);
-        true ->
-            unicode_char(Str, <<2#110:3, B1:5, 2#10:2, B2:6>>, Nexts, Buf, Opt)
+             Unicode = (B1 bsl 6) + B2,
+             escape_unicode_char(Str, Unicode, Nexts, Buf, Opt);
+         true ->
+             unicode_char(Str, <<2#110:3, B1:5, 2#10:2, B2:6>>, Nexts, Buf, Opt)
     end;
 escape_string(<<2#1110:4, B1:4, 2#10:2, B2:6, 2#10:2, B3:6, Str/binary>>, Nexts, Buf, Opt) when not ?IS_REDUNDANT_UTF8(B1, B2, 4) ->
     case Opt?OPT.native_utf8 of
@@ -154,29 +158,54 @@ escape_unicode_char(<<Str/binary>>, Unicode, Nexts, Buf, Opt) ->
 
 -spec array(jsone:json_array(), [next()], binary(), opt()) -> encode_result().
 array(List, Nexts, Buf, Opt) ->
-    array_values(List, Nexts, <<Buf/binary, $[>>, Opt).
+    array_values(List, Nexts, pp_newline(<<Buf/binary, $[>>, Nexts, 1, Opt), Opt).
 
 -spec array_values(jsone:json_array(), [next()], binary(), opt()) -> encode_result().
-array_values([],       Nexts, Buf, Opt) -> next(Nexts, <<Buf/binary, $]>>, Opt);
+array_values([],       Nexts, Buf, Opt) -> next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $]>>, Opt);
 array_values([X | Xs], Nexts, Buf, Opt) -> value(X, [{array_values, Xs} | Nexts], Buf, Opt).
 
 -spec object(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
 object(Members, Nexts, Buf, Opt) ->
-    object_members(Members, Nexts, <<Buf/binary, ${>>, Opt).
+    object_members(Members, Nexts, pp_newline(<<Buf/binary, ${>>, Nexts, 1, Opt), Opt).
 
 -spec object_members(jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
-object_members([],                             Nexts, Buf, Opt)        -> next(Nexts, <<Buf/binary, $}>>, Opt);
+object_members([],                             Nexts, Buf, Opt)        -> next(Nexts, <<(pp_newline(Buf, Nexts, Opt))/binary, $}>>, Opt);
 object_members([{Key, Value} | Xs], Nexts, Buf, Opt) when ?IS_STR(Key) -> string(Key, [{object_value, Value, Xs} | Nexts], Buf, Opt);
 object_members(Arg, Nexts, Buf, Opt)                                   -> ?ERROR(object_members, [Arg, Nexts, Buf, Opt]).
 
 -spec object_value(jsone:json_value(), jsone:json_object_members(), [next()], binary(), opt()) -> encode_result().
 object_value(Value, Members, Nexts, Buf, Opt) ->
-    value(Value, [{object_members, Members} | Nexts], <<Buf/binary, $:>>, Opt).
+    value(Value, [{object_members, Members} | Nexts], Buf, Opt).
+
+-spec pp_space(binary(), opt()) -> binary().
+pp_space(Buf, Opt) -> padding(Buf, Opt?OPT.space).
+
+-spec pp_newline(binary(), list(), opt()) -> binary().
+pp_newline(Buf, Level, Opt) -> pp_newline(Buf, Level, 0, Opt).
+
+-spec pp_newline(binary(), list(), non_neg_integer(), opt()) -> binary().
+pp_newline(Buf, _, _,     ?OPT{indent = 0}) -> Buf;
+pp_newline(Buf, L, Extra, ?OPT{indent = N}) -> lists:foldl(fun (_, B) -> padding(B, N) end, padding(<<Buf/binary, $\n>>, Extra * N), L).
+
+-spec pp_newline_or_space(binary(), list(), opt()) -> binary().
+pp_newline_or_space(Buf, _, Opt = ?OPT{indent = 0}) -> pp_space(Buf, Opt);
+pp_newline_or_space(Buf, L, Opt)                    -> pp_newline(Buf, L, Opt).
+
+-spec padding(binary(), non_neg_integer()) -> binary().
+padding(Buf, 0) -> Buf;
+padding(Buf, N) -> padding(<<Buf/binary, $ >>, N - 1).
 
 -spec parse_options([jsone:encode_option()]) -> opt().
 parse_options(Options) ->
     parse_option(Options, ?OPT{}).
 
+-spec parse_option([jsone:encode_option()], opt()) -> opt().
 parse_option([], Opt) -> Opt;
 parse_option([native_utf8|T], Opt) ->
-    parse_option(T, Opt?OPT{native_utf8=true}).
+    parse_option(T, Opt?OPT{native_utf8=true});
+parse_option([{space, N}|T], Opt) when is_integer(N), N >= 0 ->
+    parse_option(T, Opt?OPT{space = N});
+parse_option([{indent, N}|T], Opt) when is_integer(N), N >= 0 ->
+    parse_option(T, Opt?OPT{indent = N});
+parse_option(List, Opt) ->
+    error(badarg, [List, Opt]).

+ 31 - 1
test/jsone_encode_tests.erl

@@ -139,15 +139,45 @@ encode_test_() ->
               ?assertMatch({error, {badarg, _}}, jsone_encode:encode({[{"1", 2}]}))
       end},
 
+     %% Pretty Print
+     {"space",
+      fun () ->
+              ?assertEqual({ok, <<"[1, 2, 3]">>}, jsone_encode:encode([1,2,3], [{space, 1}])),
+              ?assertEqual({ok, <<"[1,  2,  3]">>}, jsone_encode:encode([1,2,3], [{space, 2}])),
+              ?assertEqual({ok, <<"{\"a\": 1, \"b\": 2}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{space, 1}])),
+              ?assertEqual({ok, <<"{\"a\":  1,  \"b\":  2}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{space, 2}]))
+      end},
+     {"indent",
+      fun () ->
+              ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 1}])),
+              ?assertEqual({ok, <<"[\n  1,\n  2,\n  3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 2}])),
+              ?assertEqual({ok, <<"{\n \"a\":1,\n \"b\":2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 1}])),
+              ?assertEqual({ok, <<"{\n  \"a\":1,\n  \"b\":2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 2}]))
+      end},
+     {"indent+space",
+      fun () ->
+              ?assertEqual({ok, <<"[\n 1,\n 2,\n 3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 1}, {space, 1}])),
+              ?assertEqual({ok, <<"[\n  1,\n  2,\n  3\n]">>}, jsone_encode:encode([1,2,3], [{indent, 2}, {space, 2}])),
+              ?assertEqual({ok, <<"{\n \"a\": 1,\n \"b\": 2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 1}, {space, 1}])),
+              ?assertEqual({ok, <<"{\n  \"a\":  1,\n  \"b\":  2\n}">>}, jsone_encode:encode(#{a=>1, b=>2}, [{indent, 2}, {space, 2}]))
+      end},
+
      %% Others
      {"compound data",
       fun () ->
               Input    = [true, {[{<<"1">>, 2}, {<<"array">>, [[[[1]]], {[{<<"ab">>, <<"cd">>}]}, false]}]}, null],
               Expected = <<"[true,{\"1\":2,\"array\":[[[[1]]],{\"ab\":\"cd\"},false]},null]">>,
-              ?assertEqual({ok, Expected}, jsone_encode:encode(Input))
+              ?assertEqual({ok, Expected}, jsone_encode:encode(Input)),
+
+              PpExpected = <<"[\n true,\n {\n  \"1\": 2,\n  \"array\": [\n   [\n    [\n     [\n      1\n     ]\n    ]\n   ],\n   {\n    \"ab\": \"cd\"\n   },\n   false\n  ]\n },\n null\n]">>,
+              ?assertEqual({ok, PpExpected}, jsone_encode:encode(Input, [{indent, 1}, {space, 1}]))
       end},
      {"invalid value",
       fun () ->
               ?assertMatch({error, {badarg, _}}, jsone_encode:encode(self()))
+      end},
+     {"wrong option",
+      fun () ->
+              ?assertError(badarg, jsone_encode:encode(1, [{no_such_option, hoge}]))
       end}
     ].