This is an automated email from the ASF dual-hosted git repository.

nickva pushed a commit to tag 2.0.0
in repository https://gitbox.apache.org/repos/asf/couchdb-jiffy.git

commit 3e3a06bf5cf9d4301bbaab93cb01bd4cd2c8a35e
Author: Nick Vatamaniuc <[email protected]>
AuthorDate: Thu Apr 23 22:20:41 2026 -0400

    Implement pre-encoded json
    
    There have been issues raised and at at least two PRs trying to implement 
this over
    the years:
    
    https://github.com/davisp/jiffy/issues/128
    https://github.com/davisp/jiffy/pull/139
    https://github.com/davisp/jiffy/pull/140
    
    Here we'll just go with @davisp's neat idea from
    
https://github.com/davisp/jiffy/commit/46136403945d03224f9e7fe545ebbfdf5dfae057
    with a few tests attached. It's minimal and does what most folks seem to 
want.
    
    Co-authored-by: Paul J. Davis <[email protected]>
---
 README.md                           | 14 ++++++++++
 c_src/encoder.c                     |  8 ++++--
 src/jiffy.erl                       | 10 +++++--
 test/jiffy_18_pre_encoded_tests.erl | 55 +++++++++++++++++++++++++++++++++++++
 4 files changed, 83 insertions(+), 4 deletions(-)

diff --git a/README.md b/README.md
index 5572a4b..0ef11c7 100644
--- a/README.md
+++ b/README.md
@@ -88,6 +88,20 @@ The options for encode are:
 * `{bytes_per_red, N}` - Refer to the decode options
 * `{bytes_per_iter, N}` - Refer to the decode options
 
+Pre-encoded JSON
+----------------
+
+A `{json, IoData}` tuple can appear anywhere a JSON value is expected (except
+as an object key). The `IoData` is spliced into the output as is. Jiffy does
+not parse, validate, copy, or pretty-print it.
+
+    1> jiffy:encode([1, {json, <<"{\"cached\":true}">>}, 3]).
+    <<"[1,{\"cached\":true},3]">>
+    2> jiffy:encode({[{<<"a">>, {json, [<<"[1,">>, "2,3]"]}}]}).
+    <<"{\"a\":[1,2,3]}">>
+
+The caller is responsible for ensuring it is well-formed JSON.
+
 Data Format
 -----------
 
diff --git a/c_src/encoder.c b/c_src/encoder.c
index ca1689e..a9644fd 100644
--- a/c_src/encoder.c
+++ b/c_src/encoder.c
@@ -961,8 +961,12 @@ encode_iter(ErlNifEnv* env, int argc, const ERL_NIF_TERM 
argv[])
             }
         } else if(enif_get_tuple(env, curr, &arity, &tuple)) {
             if(arity != 1) {
-                ret = enc_obj_error(e, "invalid_ejson", curr);
-                goto done;
+                // Handle unknown or pre-encoded JSON in finish_encode/2
+                if(!enc_unknown(e, curr)) {
+                    ret = enc_error(e, "internal_error");
+                    goto done;
+                }
+                continue;
             }
             if(!enif_is_list(env, tuple[0])) {
                 ret = enc_obj_error(e, "invalid_object", curr);
diff --git a/src/jiffy.erl b/src/jiffy.erl
index 81275ab..5313e5a 100644
--- a/src/jiffy.erl
+++ b/src/jiffy.erl
@@ -23,6 +23,8 @@
 -type json_object() :: {[{json_string(),json_value()}]}
                         | #{json_string() => json_value()}.
 
+-type json_preencoded() :: {json, iodata()}.
+
 -type jiffy_decode_result() :: json_value()
                         | {has_trailer, json_value(), binary()}.
 
@@ -70,12 +72,12 @@ decode(Data, Opts) when is_list(Data) ->
     decode(iolist_to_binary(Data), Opts).
 
 
--spec encode(json_value()) -> iodata().
+-spec encode(json_value() | json_preencoded()) -> iodata().
 encode(Data) ->
     encode(Data, []).
 
 
--spec encode(json_value(), encode_options()) -> iodata().
+-spec encode(json_value() | json_preencoded(), encode_options()) -> iodata().
 encode(Data, Options) ->
     ForceUTF8 = lists:member(force_utf8, Options),
     case nif_encode_init(Data, Options) of
@@ -162,6 +164,10 @@ finish_encode([<<_/binary>>=B | Rest], Acc) ->
     finish_encode(Rest, [B | Acc]);
 finish_encode([Val | Rest], Acc) when is_integer(Val) ->
     finish_encode(Rest, [integer_to_binary(Val) | Acc]);
+finish_encode([{json, Json} | Rest], Acc) ->
+    %% Pre-encoded JSON spliced into the output as-is. This came from
+    %% enc_unknown.
+    finish_encode(Rest, [Json | Acc]);
 finish_encode([InvalidEjson | _], _) ->
     error({invalid_ejson, InvalidEjson});
 finish_encode(_, _) ->
diff --git a/test/jiffy_18_pre_encoded_tests.erl 
b/test/jiffy_18_pre_encoded_tests.erl
new file mode 100644
index 0000000..78c3863
--- /dev/null
+++ b/test/jiffy_18_pre_encoded_tests.erl
@@ -0,0 +1,55 @@
+% This file is part of Jiffy released under the MIT license.
+% See the LICENSE file for more information.
+
+-module(jiffy_18_pre_encoded_tests).
+
+-include_lib("eunit/include/eunit.hrl").
+-include("jiffy_util.hrl").
+
+bare_pre_encoded_test() ->
+    ?assertEqual(<<"[1,2,3]">>, enc({json, <<"[1,2,3]">>})),
+    ?assertEqual(<<"42">>, enc({json, <<"42">>})),
+    ?assertEqual(<<"\"hi\"">>, enc({json, <<"\"hi\"">>})).
+
+iolist_pre_encoded_test() ->
+    ?assertEqual(<<"[1,2,3]">>, enc({json, [<<"[">>, "1,2,", <<"3]">>]})),
+    ?assertEqual(<<"[1,2,3]">>, enc({json, [$[, [$1, $,, $2], <<",3]">>]})).
+
+inside_array_test() ->
+    Pre = {json, <<"{\"x\":42}">>},
+    ?assertEqual(<<"[1,{\"x\":42},3]">>, enc([1, Pre, 3])).
+
+inside_object_test() ->
+    Pre = {json, <<"[1,2,3]">>},
+    ?assertEqual(
+        <<"{\"a\":[1,2,3],\"b\":2}">>,
+        enc({[{<<"a">>, Pre}, {<<"b">>, 2}]})
+    ),
+    % Map iteration order is unpredictable so round-trip it
+    MapOut = enc(#{<<"a">> => Pre, <<"b">> => 2}),
+    ?assertEqual(
+        #{<<"a">> => [1, 2, 3], <<"b">> => 2},
+        jiffy:decode(MapOut, [return_maps])
+    ).
+
+nested_pre_encoded_test() ->
+    % Nesting
+    Inner = {json, <<"\"raw\"">>},
+    EJson = {[
+        {<<"k1">>, [1, Inner, 3]},
+        {<<"k2">>, {json, <<"null">>}}
+    ]},
+    ?assertEqual(
+        <<"{\"k1\":[1,\"raw\",3],\"k2\":null}">>,
+        enc(EJson)
+    ).
+
+invalid_non_arity_one_tuple_still_errors_test() ->
+    % Other arity-2 things still surface as an error
+    ?assertError({invalid_ejson, {foo, bar}}, enc({foo, bar})).
+
+pretty_with_pre_encoded_test() ->
+    % No pretty-printing or anything for spliced json
+    Pre = {json, <<"[1,2,3]">>},
+    Out = iol2b(jiffy:encode([1, Pre], [pretty])),
+    ?assertNotEqual(nomatch, binary:match(Out, <<"[1,2,3]">>)).

Reply via email to