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

jcoglan pushed a commit to branch mango-match-failures
in repository https://gitbox.apache.org/repos/asf/couchdb.git

commit d11cbf6c9e257d0a82f6f0b6faf4b71f6d3b878a
Author: James Coglan <[email protected]>
AuthorDate: Fri Jan 30 17:44:25 2026 +0000

    feat: Return VDU failure reports to the caller
    
    Until now, document updates rejected by a Mango VDU returned an opaque
    "forbidden" message to the client. This commit adds a detailed list of
    failures, obtained by converting the `#failure` records returned by
    `mango_selector:match/3` into human-readable messages.
---
 src/mango/src/mango_native_proc.erl           |   8 +-
 src/mango/src/mango_selector.erl              | 698 ++++++++++++++++++++++----
 test/elixir/test/validate_doc_update_test.exs |   5 +
 3 files changed, 624 insertions(+), 87 deletions(-)

diff --git a/src/mango/src/mango_native_proc.erl 
b/src/mango/src/mango_native_proc.erl
index edcecd4b6..d3a0078be 100644
--- a/src/mango/src/mango_native_proc.erl
+++ b/src/mango/src/mango_native_proc.erl
@@ -115,9 +115,11 @@ handle_call({prompt, [<<"ddoc">>, DDocId, 
[<<"validate_doc_update">>], Args]}, _
             [NewDoc, OldDoc, _Ctx, _SecObj] = Args,
             Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
             Reply =
-                case mango_selector:match(Selector, Struct) of
-                    true -> true;
-                    _ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
+                case mango_selector:match_failures(Selector, Struct) of
+                    [] ->
+                        true;
+                    Failures ->
+                        {[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]}
                 end,
             {reply, Reply, St}
     end;
diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl
index 9333822ba..3c58188cf 100644
--- a/src/mango/src/mango_selector.erl
+++ b/src/mango/src/mango_selector.erl
@@ -70,7 +70,7 @@ match(Selector, D) ->
 
 match_failures(Selector, D) ->
     couch_stats:increment_counter([mango, evaluate_selector]),
-    match_int(Selector, D, true).
+    [format_failure(F) || F <- match_int(Selector, D, true)].
 
 match_int(Selector, D) ->
     match_int(Selector, D, false).
@@ -585,8 +585,8 @@ match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{path = 
Path} = Ctx) when is_tup
         true -> [];
         false -> lists:flatten(KeyFailures)
     end;
-match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, Ctx) ->
-    [#failure{op = keyMapMatch, type = bad_value, ctx = Ctx}];
+match({[{<<"$keyMapMatch">>, _Arg}]}, Value, Ctx) ->
+    [#failure{op = keyMapMatch, type = bad_value, params = [Value], ctx = 
Ctx}];
 % Our comparison operators are fairly straight forward
 match({[{<<"$lt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
     compare(lt, Arg, Ctx, Cmp(Value, Arg) < 0);
@@ -728,6 +728,92 @@ compare(Op, Arg, #ctx{negate = Neg} = Ctx, Cond) ->
         _ -> [#failure{op = Op, params = [Arg], ctx = Ctx}]
     end.
 
+format_failure(#failure{op = Op, type = Type, params = Params, ctx = Ctx}) ->
+    Path = format_path(Ctx#ctx.path),
+    Msg = format_op(Op, Ctx#ctx.negate, Type, Params),
+    {[{<<"path">>, Path}, {<<"message">>, iolist_to_binary(Msg)}]}.
+
+format_op(Op, _, empty_list, _) ->
+    io_lib:format("operator $~p was invoked with an empty list", [Op]);
+format_op(Op, _, bad_value, [Value]) ->
+    io_lib:format("operator $~p was invoked with a bad value: ~s", [
+        Op, couch_util:json_encode(Value)
+    ]);
+format_op(_, _, not_found, []) ->
+    io_lib:format("must be present", []);
+format_op(_, _, bad_path, []) ->
+    io_lib:format("used an invalid path", []);
+format_op(eq, false, mismatch, [X]) ->
+    io_lib:format("must be equal to ~s", [couch_util:json_encode(X)]);
+format_op(eq, true, Type, Params) ->
+    format_op(ne, false, Type, Params);
+format_op(ne, false, mismatch, [X]) ->
+    io_lib:format("must not be equal to ~s", [couch_util:json_encode(X)]);
+format_op(ne, true, Type, Params) ->
+    format_op(eq, false, Type, Params);
+format_op(lt, false, mismatch, [X]) ->
+    io_lib:format("must be less than ~s", [couch_util:json_encode(X)]);
+format_op(lt, true, Type, Params) ->
+    format_op(gte, false, Type, Params);
+format_op(lte, false, mismatch, [X]) ->
+    io_lib:format("must be less than or equal to ~s", 
[couch_util:json_encode(X)]);
+format_op(lte, true, Type, Params) ->
+    format_op(gt, false, Type, Params);
+format_op(gt, false, mismatch, [X]) ->
+    io_lib:format("must be greater than ~s", [couch_util:json_encode(X)]);
+format_op(gt, true, Type, Params) ->
+    format_op(lte, false, Type, Params);
+format_op(gte, false, mismatch, [X]) ->
+    io_lib:format("must be greater than or equal to ~s", 
[couch_util:json_encode(X)]);
+format_op(gte, true, Type, Params) ->
+    format_op(lt, false, Type, Params);
+format_op(in, false, mismatch, [X]) ->
+    io_lib:format("must be one of ~s", [couch_util:json_encode(X)]);
+format_op(in, true, Type, Params) ->
+    format_op(nin, false, Type, Params);
+format_op(nin, false, mismatch, [X]) ->
+    io_lib:format("must not be one of ~s", [couch_util:json_encode(X)]);
+format_op(nin, true, Type, Params) ->
+    format_op(in, false, Type, Params);
+format_op(all, false, mismatch, [X]) ->
+    io_lib:format("must contain all the values in ~s", 
[couch_util:json_encode(X)]);
+format_op(all, true, mismatch, [X]) ->
+    io_lib:format("must not contain at least one of the values in ~s", 
[couch_util:json_encode(X)]);
+format_op(exists, false, mismatch, [true]) ->
+    io_lib:format("must be present", []);
+format_op(exists, false, mismatch, [false]) ->
+    io_lib:format("must not be present", []);
+format_op(exists, true, Type, [Exist]) ->
+    format_op(exists, false, Type, [not Exist]);
+format_op(type, false, mismatch, [Type]) ->
+    io_lib:format("must be of type '~s'", [Type]);
+format_op(type, true, mismatch, [Type]) ->
+    io_lib:format("must not be of type '~s'", [Type]);
+format_op(mod, false, mismatch, [D, R]) ->
+    io_lib:format("must leave a remainder of ~p when divided by ~p", [R, D]);
+format_op(mod, true, mismatch, [D, R]) ->
+    io_lib:format("must leave a remainder other than ~p when divided by ~p", 
[R, D]);
+format_op(regex, false, mismatch, [P]) ->
+    io_lib:format("must match the pattern '~s'", [P]);
+format_op(regex, true, mismatch, [P]) ->
+    io_lib:format("must not match the pattern '~s'", [P]);
+format_op(beginsWith, false, mismatch, [P]) ->
+    io_lib:format("must begin with '~s'", [P]);
+format_op(beginsWith, true, mismatch, [P]) ->
+    io_lib:format("must not begin with '~s'", [P]);
+format_op(size, false, mismatch, [N]) ->
+    io_lib:format("must contain ~p items", [N]);
+format_op(size, true, mismatch, [N]) ->
+    io_lib:format("must not contain ~p items", [N]).
+
+format_path([]) ->
+    [];
+format_path([Item | Rest]) when is_binary(Item) ->
+    {ok, Path} = mango_util:parse_field(Item),
+    format_path(Rest) ++ Path;
+format_path([Item | Rest]) when is_integer(Item) ->
+    format_path(Rest) ++ [Item].
+
 % Returns true if Selector requires all
 % fields in RequiredFields to exist in any matching documents.
 
@@ -1298,7 +1384,21 @@ match_lt_test() ->
         {false, [1, 2, 3]},
         {false, [1, 2, 4]},
         {false, [1, 3]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$lt">>, 5}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 6}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be less than 
5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$lt">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [3]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be greater than 
or equal to 5">>}]}],
+        FailsNeg
+    ).
 
 match_lte_test() ->
     check_selector({[{<<"$lte">>, 5}]}, [{true, 4}, {true, 5}, {false, 6}]),
@@ -1315,7 +1415,21 @@ match_lte_test() ->
         {true, [1, 2, 3]},
         {false, [1, 2, 4]},
         {false, [1, 3]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$lte">>, 5}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 6}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be less than or 
equal to 5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$lte">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [3]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be greater than 
5">>}]}],
+        FailsNeg
+    ).
 
 match_gt_test() ->
     check_selector({[{<<"$gt">>, 5}]}, [{false, 4}, {false, 5}, {true, 6}]),
@@ -1332,7 +1446,21 @@ match_gt_test() ->
         {false, [1, 2, 3]},
         {true, [1, 2, 4]},
         {true, [1, 3]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$gt">>, 5}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 4}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be greater than 
5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$gt">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [7]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be less than or 
equal to 5">>}]}],
+        FailsNeg
+    ).
 
 match_gte_test() ->
     check_selector({[{<<"$gte">>, 5}]}, [{false, 4}, {true, 5}, {true, 6}]),
@@ -1349,7 +1477,21 @@ match_gte_test() ->
         {true, [1, 2, 3]},
         {true, [1, 2, 4]},
         {true, [1, 3]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$gte">>, 5}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 4}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be greater than or 
equal to 5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$gte">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [5]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be less than 
5">>}]}],
+        FailsNeg
+    ).
 
 match_eq_test() ->
     check_selector({[{<<"$eq">>, 5}]}, [{true, 5}, {false, 6}]),
@@ -1367,7 +1509,21 @@ match_eq_test() ->
         {false, {[{<<"a">>, {[{<<"b">>, {[{<<"c">>, 8}]}}]}}]}},
         {false, {[{<<"a">>, {[{<<"b">>, {[{<<"d">>, 7}]}}]}}]}},
         {false, {[{<<"a">>, {[{<<"d">>, {[{<<"c">>, 7}]}}]}}]}}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, 5}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 4}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be equal to 
5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$eq">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [5]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must not be equal to 
5">>}]}],
+        FailsNeg
+    ).
 
 match_ne_test() ->
     check_selector({[{<<"$ne">>, 5}]}, [{false, 5}, {true, 6}]),
@@ -1395,7 +1551,21 @@ match_ne_test() ->
         {true, {[{<<"a">>, {[{<<"b">>, {[{<<"c">>, 8}]}}]}}]}},
         {true, {[{<<"a">>, {[{<<"b">>, {[{<<"d">>, 7}]}}]}}]}},
         {true, {[{<<"a">>, {[{<<"d">>, {[{<<"c">>, 7}]}}]}}]}}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$ne">>, 5}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 5}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must not be equal to 
5">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$ne">>, 5}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [4]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be equal to 
5">>}]}],
+        FailsNeg
+    ).
 
 match_in_test() ->
     check_selector({[{<<"$in">>, []}]}, [
@@ -1437,6 +1607,30 @@ match_in_test() ->
             {false, [[<<"nested">>], <<"list">>]},
             {true, [0, [[<<"nested">>], <<"list">>]]}
         ]
+    ),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$in">>, [42, false, [<<"bar">>]]}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 5}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be one of 
[42,false,[\"bar\"]]">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[
+            {<<"x">>,
+                {[{<<"$not">>, {[{<<"$allMatch">>, {[{<<"$in">>, [42, false, 
[<<"bar">>]]}]}}]}}]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [42]}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>, 0]},
+                {<<"message">>, <<"must not be one of [42,false,[\"bar\"]]">>}
+            ]}
+        ],
+        FailsNeg
     ).
 
 match_nin_test() ->
@@ -1479,6 +1673,35 @@ match_nin_test() ->
             {true, [[<<"nested">>], <<"list">>]},
             {false, [0, [[<<"nested">>], <<"list">>]]}
         ]
+    ),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$nin">>, [42, false, [<<"bar">>]]}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, false}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must not be one of [42,false,[\"bar\"]]">>}
+            ]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[
+            {<<"x">>,
+                {[{<<"$not">>, {[{<<"$allMatch">>, {[{<<"$nin">>, [42, false, 
[<<"bar">>]]}]}}]}}]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [true]}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>, 0]},
+                {<<"message">>, <<"must be one of [42,false,[\"bar\"]]">>}
+            ]}
+        ],
+        FailsNeg
     ).
 
 match_all_test() ->
@@ -1529,7 +1752,56 @@ match_all_test() ->
     check_selector({[{<<"$all">>, [{[{<<"a">>, 1}]}]}]}, [
         {true, [{[{<<"a">>, 1}]}]},
         {false, [{[{<<"a">>, 1}, {<<"b">>, 2}]}]}
-    ]).
+    ]),
+
+    SelEmpty = normalize({[{<<"x">>, {[{<<"$all">>, []}]}}]}),
+    FailsEmpty = match_failures(SelEmpty, {[{<<"x">>, 0}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"operator $all was invoked with an empty 
list">>}
+            ]}
+        ],
+        FailsEmpty
+    ),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$all">>, [1, 2, 3]}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, [2, 3]}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must contain all the values in [1,2,3]">>}
+            ]}
+        ],
+        Fails
+    ),
+
+    FailsBadVal = match_failures(Sel, {[{<<"x">>, 3}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must contain all the values in [1,2,3]">>}
+            ]}
+        ],
+        FailsBadVal
+    ),
+
+    SelNeg = normalize(
+        {[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, {[{<<"$all">>, [1, 2, 
3]}]}}]}}]}}]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [[1, 2, 3, 5]]}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>, 0]},
+                {<<"message">>, <<"must not contain at least one of the values 
in [1,2,3]">>}
+            ]}
+        ],
+        FailsNeg
+    ).
 
 match_exists_test() ->
     check_selector({[{<<"x">>, {[{<<"$exists">>, true}]}}]}, [
@@ -1567,6 +1839,25 @@ match_exists_test() ->
             {false, {[{<<"x">>, 0}]}},
             {true, {[]}}
         ]
+    ),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$exists">>, true}]}}]}),
+    Fails = match_failures(Sel, {[]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be present">>}]}],
+        Fails
+    ),
+
+    SelNeg1 = normalize(
+        {[
+            {<<"x">>,
+                {[{<<"$not">>, {[{<<"$allMatch">>, {[{<<"y">>, 
{[{<<"$exists">>, true}]}}]}}]}}]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg1, {[{<<"x">>, [{[{<<"y">>, 0}]}]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0, <<"y">>]}, {<<"message">>, <<"must not be 
present">>}]}],
+        FailsNeg
     ).
 
 match_type_test() ->
@@ -1609,7 +1900,21 @@ match_type_test() ->
         {true, {[{<<"a">>, 1}]}},
         {false, [{<<"a">>, 1}]},
         {false, null}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$type">>, <<"number">>}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, true}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be of type 
'number'">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$type">>, 
<<"number">>}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, 8}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must not be of type 
'number'">>}]}],
+        FailsNeg
+    ).
 
 match_regex_test() ->
     check_selector({[{<<"$regex">>, <<"^[0-9a-f]+$">>}]}, [
@@ -1617,7 +1922,26 @@ match_regex_test() ->
         {true, <<"3a0df5e">>},
         {false, <<"3a0gf5e">>},
         {false, 42}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$regex">>, <<"^[0-9a-f]+$">>}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, <<"hello">>}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must match the pattern 
'^[0-9a-f]+$'">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$regex">>, 
<<"^[0-9a-f]+$">>}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, <<"abc123">>}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must not match the pattern '^[0-9a-f]+$'">>}
+            ]}
+        ],
+        FailsNeg
+    ).
 
 match_beginswith_test() ->
     check_selector({[{<<"$beginsWith">>, <<"foo">>}]}, [
@@ -1627,7 +1951,21 @@ match_beginswith_test() ->
         {false, <<"more food">>},
         {false, <<"fo">>},
         {false, 42}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$beginsWith">>, <<"foo">>}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, <<"hello">>}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must begin with 
'foo'">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$beginsWith">>, 
<<"foo">>}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, <<"fools">>}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must not begin with 
'foo'">>}]}],
+        FailsNeg
+    ).
 
 match_mod_test() ->
     check_selector({[{<<"$mod">>, [28, 1]}]}, [
@@ -1636,7 +1974,31 @@ match_mod_test() ->
         {true, 57},
         {false, 58},
         {false, <<"57">>}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$mod">>, [28, 1]}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, 27}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must leave a remainder of 1 when divided by 
28">>}
+            ]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$mod">>, [28, 
1]}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, 29}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>]},
+                {<<"message">>, <<"must leave a remainder other than 1 when 
divided by 28">>}
+            ]}
+        ],
+        FailsNeg
+    ).
 
 match_size_test() ->
     check_selector({[{<<"$size">>, 3}]}, [
@@ -1645,7 +2007,21 @@ match_size_test() ->
         {true, [0, 0, 0]},
         {false, [0, 0]},
         {false, [0, 0, 0, 0]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$size">>, 3}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, [0, 1]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must contain 3 
items">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$size">>, 3}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [0, 1, 2]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must not contain 3 
items">>}]}],
+        FailsNeg
+    ).
 
 match_allmatch_test() ->
     % $allMatch is defined to return false for empty lists
@@ -1663,7 +2039,21 @@ match_allmatch_test() ->
         {false, [0]},
         {true, [1]},
         {true, [0, 1]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$allMatch">>, {[{<<"$eq">>, 0}]}}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, [0, 1]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 1]}, {<<"message">>, <<"must be equal to 
0">>}]}],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$allMatch">>, 
{[{<<"$eq">>, 0}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [0]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must not be equal to 
0">>}]}],
+        FailsNeg
+    ).
 
 match_elemmatch_test() ->
     check_selector({[{<<"$elemMatch">>, {[{<<"$eq">>, 0}]}}]}, [
@@ -1671,7 +2061,24 @@ match_elemmatch_test() ->
         {true, [0]},
         {false, [1]},
         {true, [0, 1]}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$elemMatch">>, {[{<<"$eq">>, 0}]}}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, [1, 2]}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be equal to 
0">>}]},
+            {[{<<"path">>, [<<"x">>, 1]}, {<<"message">>, <<"must be equal to 
0">>}]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, {[{<<"$elemMatch">>, 
{[{<<"$eq">>, 0}]}}]}}]}}]}),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [0]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must not be equal to 
0">>}]}],
+        FailsNeg
+    ).
 
 match_keymapmatch_test() ->
     check_selector({[{<<"$keyMapMatch">>, {[{<<"$regex">>, 
<<"^[a-z]+$">>}]}}]}, [
@@ -1680,7 +2087,26 @@ match_keymapmatch_test() ->
         {true, {[{<<"a">>, 1}, {<<"b4">>, 2}]}},
         {false, {[{<<"b4">>, 2}]}},
         {false, {[]}}
-    ]).
+    ]),
+
+    Sel = normalize({[{<<"x">>, {[{<<"$keyMapMatch">>, {[{<<"$beginsWith">>, 
<<"a">>}]}}]}}]}),
+    Fails = match_failures(Sel, {[{<<"x">>, {[{<<"bravo">>, 0}, 
{<<"charlie">>, 1}]}}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>, <<"bravo">>]}, {<<"message">>, <<"must 
begin with 'a'">>}]},
+            {[{<<"path">>, [<<"x">>, <<"charlie">>]}, {<<"message">>, <<"must 
begin with 'a'">>}]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[{<<"x">>, {[{<<"$not">>, {[{<<"$keyMapMatch">>, 
{[{<<"$beginsWith">>, <<"a">>}]}}]}}]}}]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, {[{<<"alfa">>, 0}]}}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, <<"alfa">>]}, {<<"message">>, <<"must not 
begin with 'a'">>}]}],
+        FailsNeg
+    ).
 
 match_object_test() ->
     Doc1 = {[]},
@@ -1749,7 +2175,13 @@ match_object_test() ->
     ?assertEqual(false, match_int(SelShort, Doc2)),
     ?assertEqual(false, match_int(SelShort, Doc3)),
     ?assertEqual(true, match_int(SelShort, Doc4)),
-    ?assertEqual(false, match_int(SelShort, Doc5)).
+    ?assertEqual(false, match_int(SelShort, Doc5)),
+
+    Fails = match_failures(SelShort, Doc3),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, <<"b">>]}, {<<"message">>, <<"must be 
present">>}]}],
+        Fails
+    ).
 
 match_and_test() ->
     % $and with an empty array matches anything
@@ -1807,7 +2239,40 @@ match_and_test() ->
     ?assertEqual(false, match_int(SelNotMulti, {[{<<"x">>, 6}]})),
     ?assertEqual(true, match_int(SelNotMulti, {[{<<"x">>, 2}]})),
     ?assertEqual(true, match_int(SelNotMulti, {[{<<"x">>, 9}]})),
-    ?assertEqual(false, match_int(SelNotMulti, {[]})).
+    ?assertEqual(false, match_int(SelNotMulti, {[]})),
+
+    Fails = match_failures(SelNotMulti, {[{<<"x">>, 6}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be less than or 
equal to 3">>}]},
+            {[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be greater than 
or equal to 7">>}]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[
+            {<<"x">>,
+                {[
+                    {<<"$not">>,
+                        {[
+                            {<<"$allMatch">>,
+                                {[{<<"$and">>, [{[{<<"$gt">>, 3}]}, 
{[{<<"$lt">>, 5}]}]}]}}
+                        ]}}
+                ]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [4]}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be less than 
or equal to 3">>}]},
+            {[
+                {<<"path">>, [<<"x">>, 0]},
+                {<<"message">>, <<"must be greater than or equal to 5">>}
+            ]}
+        ],
+        FailsNeg
+    ).
 
 match_or_test() ->
     % $or with an empty array matches anything
@@ -1860,7 +2325,33 @@ match_or_test() ->
     ?assertEqual(true, match_int(SelNotMulti, {[{<<"x">>, 6}]})),
     ?assertEqual(false, match_int(SelNotMulti, {[{<<"x">>, 2}]})),
     ?assertEqual(false, match_int(SelNotMulti, {[{<<"x">>, 9}]})),
-    ?assertEqual(false, match_int(SelNotMulti, {[]})).
+    ?assertEqual(false, match_int(SelNotMulti, {[]})),
+
+    Fails = match_failures(SelNotMulti, {[{<<"x">>, 2}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be greater than 
or equal to 3">>}]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[
+            {<<"x">>,
+                {[
+                    {<<"$not">>,
+                        {[
+                            {<<"$allMatch">>,
+                                {[{<<"$or">>, [{[{<<"$lt">>, 3}]}, 
{[{<<"$gt">>, 5}]}]}]}}
+                        ]}}
+                ]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [0]}]}),
+    ?assertEqual(
+        [{[{<<"path">>, [<<"x">>, 0]}, {<<"message">>, <<"must be greater than 
or equal to 3">>}]}],
+        FailsNeg
+    ).
 
 match_nor_test() ->
     % $nor with an empty array matches anything
@@ -1885,7 +2376,42 @@ match_nor_test() ->
     ?assertEqual(true, match_int(SelMulti, {[{<<"x">>, 6}]})),
     ?assertEqual(false, match_int(SelMulti, {[{<<"x">>, 2}]})),
     ?assertEqual(false, match_int(SelMulti, {[{<<"x">>, 9}]})),
-    ?assertEqual(false, match_int(SelMulti, {[]})).
+    ?assertEqual(false, match_int(SelMulti, {[]})),
+
+    Fails = match_failures(SelMulti, {[{<<"x">>, 2}]}),
+    ?assertEqual(
+        [
+            {[{<<"path">>, [<<"x">>]}, {<<"message">>, <<"must be greater than 
or equal to 3">>}]}
+        ],
+        Fails
+    ),
+
+    SelNeg = normalize(
+        {[
+            {<<"x">>,
+                {[
+                    {<<"$allMatch">>,
+                        {[
+                            {<<"y">>,
+                                {[
+                                    {<<"$nor">>, [
+                                        {[{<<"$lt">>, 3}]}, {[{<<"$gt">>, 5}]}
+                                    ]}
+                                ]}}
+                        ]}}
+                ]}}
+        ]}
+    ),
+    FailsNeg = match_failures(SelNeg, {[{<<"x">>, [{[{<<"y">>, 6}]}]}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"x">>, 0, <<"y">>]},
+                {<<"message">>, <<"must be less than or equal to 5">>}
+            ]}
+        ],
+        FailsNeg
+    ).
 
 normalize_nor_test() ->
     Sel1 = normalize(
@@ -1971,7 +2497,7 @@ match_failures_object_test() ->
         ]}
     ),
     ?assertMatch(
-        [#failure{op = eq, type = mismatch, params = [1], ctx = #ctx{path = 
[<<"a">>]}}],
+        [{[{<<"path">>, [<<"a">>]}, {<<"message">>, <<"must be equal to 
1">>}]}],
         Fails1
     ),
 
@@ -1983,7 +2509,7 @@ match_failures_object_test() ->
         ]}
     ),
     ?assertMatch(
-        [#failure{op = eq, type = mismatch, params = [3], ctx = #ctx{path = 
[<<"c">>, <<"b">>]}}],
+        [{[{<<"path">>, [<<"b">>, <<"c">>]}, {<<"message">>, <<"must be equal 
to 3">>}]}],
         Fails2
     ),
 
@@ -1996,8 +2522,8 @@ match_failures_object_test() ->
     ),
     ?assertMatch(
         [
-            #failure{op = eq, type = mismatch, params = [1], ctx = #ctx{path = 
[<<"a">>]}},
-            #failure{op = eq, type = mismatch, params = [3], ctx = #ctx{path = 
[<<"c">>, <<"b">>]}}
+            {[{<<"path">>, [<<"a">>]}, {<<"message">>, <<"must be equal to 
1">>}]},
+            {[{<<"path">>, [<<"b">>, <<"c">>]}, {<<"message">>, <<"must be 
equal to 3">>}]}
         ],
         Fails3
     ).
@@ -2012,28 +2538,38 @@ match_failures_elemmatch_test() ->
         ]}
     ),
 
-    Fails0 = match_failures(
-        SelElemMatch, {[{<<"a">>, [5, 3, 2]}]}
-    ),
+    Fails0 = match_failures(SelElemMatch, {[{<<"a">>, [5, 3, 2]}]}),
     ?assertEqual([], Fails0),
 
-    Fails1 = match_failures(
-        SelElemMatch, {[{<<"a">>, []}]}
-    ),
+    Fails1 = match_failures(SelElemMatch, {[{<<"a">>, []}]}),
     ?assertMatch(
-        [#failure{op = elemMatch, type = empty_list, params = [], ctx = 
#ctx{path = [<<"a">>]}}],
+        [
+            {[
+                {<<"path">>, [<<"a">>]},
+                {<<"message">>, <<"operator $elemMatch was invoked with an 
empty list">>}
+            ]}
+        ],
         Fails1
     ),
 
-    Fails2 = match_failures(
-        SelElemMatch, {[{<<"a">>, [3, 2]}]}
-    ),
+    Fails2 = match_failures(SelElemMatch, {[{<<"a">>, [3, 2]}]}),
     ?assertMatch(
         [
-            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[0, <<"a">>]}},
-            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[1, <<"a">>]}}
+            {[{<<"path">>, [<<"a">>, 0]}, {<<"message">>, <<"must be greater 
than 4">>}]},
+            {[{<<"path">>, [<<"a">>, 1]}, {<<"message">>, <<"must be greater 
than 4">>}]}
         ],
         Fails2
+    ),
+
+    Fails3 = match_failures(SelElemMatch, {[{<<"a">>, 3}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"a">>]},
+                {<<"message">>, <<"operator $elemMatch was invoked with a bad 
value: 3">>}
+            ]}
+        ],
+        Fails3
     ).
 
 match_failures_allmatch_test() ->
@@ -2046,28 +2582,33 @@ match_failures_allmatch_test() ->
         ]}
     ),
 
-    Fails0 = match_failures(
-        SelAllMatch, {[{<<"a">>, [5]}]}
-    ),
+    Fails0 = match_failures(SelAllMatch, {[{<<"a">>, [5]}]}),
     ?assertEqual([], Fails0),
 
-    Fails1 = match_failures(
-        SelAllMatch, {[{<<"a">>, [4]}]}
-    ),
+    Fails1 = match_failures(SelAllMatch, {[{<<"a">>, [4]}]}),
     ?assertMatch(
-        [#failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[0, <<"a">>]}}],
+        [{[{<<"path">>, [<<"a">>, 0]}, {<<"message">>, <<"must be greater than 
4">>}]}],
         Fails1
     ),
 
-    Fails2 = match_failures(
-        SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}
-    ),
+    Fails2 = match_failures(SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}),
     ?assertMatch(
         [
-            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[2, <<"a">>]}},
-            #failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = 
[4, <<"a">>]}}
+            {[{<<"path">>, [<<"a">>, 2]}, {<<"message">>, <<"must be greater 
than 4">>}]},
+            {[{<<"path">>, [<<"a">>, 4]}, {<<"message">>, <<"must be greater 
than 4">>}]}
         ],
         Fails2
+    ),
+
+    Fails3 = match_failures(SelAllMatch, {[{<<"a">>, 3}]}),
+    ?assertEqual(
+        [
+            {[
+                {<<"path">>, [<<"a">>]},
+                {<<"message">>, <<"operator $allMatch was invoked with a bad 
value: 3">>}
+            ]}
+        ],
+        Fails3
     ).
 
 match_failures_allmatch_object_test() ->
@@ -2080,22 +2621,16 @@ match_failures_allmatch_object_test() ->
         ]}
     ),
 
-    Fails0 = match_failures(
-        SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}]}]}}]}
-    ),
+    Fails0 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 
5}]}]}]}}]}),
     ?assertEqual([], Fails0),
 
-    Fails1 = match_failures(
-        SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 4}]}]}]}}]}
-    ),
+    Fails1 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 
4}]}]}]}}]}),
     ?assertMatch(
         [
-            #failure{
-                op = gt,
-                type = mismatch,
-                params = [4],
-                ctx = #ctx{path = [<<"c">>, 0, <<"b">>, <<"a">>]}
-            }
+            {[
+                {<<"path">>, [<<"a">>, <<"b">>, 0, <<"c">>]},
+                {<<"message">>, <<"must be greater than 4">>}
+            ]}
         ],
         Fails1
     ),
@@ -2106,36 +2641,31 @@ match_failures_allmatch_object_test() ->
     ),
     ?assertMatch(
         [
-            #failure{
-                op = gt,
-                type = mismatch,
-                params = [4],
-                ctx = #ctx{path = [<<"c">>, 2, <<"b">>, <<"a">>]}
-            }
+            {[
+                {<<"path">>, [<<"a">>, <<"b">>, 2, <<"c">>]},
+                {<<"message">>, <<"must be greater than 4">>}
+            ]}
         ],
         Fails2
     ),
 
-    Fails3 = match_failures(
-        SelAllMatch,
-        {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 1}]}, {[]}]}]}}]}
-    ),
+    Fails3 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 
1}]}, {[]}]}]}}]}),
     ?assertMatch(
         [
-            #failure{
-                op = gt,
-                type = mismatch,
-                params = [4],
-                ctx = #ctx{path = [<<"c">>, 0, <<"b">>, <<"a">>]}
-            },
-            #failure{
-                op = field,
-                type = not_found,
-                params = [],
-                ctx = #ctx{path = [<<"c">>, 1, <<"b">>, <<"a">>]}
-            }
+            {[
+                {<<"path">>, [<<"a">>, <<"b">>, 0, <<"c">>]},
+                {<<"message">>, <<"must be greater than 4">>}
+            ]},
+            {[{<<"path">>, [<<"a">>, <<"b">>, 1, <<"c">>]}, {<<"message">>, 
<<"must be present">>}]}
         ],
         Fails3
     ).
 
+format_path_test() ->
+    ?assertEqual([], format_path([])),
+    ?assertEqual([<<"a">>], format_path([<<"a">>])),
+    ?assertEqual([<<"a">>, <<"b">>], format_path([<<"b">>, <<"a">>])),
+    ?assertEqual([<<"a">>, <<"b">>, <<"c">>], format_path([<<"b.c">>, 
<<"a">>])),
+    ?assertEqual([<<"a">>, 42, <<"b">>, <<"c">>], format_path([<<"b.c">>, 42, 
<<"a">>])).
+
 -endif.
diff --git a/test/elixir/test/validate_doc_update_test.exs 
b/test/elixir/test/validate_doc_update_test.exs
index 93ed8f177..216f0b6cf 100644
--- a/test/elixir/test/validate_doc_update_test.exs
+++ b/test/elixir/test/validate_doc_update_test.exs
@@ -105,6 +105,11 @@ defmodule ValidateDocUpdateTest do
     resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
     assert resp.status_code == 403
     assert resp.body["error"] == "forbidden"
+    assert resp.body["reason"] == %{
+      "failures" => [
+        %{"path" => ["newDoc", "type"], "message" => "must be present"}
+      ]
+    }
   end
 
   @tag :with_db

Reply via email to