-module(gleamson@patch).
-compile([no_auto_import, nowarn_unused_vars, nowarn_unused_function, nowarn_nomatch, inline]).
-define(FILEPATH, "src/gleamson/patch.gleam").
-export([apply/2, diff/2, to_json/1, decoder/0]).
-export_type([operation/0, patch_error/0]).
-if(?OTP_RELEASE >= 27).
-define(MODULEDOC(Str), -moduledoc(Str)).
-define(DOC(Str), -doc(Str)).
-else.
-define(MODULEDOC(Str), -compile([])).
-define(DOC(Str), -compile([])).
-endif.
?MODULEDOC(
" JSON Patch (RFC 6902): describe and apply changes to a `gleamson.Json`\n"
" document, and compute the difference between two documents.\n"
"\n"
" A patch is a list of `Operation`s applied in order and atomically — if any\n"
" operation fails, `apply` returns an error and the original document is\n"
" left untouched. Paths are JSON Pointers (RFC 6901).\n"
"\n"
" ```gleam\n"
" import gleamson\n"
" import gleamson/patch.{Add, Replace}\n"
"\n"
" let assert Ok(doc) = gleamson.parse(\"{\\\"a\\\":1,\\\"b\\\":[10]}\")\n"
" let assert Ok(out) =\n"
" patch.apply(doc, [Replace(\"/a\", gleamson.Int(2)), Add(\"/b/-\", gleamson.Int(20))])\n"
" // out == {\"a\":2,\"b\":[10,20]}\n"
" ```\n"
).
-type operation() :: {add, binary(), gleamson:json()} |
{remove, binary()} |
{replace, binary(), gleamson:json()} |
{move, binary(), binary()} |
{copy, binary(), binary()} |
{test, binary(), gleamson:json()}.
-type patch_error() :: {path_not_found, binary()} |
{invalid_path, binary()} |
{test_failed, binary(), gleamson:json(), gleamson:json()}.
-file("src/gleamson/patch.gleam", 474).
-spec element_at(list(gleamson:json()), integer(), binary()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
element_at(Items, I, Token) ->
{_, Rest} = gleam@list:split(Items, I),
case Rest of
[Value | _] ->
{ok, Value};
[] ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 439).
-spec parse_index(binary()) -> {ok, integer()} | {error, nil}.
parse_index(Token) ->
case gleam_stdlib:parse_int(Token) of
{ok, I} when I >= 0 ->
{ok, I};
_ ->
{error, nil}
end.
-file("src/gleamson/patch.gleam", 216).
-spec get_child(gleamson:json(), binary()) -> {ok, gleamson:json()} |
{error, patch_error()}.
get_child(Json, Token) ->
case Json of
{object, Entries} ->
case gleam@list:key_find(Entries, Token) of
{ok, Value} ->
{ok, Value};
{error, _} ->
{error, {path_not_found, Token}}
end;
{array, Items} ->
case parse_index(Token) of
{ok, I} ->
element_at(Items, I, Token);
{error, _} ->
{error, {invalid_path, Token}}
end;
_ ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 97).
-spec get_at(gleamson:json(), list(binary())) -> {ok, gleamson:json()} |
{error, patch_error()}.
get_at(Json, Path) ->
case Path of
[] ->
{ok, Json};
[Head | Rest] ->
gleam@result:'try'(
get_child(Json, Head),
fun(Child) -> get_at(Child, Rest) end
)
end.
-file("src/gleamson/patch.gleam", 423).
-spec unescape(binary()) -> binary().
unescape(Token) ->
_pipe = Token,
_pipe@1 = gleam@string:replace(_pipe, <<"~1"/utf8>>, <<"/"/utf8>>),
gleam@string:replace(_pipe@1, <<"~0"/utf8>>, <<"~"/utf8>>).
-file("src/gleamson/patch.gleam", 412).
-spec tokens(binary()) -> {ok, list(binary())} | {error, patch_error()}.
tokens(Path) ->
case Path of
<<""/utf8>> ->
{ok, []};
_ ->
case gleam@string:split(Path, <<"/"/utf8>>) of
[<<""/utf8>> | Rest] ->
{ok, gleam@list:map(Rest, fun unescape/1)};
_ ->
{error, {invalid_path, Path}}
end
end.
-file("src/gleamson/patch.gleam", 495).
-spec replace_index(list(gleamson:json()), integer(), gleamson:json()) -> list(gleamson:json()).
replace_index(Items, I, Value) ->
{Before, After} = gleam@list:split(Items, I),
case After of
[_ | Tail] ->
lists:append(Before, [Value | Tail]);
[] ->
lists:append(Before, [Value])
end.
-file("src/gleamson/patch.gleam", 446).
-spec has_key(list({binary(), gleamson:json()}), binary()) -> boolean().
has_key(Entries, Key) ->
gleam@list:any(Entries, fun(Entry) -> erlang:element(1, Entry) =:= Key end).
-file("src/gleamson/patch.gleam", 457).
-spec set_key(list({binary(), gleamson:json()}), binary(), gleamson:json()) -> list({binary(),
gleamson:json()}).
set_key(Entries, Key, Value) ->
case has_key(Entries, Key) of
true ->
gleam@list:map(
Entries,
fun(Entry) -> case erlang:element(1, Entry) =:= Key of
true ->
{Key, Value};
false ->
Entry
end end
);
false ->
lists:append(Entries, [{Key, Value}])
end.
-file("src/gleamson/patch.gleam", 232).
-spec set_child(gleamson:json(), binary(), gleamson:json()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
set_child(Json, Token, Child) ->
case Json of
{object, Entries} ->
{ok, {object, set_key(Entries, Token, Child)}};
{array, Items} ->
case parse_index(Token) of
{ok, I} ->
case I < erlang:length(Items) of
true ->
{ok, {array, replace_index(Items, I, Child)}};
false ->
{error, {path_not_found, Token}}
end;
{error, _} ->
{error, {invalid_path, Token}}
end;
_ ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 482).
-spec insert_at(list(gleamson:json()), integer(), gleamson:json()) -> list(gleamson:json()).
insert_at(Items, I, Value) ->
{Before, After} = gleam@list:split(Items, I),
lists:append(Before, [Value | After]).
-file("src/gleamson/patch.gleam", 124).
-spec add_here(gleamson:json(), binary(), gleamson:json()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
add_here(Json, Token, Value) ->
case Json of
{object, Entries} ->
{ok, {object, set_key(Entries, Token, Value)}};
{array, Items} ->
case Token of
<<"-"/utf8>> ->
{ok, {array, lists:append(Items, [Value])}};
_ ->
case parse_index(Token) of
{ok, I} ->
case I =< erlang:length(Items) of
true ->
{ok, {array, insert_at(Items, I, Value)}};
false ->
{error, {path_not_found, Token}}
end;
{error, _} ->
{error, {invalid_path, Token}}
end
end;
_ ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 107).
-spec add_at(gleamson:json(), list(binary()), gleamson:json()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
add_at(Json, Path, Value) ->
case Path of
[] ->
{ok, Value};
[Token] ->
add_here(Json, Token, Value);
[Head | Rest] ->
gleam@result:'try'(
get_child(Json, Head),
fun(Child) ->
gleam@result:'try'(
add_at(Child, Rest, Value),
fun(Updated) -> set_child(Json, Head, Updated) end
)
end
)
end.
-file("src/gleamson/patch.gleam", 487).
-spec delete_at(list(gleamson:json()), integer()) -> list(gleamson:json()).
delete_at(Items, I) ->
{Before, After} = gleam@list:split(Items, I),
case After of
[_ | Tail] ->
lists:append(Before, Tail);
[] ->
Before
end.
-file("src/gleamson/patch.gleam", 450).
-spec delete_key(list({binary(), gleamson:json()}), binary()) -> list({binary(),
gleamson:json()}).
delete_key(Entries, Key) ->
gleam@list:filter(
Entries,
fun(Entry) -> erlang:element(1, Entry) /= Key end
).
-file("src/gleamson/patch.gleam", 156).
-spec remove_here(gleamson:json(), binary()) -> {ok, gleamson:json()} |
{error, patch_error()}.
remove_here(Json, Token) ->
case Json of
{object, Entries} ->
case has_key(Entries, Token) of
true ->
{ok, {object, delete_key(Entries, Token)}};
false ->
{error, {path_not_found, Token}}
end;
{array, Items} ->
case parse_index(Token) of
{ok, I} ->
case I < erlang:length(Items) of
true ->
{ok, {array, delete_at(Items, I)}};
false ->
{error, {path_not_found, Token}}
end;
{error, _} ->
{error, {invalid_path, Token}}
end;
_ ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 144).
-spec remove_at(gleamson:json(), list(binary())) -> {ok, gleamson:json()} |
{error, patch_error()}.
remove_at(Json, Path) ->
case Path of
[] ->
{error, {invalid_path, <<""/utf8>>}};
[Token] ->
remove_here(Json, Token);
[Head | Rest] ->
gleam@result:'try'(
get_child(Json, Head),
fun(Child) ->
gleam@result:'try'(
remove_at(Child, Rest),
fun(Updated) -> set_child(Json, Head, Updated) end
)
end
)
end.
-file("src/gleamson/patch.gleam", 192).
-spec replace_here(gleamson:json(), binary(), gleamson:json()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
replace_here(Json, Token, Value) ->
case Json of
{object, Entries} ->
case has_key(Entries, Token) of
true ->
{ok, {object, set_key(Entries, Token, Value)}};
false ->
{error, {path_not_found, Token}}
end;
{array, Items} ->
case parse_index(Token) of
{ok, I} ->
case I < erlang:length(Items) of
true ->
{ok, {array, replace_index(Items, I, Value)}};
false ->
{error, {path_not_found, Token}}
end;
{error, _} ->
{error, {invalid_path, Token}}
end;
_ ->
{error, {path_not_found, Token}}
end.
-file("src/gleamson/patch.gleam", 176).
-spec replace_at(gleamson:json(), list(binary()), gleamson:json()) -> {ok,
gleamson:json()} |
{error, patch_error()}.
replace_at(Json, Path, Value) ->
case Path of
[] ->
{ok, Value};
[Token] ->
replace_here(Json, Token, Value);
[Head | Rest] ->
gleam@result:'try'(
get_child(Json, Head),
fun(Child) ->
gleam@result:'try'(
replace_at(Child, Rest, Value),
fun(Updated) -> set_child(Json, Head, Updated) end
)
end
)
end.
-file("src/gleamson/patch.gleam", 57).
-spec apply_one(gleamson:json(), operation()) -> {ok, gleamson:json()} |
{error, patch_error()}.
apply_one(Json, Operation) ->
case Operation of
{add, Path, Value} ->
gleam@result:'try'(
tokens(Path),
fun(Path@1) -> add_at(Json, Path@1, Value) end
);
{remove, Path@2} ->
gleam@result:'try'(
tokens(Path@2),
fun(Path@3) -> remove_at(Json, Path@3) end
);
{replace, Path@4, Value@1} ->
gleam@result:'try'(
tokens(Path@4),
fun(Path@5) -> replace_at(Json, Path@5, Value@1) end
);
{copy, From, Path@6} ->
gleam@result:'try'(
tokens(From),
fun(From@1) ->
gleam@result:'try'(
tokens(Path@6),
fun(Path@7) ->
gleam@result:'try'(
get_at(Json, From@1),
fun(Value@2) ->
add_at(Json, Path@7, Value@2)
end
)
end
)
end
);
{move, From@2, Path@8} ->
gleam@result:'try'(
tokens(From@2),
fun(From@3) ->
gleam@result:'try'(
tokens(Path@8),
fun(Path@9) ->
gleam@result:'try'(
get_at(Json, From@3),
fun(Value@3) ->
gleam@result:'try'(
remove_at(Json, From@3),
fun(Without) ->
add_at(Without, Path@9, Value@3)
end
)
end
)
end
)
end
);
{test, Path@10, Value@4} ->
gleam@result:'try'(
tokens(Path@10),
fun(Parsed) ->
gleam@result:'try'(
get_at(Json, Parsed),
fun(Actual) ->
case gleamson:semantically_equal(Actual, Value@4) of
true ->
{ok, Json};
false ->
{error,
{test_failed, Path@10, Value@4, Actual}}
end
end
)
end
)
end.
-file("src/gleamson/patch.gleam", 50).
?DOC(" Apply a patch to a document. All operations succeed, or none are applied.\n").
-spec apply(gleamson:json(), list(operation())) -> {ok, gleamson:json()} |
{error, patch_error()}.
apply(Json, Operations) ->
gleam@list:try_fold(Operations, Json, fun apply_one/2).
-file("src/gleamson/patch.gleam", 433).
-spec escape(binary()) -> binary().
escape(Token) ->
_pipe = Token,
_pipe@1 = gleam@string:replace(_pipe, <<"~"/utf8>>, <<"~0"/utf8>>),
gleam@string:replace(_pipe@1, <<"/"/utf8>>, <<"~1"/utf8>>).
-file("src/gleamson/patch.gleam", 429).
-spec join(binary(), binary()) -> binary().
join(Path, Token) ->
<<<<Path/binary, "/"/utf8>>/binary, (escape(Token))/binary>>.
-file("src/gleamson/patch.gleam", 294).
-spec diff_arrays(
list(gleamson:json()),
list(gleamson:json()),
binary(),
integer()
) -> list(operation()).
diff_arrays(A, B, Path, I) ->
case {A, B} of
{[], []} ->
[];
{[], [Y | Ys]} ->
[{add, join(Path, <<"-"/utf8>>), Y} | diff_arrays([], Ys, Path, I)];
{[_ | Xs], []} ->
[{remove, join(Path, erlang:integer_to_binary(I))} |
diff_arrays(Xs, [], Path, I)];
{[X | Xs@1], [Y@1 | Ys@1]} ->
lists:append(
diff_at(X, Y@1, join(Path, erlang:integer_to_binary(I))),
diff_arrays(Xs@1, Ys@1, Path, I + 1)
)
end.
-file("src/gleamson/patch.gleam", 271).
-spec diff_objects(
list({binary(), gleamson:json()}),
list({binary(), gleamson:json()}),
binary()
) -> list(operation()).
diff_objects(A, B, Path) ->
Removes = gleam@list:filter_map(
A,
fun(Entry) -> case has_key(B, erlang:element(1, Entry)) of
true ->
{error, nil};
false ->
{ok, {remove, join(Path, erlang:element(1, Entry))}}
end end
),
Changes = gleam@list:flat_map(
B,
fun(Entry@1) ->
{Key, Value_b} = Entry@1,
case gleam@list:key_find(A, Key) of
{ok, Value_a} ->
diff_at(Value_a, Value_b, join(Path, Key));
{error, _} ->
[{add, join(Path, Key), Value_b}]
end
end
),
lists:append(Removes, Changes).
-file("src/gleamson/patch.gleam", 259).
-spec diff_at(gleamson:json(), gleamson:json(), binary()) -> list(operation()).
diff_at(From, To, Path) ->
case gleamson:semantically_equal(From, To) of
true ->
[];
false ->
case {From, To} of
{{object, A}, {object, B}} ->
diff_objects(A, B, Path);
{{array, A@1}, {array, B@1}} ->
diff_arrays(A@1, B@1, Path, 0);
{_, _} ->
[{replace, Path, To}]
end
end.
-file("src/gleamson/patch.gleam", 255).
?DOC(
" Compute a patch that turns `from` into `to`. The result is correct but not\n"
" guaranteed minimal: array changes are emitted positionally rather than by\n"
" detecting moves.\n"
).
-spec diff(gleamson:json(), gleamson:json()) -> list(operation()).
diff(From, To) ->
diff_at(From, To, <<""/utf8>>).
-file("src/gleamson/patch.gleam", 326).
-spec operation_to_json(operation()) -> gleamson:json().
operation_to_json(Operation) ->
case Operation of
{add, Path, Value} ->
{object,
[{<<"op"/utf8>>, {string, <<"add"/utf8>>}},
{<<"path"/utf8>>, {string, Path}},
{<<"value"/utf8>>, Value}]};
{remove, Path@1} ->
{object,
[{<<"op"/utf8>>, {string, <<"remove"/utf8>>}},
{<<"path"/utf8>>, {string, Path@1}}]};
{replace, Path@2, Value@1} ->
{object,
[{<<"op"/utf8>>, {string, <<"replace"/utf8>>}},
{<<"path"/utf8>>, {string, Path@2}},
{<<"value"/utf8>>, Value@1}]};
{move, From, Path@3} ->
{object,
[{<<"op"/utf8>>, {string, <<"move"/utf8>>}},
{<<"from"/utf8>>, {string, From}},
{<<"path"/utf8>>, {string, Path@3}}]};
{copy, From@1, Path@4} ->
{object,
[{<<"op"/utf8>>, {string, <<"copy"/utf8>>}},
{<<"from"/utf8>>, {string, From@1}},
{<<"path"/utf8>>, {string, Path@4}}]};
{test, Path@5, Value@2} ->
{object,
[{<<"op"/utf8>>, {string, <<"test"/utf8>>}},
{<<"path"/utf8>>, {string, Path@5}},
{<<"value"/utf8>>, Value@2}]}
end.
-file("src/gleamson/patch.gleam", 322).
?DOC(" Encode a patch as a JSON value (an array of operation objects).\n").
-spec to_json(list(operation())) -> gleamson:json().
to_json(Operations) ->
gleamson:array(Operations, fun operation_to_json/1).
-file("src/gleamson/patch.gleam", 368).
-spec operation_decoder() -> fun((gleamson:json()) -> {operation(),
list(gleamson@decode:decode_error())}).
operation_decoder() ->
gleamson@decode:field(
<<"op"/utf8>>,
fun gleamson@decode:string/1,
fun(Op) -> case Op of
<<"add"/utf8>> ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path) ->
gleamson@decode:field(
<<"value"/utf8>>,
fun gleamson@decode:json/1,
fun(Value) ->
gleamson@decode:success({add, Path, Value})
end
)
end
);
<<"remove"/utf8>> ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path@1) ->
gleamson@decode:success({remove, Path@1})
end
);
<<"replace"/utf8>> ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path@2) ->
gleamson@decode:field(
<<"value"/utf8>>,
fun gleamson@decode:json/1,
fun(Value@1) ->
gleamson@decode:success(
{replace, Path@2, Value@1}
)
end
)
end
);
<<"move"/utf8>> ->
gleamson@decode:field(
<<"from"/utf8>>,
fun gleamson@decode:string/1,
fun(From) ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path@3) ->
gleamson@decode:success(
{move, From, Path@3}
)
end
)
end
);
<<"copy"/utf8>> ->
gleamson@decode:field(
<<"from"/utf8>>,
fun gleamson@decode:string/1,
fun(From@1) ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path@4) ->
gleamson@decode:success(
{copy, From@1, Path@4}
)
end
)
end
);
<<"test"/utf8>> ->
gleamson@decode:field(
<<"path"/utf8>>,
fun gleamson@decode:string/1,
fun(Path@5) ->
gleamson@decode:field(
<<"value"/utf8>>,
fun gleamson@decode:json/1,
fun(Value@2) ->
gleamson@decode:success(
{test, Path@5, Value@2}
)
end
)
end
);
_ ->
gleamson@decode:failure(
{remove, <<""/utf8>>},
<<"a JSON Patch op (add/remove/replace/move/copy/test)"/utf8>>
)
end end
).
-file("src/gleamson/patch.gleam", 364).
?DOC(
" A decoder for a JSON Patch document (an array of operations). Pair it with\n"
" `gleamson.parse` / `decode.from_string` to read a patch off the wire.\n"
).
-spec decoder() -> fun((gleamson:json()) -> {list(operation()),
list(gleamson@decode:decode_error())}).
decoder() ->
gleamson@decode:list(operation_decoder()).