Skip to content

Commit fa65b74

Browse files
committed
[wip] return failure report from mango VDU on PUT /db/doc
1 parent 1c51d90 commit fa65b74

4 files changed

Lines changed: 136 additions & 32 deletions

File tree

src/couch/src/couch_query_servers.erl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -481,6 +481,8 @@ validate_doc_update(Db, DDoc, EditDoc, DiskDoc, Ctx, SecObj) ->
481481
case Resp of
482482
ok ->
483483
ok;
484+
{[{<<"forbidden">>, Message}, {<<"failures">>, Failures}]} ->
485+
throw({forbidden, Message, Failures});
484486
{[{<<"forbidden">>, Message}]} ->
485487
throw({forbidden, Message});
486488
{[{<<"unauthorized">>, Message}]} ->

src/mango/src/mango_native_proc.erl

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,14 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _
115115
[NewDoc, OldDoc, _Ctx, _SecObj] = Args,
116116
Struct = {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, OldDoc}]},
117117
Reply =
118-
case mango_selector:match(Selector, Struct) of
119-
true -> true;
120-
_ -> {[{<<"forbidden">>, <<"document is not valid">>}]}
118+
case mango_selector:match_failures(Selector, Struct) of
119+
[] ->
120+
true;
121+
Failures ->
122+
{[
123+
{<<"forbidden">>, <<"forbidden">>},
124+
{<<"failures">>, Failures}
125+
]}
121126
end,
122127
{reply, Reply, St}
123128
end;

src/mango/src/mango_selector.erl

Lines changed: 123 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ match(Selector, D) ->
7070

7171
match_failures(Selector, D) ->
7272
couch_stats:increment_counter([mango, evaluate_selector]),
73-
match_int(Selector, D, true).
73+
[format_failure(F) || F <- match_int(Selector, D, true)].
7474

7575
match_int(Selector, D) ->
7676
match_int(Selector, D, false).
@@ -575,8 +575,8 @@ match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{path = Path} = Ctx) when is_tup
575575
true -> [];
576576
false -> lists:flatten(KeyFailures)
577577
end;
578-
match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, Ctx) ->
579-
[#failure{op = keyMapMatch, type = bad_value, ctx = Ctx}];
578+
match({[{<<"$keyMapMatch">>, _Arg}]}, Value, Ctx) ->
579+
[#failure{op = keyMapMatch, type = bad_value, params = [Value], ctx = Ctx}];
580580
% Our comparison operators are fairly straight forward
581581
match({[{<<"$lt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) ->
582582
compare(lt, Arg, Ctx, Cmp(Value, Arg) < 0);
@@ -712,6 +712,88 @@ compare(Op, Arg, #ctx{negate = Neg} = Ctx, Cond) ->
712712
_ -> [#failure{op = Op, params = [Arg], ctx = Ctx}]
713713
end.
714714

715+
format_failure(#failure{op = Op, type = Type, params = Params, ctx = Ctx}) ->
716+
Path = format_path(Ctx#ctx.path),
717+
Msg = format_op(Op, Ctx#ctx.negate, Type, Params),
718+
{[{<<"path">>, Path}, {<<"message">>, list_to_binary(Msg)}]}.
719+
720+
format_op(Op, _, empty_list, _) ->
721+
io_lib:format("operator $~p was invoked with an empty list", [Op]);
722+
format_op(Op, _, bad_value, [Value]) ->
723+
io_lib:format("operator $~p was invoked with a bad value: ~s", [Op, jiffy:encode(Value)]);
724+
format_op(_, _, not_found, []) ->
725+
io_lib:format("must be present", []);
726+
format_op(_, _, bad_path, []) ->
727+
io_lib:format("used an invalid path", []);
728+
format_op(eq, false, mismatch, [X]) ->
729+
io_lib:format("must be equal to ~s", [jiffy:encode(X)]);
730+
format_op(ne, false, mismatch, [X]) ->
731+
io_lib:format("must not be equal to ~s", [jiffy:encode(X)]);
732+
format_op(lt, false, mismatch, [X]) ->
733+
io_lib:format("must be less than ~s", [jiffy:encode(X)]);
734+
format_op(lte, false, mismatch, [X]) ->
735+
io_lib:format("must be less than or equal to ~s", [jiffy:encode(X)]);
736+
format_op(gt, false, mismatch, [X]) ->
737+
io_lib:format("must be greater than ~s", [jiffy:encode(X)]);
738+
format_op(gte, false, mismatch, [X]) ->
739+
io_lib:format("must be greater than or equal to ~s", [jiffy:encode(X)]);
740+
format_op(in, false, mismatch, [X]) ->
741+
io_lib:format("must be one of ~s", [jiffy:encode(X)]);
742+
format_op(nin, false, mismatch, [X]) ->
743+
io_lib:format("must not be one of ~s", [jiffy:encode(X)]);
744+
format_op(all, false, mismatch, [X]) ->
745+
io_lib:format("must contain all the values in ~s", [jiffy:encode(X)]);
746+
format_op(exists, false, mismatch, [true]) ->
747+
io_lib:format("must be present", []);
748+
format_op(exists, false, mismatch, [false]) ->
749+
io_lib:format("must not be present", []);
750+
format_op(type, false, mismatch, [Type]) ->
751+
io_lib:format("must be of type '~s'", [Type]);
752+
format_op(type, true, mismatch, [Type]) ->
753+
io_lib:format("must not be of type '~s'", [Type]);
754+
format_op(mod, false, mismatch, [D, R]) ->
755+
io_lib:format("must leave a remainder of ~p when divided by ~p", [R, D]);
756+
format_op(mod, true, mismatch, [D, R]) ->
757+
io_lib:format("must leave a remainder other than ~p when divided by ~p", [R, D]);
758+
format_op(regex, false, mismatch, [P]) ->
759+
io_lib:format("must match the pattern '~s'", [P]);
760+
format_op(regex, true, mismatch, [P]) ->
761+
io_lib:format("must not match the pattern '~s'", [P]);
762+
format_op(beginsWith, false, mismatch, [P]) ->
763+
io_lib:format("must begin with '~s'", [P]);
764+
format_op(beginsWith, true, mismatch, [P]) ->
765+
io_lib:format("must not begin with '~s'", [P]);
766+
format_op(size, false, mismatch, [N]) ->
767+
io_lib:format("must contain ~p items", [N]);
768+
format_op(size, true, mismatch, [N]) ->
769+
io_lib:format("must not contain ~p items", [N]);
770+
format_op(eq, true, Type, Params) ->
771+
format_op(ne, false, Type, Params);
772+
format_op(ne, true, Type, Params) ->
773+
format_op(eq, false, Type, Params);
774+
format_op(lt, true, Type, Params) ->
775+
format_op(gte, false, Type, Params);
776+
format_op(lte, true, Type, Params) ->
777+
format_op(gt, false, Type, Params);
778+
format_op(gt, true, Type, Params) ->
779+
format_op(lte, false, Type, Params);
780+
format_op(gte, true, Type, Params) ->
781+
format_op(le, false, Type, Params);
782+
format_op(in, true, Type, Params) ->
783+
format_op(nin, false, Type, Params);
784+
format_op(nin, true, Type, Params) ->
785+
format_op(in, false, Type, Params);
786+
format_op(exists, true, Type, [Exist]) ->
787+
format_op(exists, false, Type, [not Exist]).
788+
789+
format_path([]) ->
790+
[];
791+
format_path([Item | Rest]) when is_binary(Item) ->
792+
{ok, Path} = mango_util:parse_field(Item),
793+
format_path(Rest) ++ Path;
794+
format_path([Item | Rest]) when is_integer(Item) ->
795+
format_path(Rest) ++ [list_to_binary(integer_to_list(Item))].
796+
715797
% Returns true if Selector requires all
716798
% fields in RequiredFields to exist in any matching documents.
717799

@@ -1869,33 +1951,36 @@ match_failures_object_test() ->
18691951
]}
18701952
),
18711953

1872-
Fails0 = match_failures(
1954+
Fails0 = match_int(
18731955
Selector,
18741956
{[
18751957
{<<"a">>, 1},
18761958
{<<"b">>, {[{<<"c">>, 3}]}}
1877-
]}
1959+
]},
1960+
true
18781961
),
18791962
?assertEqual([], Fails0),
18801963

1881-
Fails1 = match_failures(
1964+
Fails1 = match_int(
18821965
Selector,
18831966
{[
18841967
{<<"a">>, 0},
18851968
{<<"b">>, {[{<<"c">>, 3}]}}
1886-
]}
1969+
]},
1970+
true
18871971
),
18881972
?assertMatch(
18891973
[#failure{op = eq, type = mismatch, params = [1], ctx = #ctx{path = [<<"a">>]}}],
18901974
Fails1
18911975
),
18921976

1893-
Fails2 = match_failures(
1977+
Fails2 = match_int(
18941978
Selector,
18951979
{[
18961980
{<<"a">>, 1},
18971981
{<<"b">>, {[{<<"c">>, 4}]}}
1898-
]}
1982+
]},
1983+
true
18991984
),
19001985
?assertMatch(
19011986
[#failure{op = eq, type = mismatch, params = [3], ctx = #ctx{path = [<<"b.c">>]}}],
@@ -1912,21 +1997,21 @@ match_failures_elemmatch_test() ->
19121997
]}
19131998
),
19141999

1915-
Fails0 = match_failures(
1916-
SelElemMatch, {[{<<"a">>, [5, 3, 2]}]}
2000+
Fails0 = match_int(
2001+
SelElemMatch, {[{<<"a">>, [5, 3, 2]}]}, true
19172002
),
19182003
?assertEqual([], Fails0),
19192004

1920-
Fails1 = match_failures(
1921-
SelElemMatch, {[{<<"a">>, []}]}
2005+
Fails1 = match_int(
2006+
SelElemMatch, {[{<<"a">>, []}]}, true
19222007
),
19232008
?assertMatch(
19242009
[#failure{op = elemMatch, type = empty_list, params = [], ctx = #ctx{path = [<<"a">>]}}],
19252010
Fails1
19262011
),
19272012

1928-
Fails2 = match_failures(
1929-
SelElemMatch, {[{<<"a">>, [3, 2]}]}
2013+
Fails2 = match_int(
2014+
SelElemMatch, {[{<<"a">>, [3, 2]}]}, true
19302015
),
19312016
?assertMatch(
19322017
[
@@ -1946,21 +2031,21 @@ match_failures_allmatch_test() ->
19462031
]}
19472032
),
19482033

1949-
Fails0 = match_failures(
1950-
SelAllMatch, {[{<<"a">>, [5]}]}
2034+
Fails0 = match_int(
2035+
SelAllMatch, {[{<<"a">>, [5]}]}, true
19512036
),
19522037
?assertEqual([], Fails0),
19532038

1954-
Fails1 = match_failures(
1955-
SelAllMatch, {[{<<"a">>, [4]}]}
2039+
Fails1 = match_int(
2040+
SelAllMatch, {[{<<"a">>, [4]}]}, true
19562041
),
19572042
?assertMatch(
19582043
[#failure{op = gt, type = mismatch, params = [4], ctx = #ctx{path = [0, <<"a">>]}}],
19592044
Fails1
19602045
),
19612046

1962-
Fails2 = match_failures(
1963-
SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}
2047+
Fails2 = match_int(
2048+
SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}, true
19642049
),
19652050
?assertMatch(
19662051
[
@@ -1980,13 +2065,13 @@ match_failures_allmatch_object_test() ->
19802065
]}
19812066
),
19822067

1983-
Fails0 = match_failures(
1984-
SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}]}]}}]}
2068+
Fails0 = match_int(
2069+
SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}]}]}}]}, true
19852070
),
19862071
?assertEqual([], Fails0),
19872072

1988-
Fails1 = match_failures(
1989-
SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 4}]}]}]}}]}
2073+
Fails1 = match_int(
2074+
SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 4}]}]}]}}]}, true
19902075
),
19912076
?assertMatch(
19922077
[
@@ -1997,9 +2082,10 @@ match_failures_allmatch_object_test() ->
19972082
Fails1
19982083
),
19992084

2000-
Fails2 = match_failures(
2085+
Fails2 = match_int(
20012086
SelAllMatch,
2002-
{[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}, {[{<<"c">>, 6}]}, {[{<<"c">>, 3}]}]}]}}]}
2087+
{[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}, {[{<<"c">>, 6}]}, {[{<<"c">>, 3}]}]}]}}]},
2088+
true
20032089
),
20042090
?assertMatch(
20052091
[
@@ -2010,9 +2096,10 @@ match_failures_allmatch_object_test() ->
20102096
Fails2
20112097
),
20122098

2013-
Fails3 = match_failures(
2099+
Fails3 = match_int(
20142100
SelAllMatch,
2015-
{[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 1}]}, {[]}]}]}}]}
2101+
{[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 1}]}, {[]}]}]}}]},
2102+
true
20162103
),
20172104
?assertMatch(
20182105
[
@@ -2029,6 +2116,13 @@ match_failures_allmatch_object_test() ->
20292116
Fails3
20302117
).
20312118

2119+
format_path_test() ->
2120+
?assertEqual([], format_path([])),
2121+
?assertEqual([<<"a">>], format_path([<<"a">>])),
2122+
?assertEqual([<<"a">>, <<"b">>], format_path([<<"b">>, <<"a">>])),
2123+
?assertEqual([<<"a">>, <<"b">>, <<"c">>], format_path([<<"b.c">>, <<"a">>])),
2124+
?assertEqual([<<"a">>, <<"42">>, <<"b">>, <<"c">>], format_path([<<"b.c">>, 42, <<"a">>])).
2125+
20322126
bench(Name, Selector, Doc) ->
20332127
Sel1 = normalize(Selector),
20342128
[Normal, Verbose] = erlperf:compare(

test/elixir/test/validate_doc_update_test.exs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,9 @@ defmodule ValidateDocUpdateTest do
105105
resp = Couch.put("/#{db}/doc", body: %{"no" => "type"})
106106
assert resp.status_code == 403
107107
assert resp.body["error"] == "forbidden"
108+
assert resp.body["reason"] == [
109+
%{"path" => ["newDoc", "type"], "message" => "must be present"}
110+
]
108111
end
109112

110113
@tag :with_db

0 commit comments

Comments
 (0)