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]">>)).
