Browse Source

Merge pull request #11 from altworx/json_term

Support json terminals
Takeru Ohta 8 years ago
parent
commit
6a3665864a
5 changed files with 135 additions and 3 deletions
  1. 6 0
      README.md
  2. 53 1
      doc/jsone.md
  3. 45 2
      src/jsone.erl
  4. 17 0
      src/jsone_encode.erl
  5. 14 0
      test/jsone_encode_tests.erl

+ 6 - 0
README.md

@@ -186,8 +186,14 @@ abc                  -> "abc"                  -> <<"abc">> % non-special atom i
 [{<<"key">>, val}]   -> {"key":"val"}          -> [{<<"key">>, <<"val">>}]   % object_format=proplist
 [{<<"key">>, val}]   -> {"key":"val"}          -> [{<<"key">>, <<"val">>}]   % object_format=proplist
 #{}                  -> {}                     -> #{}                        % object_format=map
 #{}                  -> {}                     -> #{}                        % object_format=map
 #{key => val}        -> {"key":"val"}          -> #{<<"key">> => <<"val">>}  % object_format=map
 #{key => val}        -> {"key":"val"}          -> #{<<"key">> => <<"val">>}  % object_format=map
+{json, IOList}       -> Value                  -> ~~~                        % UTF-8 encoded term
+{json_utf8, Chars}   -> Value                  -> ~~~                        % Unicode code points
 ```
 ```
 
 
+`{json, IOList} and {json_utf8, Chars} allows inline already encoded JSON
+values. For example, you obtain JSON encoded data from database so you don't
+have to decode it first and encode again. See [json_term()](doc/jsone.md#type-json_term).
+
 API
 API
 ---
 ---
 
 

+ 53 - 1
doc/jsone.md

@@ -244,11 +244,63 @@ NOTE: `decode/1` always returns `binary()` value
 
 
 
 
 
 
+### <a name="type-json_term">json_term()</a> ###
+
+
+<pre><code>
+json_term() = {json, iolist()} | {json_utf8, <a href="unicode.md#type-chardata">unicode:chardata()</a>}
+</code></pre>
+
+`json_term()` allows inline already encoded JSON value. `json` variant
+expects byte encoded utf8 data values as list members. `json_utf8` expect
+Unicode code points as list members. Binaries are copied "as is" in both
+variants except `json_utf8` will check if binary contain valid `UTF-8`
+encoded data. In short, `json` uses `erlang:iolist_to_binary/1` and
+`json_utf8` uses `unicode:chardata_to_binary/1` for encoding.
+
+A simple example is worth a thousand words.
+
+```
+  1> S = "hélo".
+  "hélo"
+  2> shell:strings(false).
+  true
+  3> S.
+  [104,233,108,111]
+  4> B = jsone:encode({json, S}).  % invalid UTF-8
+  <<104,233,108,111>>
+  5> B2 = jsone:encode({json_utf8, S}). % valid UTF-8
+  <<104,195,169,108,111>>
+  6> jsone:encode({json, B}).
+  <<104,233,108,111>>
+  7> jsone:encode({json_utf8, B}).
+  ** exception error: {invalid_json_utf8,<<104>>,<<233,108,111>>}
+       in function  jsone_encode:value/4
+          called as jsone_encode:value({json_utf8,<<104,233,108,111>>},
+                                       [],<<>>,
+                                       {encode_opt_v2,false,
+                                                      [{scientific,20}],
+                                                      {iso8601,0},
+                                                      string,0,0})
+       in call from jsone:encode/2 (/home/hynek/work/altworx/jsone/_build/default/lib/jsone/src/jsone.erl, line 302)
+  8> jsone:encode({json_utf8, B2}).
+  <<104,195,169,108,111>>
+  9> shell:strings(true).
+  false
+  10> jsone:encode({json_utf8, B2}).
+  <<"hélo"/utf8>>
+  11> jsone:encode({json, binary_to_list(B2)}). % UTF-8 encoded list leads to valid UTF-8
+  <<"hélo"/utf8>>
+```
+
+
+
+
 ### <a name="type-json_value">json_value()</a> ###
 ### <a name="type-json_value">json_value()</a> ###
 
 
 
 
 <pre><code>
 <pre><code>
-json_value() = <a href="#type-json_number">json_number()</a> | <a href="#type-json_string">json_string()</a> | <a href="#type-json_array">json_array()</a> | <a href="#type-json_object">json_object()</a> | <a href="#type-json_boolean">json_boolean()</a> | null
+json_value() = <a href="#type-json_number">json_number()</a> | <a href="#type-json_string">json_string()</a> | <a href="#type-json_array">json_array()</a> | <a href="#type-json_object">json_object()</a> | <a href="#type-json_boolean">json_boolean()</a> | null | <a href="#type-json_term">json_term()</a>
 </code></pre>
 </code></pre>
 
 
 
 

+ 45 - 2
src/jsone.erl

@@ -44,6 +44,7 @@
               json_array/0,
               json_array/0,
               json_object/0,
               json_object/0,
               json_object_members/0,
               json_object_members/0,
+              json_term/0,
               json_object_format_tuple/0,
               json_object_format_tuple/0,
               json_object_format_proplist/0,
               json_object_format_proplist/0,
               json_object_format_map/0,
               json_object_format_map/0,
@@ -59,7 +60,7 @@
 %%--------------------------------------------------------------------------------
 %%--------------------------------------------------------------------------------
 %% Types & Macros
 %% Types & Macros
 %%--------------------------------------------------------------------------------
 %%--------------------------------------------------------------------------------
--type json_value()          :: json_number() | json_string() | json_array() | json_object() | json_boolean() | null.
+-type json_value()          :: json_number() | json_string() | json_array() | json_object() | json_boolean() | null | json_term().
 -type json_boolean()        :: boolean().
 -type json_boolean()        :: boolean().
 -type json_number()         :: number().
 -type json_number()         :: number().
 -type json_string()         :: binary() | atom() | calendar:datetime(). % NOTE: `decode/1' always returns `binary()' value
 -type json_string()         :: binary() | atom() | calendar:datetime(). % NOTE: `decode/1' always returns `binary()' value
@@ -68,7 +69,49 @@
                              | json_object_format_proplist()
                              | json_object_format_proplist()
                              | json_object_format_map().
                              | json_object_format_map().
 -type json_object_members() :: [{json_string(), json_value()}].
 -type json_object_members() :: [{json_string(), json_value()}].
-
+-type json_term()           :: {json, iolist()} | {json_utf8, unicode:chardata()}.
+%% `json_term()' allows inline already encoded JSON value. `json' variant
+%% expects byte encoded utf8 data values as list members. `json_utf8' expect
+%% Unicode code points as list members. Binaries are copied "as is" in both
+%% variants except `json_utf8' will check if binary contain valid `UTF-8'
+%% encoded data. In short, `json' uses `erlang:iolist_to_binary/1' and
+%% `json_utf8' uses `unicode:chardata_to_binary/1' for encoding.
+%%
+%% A simple example is worth a thousand words.
+%%
+%% ```
+%% 1> S = "hélo".
+%% "hélo"
+%% 2> shell:strings(false).
+%% true
+%% 3> S.
+%% [104,233,108,111]
+%% 4> B = jsone:encode({json, S}).  % invalid UTF-8
+%% <<104,233,108,111>>
+%% 5> B2 = jsone:encode({json_utf8, S}). % valid UTF-8
+%% <<104,195,169,108,111>>
+%% 6> jsone:encode({json, B}).
+%% <<104,233,108,111>>
+%% 7> jsone:encode({json_utf8, B}).
+%% ** exception error: {invalid_json_utf8,<<104>>,<<233,108,111>>}
+%%      in function  jsone_encode:value/4
+%%         called as jsone_encode:value({json_utf8,<<104,233,108,111>>},
+%%                                      [],<<>>,
+%%                                      {encode_opt_v2,false,
+%%                                                     [{scientific,20}],
+%%                                                     {iso8601,0},
+%%                                                     string,0,0})
+%%      in call from jsone:encode/2 (/home/hynek/work/altworx/jsone/_build/default/lib/jsone/src/jsone.erl, line 302)
+%% 8> jsone:encode({json_utf8, B2}).
+%% <<104,195,169,108,111>>
+%% 9> shell:strings(true).
+%% false
+%% 10> jsone:encode({json_utf8, B2}).
+%% <<"hélo"/utf8>>
+%% 11> jsone:encode({json, binary_to_list(B2)}). % UTF-8 encoded list leads to valid UTF-8
+%% <<"hélo"/utf8>>
+%% '''
+%%
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_tuple() :: {json_object_members()}.
 -type json_object_format_proplist() :: [{}] | json_object_members().
 -type json_object_format_proplist() :: [{}] | json_object_members().
 
 

+ 17 - 0
src/jsone_encode.erl

@@ -109,6 +109,23 @@ next(Level = [Next | Nexts], Buf, Opt) ->
 value(null, Nexts, Buf, Opt)                         -> next(Nexts, <<Buf/binary, "null">>, Opt);
 value(null, Nexts, Buf, Opt)                         -> next(Nexts, <<Buf/binary, "null">>, Opt);
 value(false, Nexts, Buf, Opt)                        -> next(Nexts, <<Buf/binary, "false">>, Opt);
 value(false, Nexts, Buf, Opt)                        -> next(Nexts, <<Buf/binary, "false">>, Opt);
 value(true, Nexts, Buf, Opt)                         -> next(Nexts, <<Buf/binary, "true">>, Opt);
 value(true, Nexts, Buf, Opt)                         -> next(Nexts, <<Buf/binary, "true">>, Opt);
+value({json, T}, Nexts, Buf, Opt) ->
+    try
+        next(Nexts, <<Buf/binary, (iolist_to_binary(T))/binary>>, Opt)
+    catch
+         error:badarg ->
+            ?ERROR(value, [{json, T}, Nexts, Buf, Opt])
+    end;
+value({json_utf8, T}, Nexts, Buf, Opt) ->
+    try unicode:characters_to_binary(T) of
+        {error, OK, Invalid} ->
+            {error, {{invalid_json_utf8, OK, Invalid}, [{?MODULE, value, [{json_utf8, T}, Nexts, Buf, Opt], [{line, ?LINE}]}]}};
+        B when is_binary(B) ->
+            next(Nexts, <<Buf/binary, B/binary>>, Opt)
+    catch
+        error:badarg ->
+            ?ERROR(value, [{json_utf8, T}, Nexts, Buf, Opt])
+    end;
 value(Value, Nexts, Buf, Opt) when is_integer(Value) -> next(Nexts, <<Buf/binary, (integer_to_binary(Value))/binary>>, Opt);
 value(Value, Nexts, Buf, Opt) when is_integer(Value) -> next(Nexts, <<Buf/binary, (integer_to_binary(Value))/binary>>, Opt);
 value(Value, Nexts, Buf, Opt) when is_float(Value)   -> next(Nexts, <<Buf/binary, (float_to_binary(Value, Opt?OPT.float_format))/binary>>, Opt);
 value(Value, Nexts, Buf, Opt) when is_float(Value)   -> next(Nexts, <<Buf/binary, (float_to_binary(Value, Opt?OPT.float_format))/binary>>, Opt);
 value(Value, Nexts, Buf, Opt) when ?IS_STR(Value)    -> string(Value, Nexts, Buf, Opt);
 value(Value, Nexts, Buf, Opt) when ?IS_STR(Value)    -> string(Value, Nexts, Buf, Opt);

+ 14 - 0
test/jsone_encode_tests.erl

@@ -30,6 +30,20 @@ encode_test_() ->
               ?assertEqual({ok, <<"null">>}, jsone_encode:encode(null))
               ?assertEqual({ok, <<"null">>}, jsone_encode:encode(null))
       end},
       end},
 
 
+     %% Numbers: Inline json term
+     {"json",
+      fun () ->
+              ?assertEqual(
+                 {ok, <<"{\"foo\":[1,2,3],\"bar\":\"",195,169,"ok\"}">>},
+                 jsone_encode:encode(
+                   ?OBJ2(foo, {json, ["["|[$1, ",2",<<",3]">>]]},
+                         <<"bar">>, {json_utf8, [$", 233, "ok", $"]}))),
+              ?assertEqual(
+                 {ok, <<"{\"foo\":[1,2,3],\"bar\":\"",233,"ok\"}">>},
+                 jsone_encode:encode(
+                   ?OBJ2(foo, {json, ["["|[$1, ",2",<<",3]">>]]},
+                         <<"bar">>, {json, [$", 233, "ok", $"]})))
+      end},
      %% Numbers: Integer
      %% Numbers: Integer
      {"zero",
      {"zero",
       fun () ->
       fun () ->