Browse Source

JSONエンコード用のモジュールを実装

Takeru Ohta 11 years ago
parent
commit
79c3d86283
5 changed files with 221 additions and 4 deletions
  1. 1 0
      rebar.config
  2. 4 2
      src/jsone.erl
  3. 65 2
      src/jsone_encode.erl
  4. 124 0
      test/jsone_encode_tests.erl
  5. 27 0
      test/jsone_tests.erl

+ 1 - 0
rebar.config

@@ -26,4 +26,5 @@
  
 {deps,
   [
+   {meck, ".*", {git, "git://github.com/eproxus/meck.git", {tag, "0.8"}}}
   ]}.

+ 4 - 2
src/jsone.erl

@@ -60,12 +60,14 @@
 %%--------------------------------------------------------------------------------
 %% @doc JSONバイナリをデコードする.
 %%
-%% デコードに失敗した場合は`{invalid_json, 失敗位置より後のJSON::binary()}'形式のエラーが送出される.
+%% デコードに失敗した場合はエラーが送出される
 -spec decode(binary()) -> {json_value(), RestJson::binary()}.
 decode(Json) ->
     jsone_decode:decode(Json).
 
-%% @doc JSON値をバイナリ形式にエンコードする.
+%% @doc JSON値をiodata形式にエンコードする.
+%%
+%% エンコードに失敗した場合はエラーが送出される
 -spec encode(json_value()) -> iodata().
 encode(JsonValue) ->
     jsone_encode:encode(JsonValue).

+ 65 - 2
src/jsone_encode.erl

@@ -35,6 +35,69 @@
 %%--------------------------------------------------------------------------------
 %% Exported Functions
 %%--------------------------------------------------------------------------------
+%% @doc JSON値をiodata形式にエンコードする.
 -spec encode(jsone:json_value()) -> iodata().
-encode(_JsonValue) ->
-    [].
+encode(null)                         -> <<"null">>;
+encode(false)                        -> <<"false">>;
+encode(true)                         -> <<"true">>;
+encode(Value) when is_integer(Value) -> integer_to_binary(Value);
+encode(Value) when is_float(Value)   -> float_to_binary(Value);
+encode(Value) when is_binary(Value)  -> string(Value);
+encode(Value) when is_list(Value)    -> array(Value);
+encode({object, _} = Value)          -> object(Value);
+encode(Value)                        -> error({invalid_json_value, Value}).
+
+%%--------------------------------------------------------------------------------
+%% Internal Functions
+%%--------------------------------------------------------------------------------
+-spec string(jsone:json_string()) -> iodata().
+string(Str) ->
+    %% XXX: 手抜き実装 (一回変換を挟んで無駄)
+    UnicodeStr = unicode:characters_to_list(Str),
+    [$", escape_string(UnicodeStr, []), $"].
+
+-spec escape_string(string(), iolist()) -> iodata().
+escape_string([], Acc)         -> lists:reverse(Acc);
+escape_string([$"  | Str], Acc) -> escape_string(Str, [$", $\\ | Acc]);
+escape_string([$\/ | Str], Acc) -> escape_string(Str, [$/, $\\ | Acc]); 
+escape_string([$\\ | Str], Acc) -> escape_string(Str, [$\\,$\\ | Acc]); 
+escape_string([$\b | Str], Acc) -> escape_string(Str, [$b, $\\ | Acc]);
+escape_string([$\f | Str], Acc) -> escape_string(Str, [$f, $\\ | Acc]); 
+escape_string([$\n | Str], Acc) -> escape_string(Str, [$n, $\\ | Acc]);
+escape_string([$\r | Str], Acc) -> escape_string(Str, [$r, $\\ | Acc]); 
+escape_string([$\t | Str], Acc) -> escape_string(Str, [$t, $\\ | Acc]); 
+escape_string([C   | Str], Acc) ->
+    case C < 16#80 of
+        true  -> escape_string(Str, [C | Acc]);
+        false -> escape_string(Str, [escape_unicode_char(C) | Acc])
+    end. 
+
+%% NOTE: `Unicode'の値が適切であることは`unicode'モジュールが保証してくれていることを期待
+-spec escape_unicode_char(char()) -> iodata().
+escape_unicode_char(Unicode) when Unicode =< 16#FFFF ->
+    io_lib:format("\\u~4.16.0b", [Unicode]);
+escape_unicode_char(Unicode) ->
+    %% サロゲートペア (非効率実装)
+    <<High:10, Low:10>> = <<(Unicode - 16#10000):20>>,
+    io_lib:format("\\u~4.16.0b\\u~4.16.0b", [High + 16#D800, Low + 16#DC00]).
+
+-spec array(jsone:json_array()) -> iodata().
+array(List) ->
+    [$[, iodata_join(lists:map(fun encode/1, List), $,), $]].
+
+-spec object(jsone:json_object()) -> iodata().
+object({object, Members} = Object) ->
+    [${,
+     iodata_join([case is_binary(Key) of
+                      false -> error({invalid_json_value, Object});
+                      true  -> [string(Key), $:, encode(Value)]
+                  end || {Key, Value} <- Members],
+                 $,),
+     $}].
+
+-spec iodata_join([iodata()], char()) -> iodata().
+iodata_join([], _Delimiter)           -> [];
+iodata_join([Head | Tail], Delimiter) ->
+    lists:foldl(fun (IoData, Acc) -> [Acc, Delimiter, IoData] end,
+                Head,
+                Tail).

+ 124 - 0
test/jsone_encode_tests.erl

@@ -0,0 +1,124 @@
+%% Copyright (c) 2013, Takeru Ohta <phjgt308@gmail.com>
+-module(jsone_encode_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+encode_test_() ->
+    [
+     %% シンボル系
+     {"false がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"false">>, iolist_to_binary(jsone_encode:encode(false)))
+      end},
+     {"true がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"true">>, iolist_to_binary(jsone_encode:encode(true)))
+      end},
+     {"null がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"null">>, iolist_to_binary(jsone_encode:encode(null)))
+      end},
+
+     %% 数値系: 整数
+     {"0がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"0">>, iolist_to_binary(jsone_encode:encode(0)))
+      end},
+     {"正の整数がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"1">>, iolist_to_binary(jsone_encode:encode(1)))
+      end},
+     {"負の整数がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"-1">>, iolist_to_binary(jsone_encode:encode(-1)))
+      end},
+     {"巨大な整数がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"11111111111111111111111111111111111111111111111111111111111111111111111">>,
+                           iolist_to_binary(jsone_encode:encode(11111111111111111111111111111111111111111111111111111111111111111111111)))
+      end},
+
+     %% 数値系: 小数
+     {"小数がエンコード可能",
+      fun () ->
+              Input   = 1.234,
+              Encoded = iolist_to_binary(jsone_encode:encode(Input)),
+              ?assertEqual(Input, binary_to_float(Encoded))
+      end},
+
+     %% 文字列系
+     {"文字列がエンコード可能",
+      fun () ->
+              ?assertEqual(<<"\"abc\"">>, iolist_to_binary(jsone_encode:encode(<<"abc">>)))
+      end},
+     {"各種エスケープ文字を含む文字列をエンコード可能",
+      fun () ->
+              Input    = <<"\"\/\\\b\f\n\r\t">>,
+              Expected = list_to_binary([$", [[$\\, C] || C <- [$", $/, $\\, $b, $f, $n, $r, $t]], $"]),
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+     {"UTF-8形式のマルチバイト文字列がエンコード可能",
+      fun () ->
+              %% 日本語
+              Input1    = <<"あいうえお">>,  % このファイルの文字エンコーディングがUTF-8であることが前提
+              Expected1 = <<"\"\\u3042\\u3044\\u3046\\u3048\\u304a\"">>,
+              ?assertEqual(Expected1, iolist_to_binary(jsone_encode:encode(Input1))),
+
+              %% 日本語以外のマルチバイト文字
+              Input2    = <<"۝۞ႮႯ">>,
+              Expected2 = <<"\"\\u06dd\\u06de\\u10ae\\u10af\"">>,
+              ?assertEqual(Expected2, iolist_to_binary(jsone_encode:encode(Input2)))
+      end},
+     {"サロゲートペアを含む文字列がエンコード可能",
+      fun () ->
+              Input    = <<"𢁉𢂚𢃼">>,
+              Expected = <<"\"\\ud848\\udc49\\ud848\\udc9a\\ud848\\udcfc\"">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+
+     %% 配列系
+     {"配列(リスト)がエンコード可能",
+      fun () ->
+              Input    = [1, 2, 3],
+              Expected = <<"[1,2,3]">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+     {"空配列がエンコード可能",
+      fun () ->
+              Input    = [],
+              Expected = <<"[]">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+
+     %% オブジェクト系
+     {"オブジェクトがエンコード可能",
+      fun () ->
+              Input    = {object, [{<<"key">>, <<"value">>}, {<<"1">>, 2}]},
+              Expected = <<"{\"key\":\"value\",\"1\":2}">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+     {"空オブジェクトがエンコード可能",
+      fun () ->
+              Input    = {object, []},
+              Expected = <<"{}">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+     {"オブジェクトのメンバのキーにはバイナリのみが使用可能",
+      fun () ->
+              ?assertError({invalid_json_value, _}, jsone_encode:encode({object, [{1, 2}]})),
+              ?assertError({invalid_json_value, _}, jsone_encode:encode({object, [{"1", 2}]})),
+              ?assertError({invalid_json_value, _}, jsone_encode:encode({object, [{true, 2}]}))
+      end},
+
+     %% その他
+     {"複雑なデータがエンコード可能",
+      fun () ->
+              Input    = [true, {object, [{<<"1">>, 2}, {<<"array">>, [[[[1]]], {object, [{<<"ab">>, <<"cd">>}]}, false]}]}, null],
+              Expected = <<"[true,{\"1\":2,\"array\":[[[[1]]],{\"ab\":\"cd\"},false]},null]">>,
+              ?assertEqual(Expected, iolist_to_binary(jsone_encode:encode(Input)))
+      end},
+     {"不正な値",
+      fun () ->
+              ?assertError({invalid_json_value, _}, jsone_encode:encode(self()))
+      end}
+    ].

+ 27 - 0
test/jsone_tests.erl

@@ -0,0 +1,27 @@
+%% Copyright (c) 2013, Takeru Ohta <phjgt308@gmail.com>
+-module(jsone_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+
+encode_test_() ->
+    [
+     {"エンコード処理は jsone_encode:encode/1 が行う",
+      fun () ->
+              ok = meck:new(jsone_encode),
+              ok = meck:expect(jsone_encode, encode, 1, dummy_result),
+              ?assertEqual(dummy_result, jsone:encode([1, 2, 3])),
+              ok = meck:unload(jsone_encode)
+      end}
+    ].
+
+decode_test_() ->
+    [
+     {"デコード処理は jsone_decode:decode/1 が行う",
+      fun () ->
+              ok = meck:new(jsone_decode),
+              ok = meck:expect(jsone_decode, decode, 1, dummy_result),
+              ?assertEqual(dummy_result, jsone:decode(<<"[1, 2, 3]">>)),
+              ok = meck:unload(jsone_decode)
+      end}
+    ].
+