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
