diff --git a/rel/overlay/etc/default.ini b/rel/overlay/etc/default.ini index aac8ff5845d..36bcbda1fa6 100644 --- a/rel/overlay/etc/default.ini +++ b/rel/overlay/etc/default.ini @@ -132,6 +132,13 @@ view_index_dir = {{view_index_dir}} ; Javascript engine. The choices are: spidermonkey and quickjs ;js_engine = spidermonkey +; When set to "true", the `validate_doc_update` field will be validated when +; design documents are updated. For `javascript` design docs, the field must +; contain a well-formed JavaScript function, and for `query` design docs it +; must contain a Mango selector that is correctly structured to validate +; document updates. +validate_vdu = true + ; Use cfile. This is a C-based file I/O module that can execute parallel file ; read calls. The regular Erlang VM file module, at least as of OTP 28 forces ; all file operations to go through a single controlling process which can diff --git a/share/server/dispatch-quickjs.js b/share/server/dispatch-quickjs.js index 2f05118b313..fc5fccdd854 100644 --- a/share/server/dispatch-quickjs.js +++ b/share/server/dispatch-quickjs.js @@ -162,6 +162,9 @@ globalThis.dispatch = function(line) { case "reset": State.reset.apply(null, cmd); break; + case "validate_fun": + State.validateFun.apply(null, cmd); + break; case "add_fun": State.addFun.apply(null, cmd); break; diff --git a/share/server/loop.js b/share/server/loop.js index 1a5cab843c2..c3872aeca85 100644 --- a/share/server/loop.js +++ b/share/server/loop.js @@ -126,6 +126,7 @@ var Loop = function() { "ddoc" : DDoc.ddoc, // "view" : Views.handler, "reset" : State.reset, + "validate_fun": State.validateFun, "add_fun" : State.addFun, "add_lib" : State.addLib, "map_doc" : Views.mapDoc, diff --git a/share/server/state.js b/share/server/state.js index a9b2f7ea041..5cefcfc8cd2 100644 --- a/share/server/state.js +++ b/share/server/state.js @@ -10,6 +10,18 @@ // License for the specific language governing permissions and limitations under // the License. +var makefun = function(newFun, option) { + switch (option) { + case 'nouveau': + var sandbox = create_nouveau_sandbox(); + break; + default: + var sandbox = create_dreyfus_sandbox(); + break; + } + return Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox); +} + var State = { reset : function(config) { // clear the globals and run gc @@ -19,17 +31,15 @@ var State = { gc(); print("true"); // indicates success }, + validateFun : function(newFun, option) { + // Validate a function but do not store it + makefun(newFun, option); + print("true"); + }, addFun : function(newFun, option) { // Compile to a function and add it to funs array - switch (option) { - case 'nouveau': - var sandbox = create_nouveau_sandbox(); - break; - default: - var sandbox = create_dreyfus_sandbox(); - break; - } - State.funs.push(Couch.compileFunction(newFun, {views : {lib : State.lib}}, undefined, sandbox)); + var fun = makefun(newFun, option); + State.funs.push(fun); print("true"); }, addLib : function(lib) { diff --git a/src/couch/src/couch_query_servers.erl b/src/couch/src/couch_query_servers.erl index 7ab662f850c..4acbee6f2ca 100644 --- a/src/couch/src/couch_query_servers.erl +++ b/src/couch/src/couch_query_servers.erl @@ -42,7 +42,12 @@ try_compile(Proc, FunctionType, FunctionName, FunctionSource) -> try - proc_prompt(Proc, [<<"add_fun">>, FunctionSource]), + case FunctionType of + validate_doc_update -> + proc_prompt(Proc, [<<"validate_fun">>, FunctionSource]); + _ -> + proc_prompt(Proc, [<<"add_fun">>, FunctionSource]) + end, ok catch {compilation_error, E} -> diff --git a/src/couch_mrview/src/couch_mrview.erl b/src/couch_mrview/src/couch_mrview.erl index 244f668af03..9350b17bf8d 100644 --- a/src/couch_mrview/src/couch_mrview.erl +++ b/src/couch_mrview/src/couch_mrview.erl @@ -248,12 +248,11 @@ validate(Db, DDoc) -> ok end, - try Views =/= [] andalso couch_query_servers:get_os_process(Lang) of - false -> - ok; + try couch_query_servers:get_os_process(Lang) of Proc -> try - lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views) + lists:foreach(fun(V) -> ValidateView(Proc, V) end, Views), + validate_vdu(Proc, DDoc) after couch_query_servers:ret_os_process(Proc) end @@ -263,6 +262,21 @@ validate(Db, DDoc) -> ok end. +validate_vdu(Proc, #doc{body = {Props}}) -> + case config:get_boolean("couchdb", "validate_vdu", false) of + true -> + case couch_util:get_value(<<"validate_doc_update">>, Props) of + undefined -> + ok; + VDU -> + couch_query_servers:try_compile( + Proc, validate_doc_update, <<"validate_doc_update">>, VDU + ) + end; + _ -> + ok + end. + check_rank(<>) -> try binary_to_integer(N) of Val when Val >= 1 andalso Val =< ?MAX_RANK -> diff --git a/src/couch_scanner/test/eunit/couch_scanner_test.erl b/src/couch_scanner/test/eunit/couch_scanner_test.erl index 9cce06f3824..ad4b7c2f984 100644 --- a/src/couch_scanner/test/eunit/couch_scanner_test.erl +++ b/src/couch_scanner/test/eunit/couch_scanner_test.erl @@ -95,7 +95,7 @@ setup() -> #{from => <<"x">>, to => <<"y">>} ], updates => #{u1 => <<"function(d,r){return [];}">>}, - validate_doc_update => <<"function(n,o,u,s){return true;">> + validate_doc_update => <<"function(n,o,u,s){return true;}">> }), ok = add_doc(DbName2, ?DOC3, #{foo3 => bax}), ok = add_doc(DbName2, ?DOC4, #{foo4 => baw, <<>> => this_is_ok_apparently}), diff --git a/src/docs/src/config/couchdb.rst b/src/docs/src/config/couchdb.rst index b07ea5a30b8..5388998812c 100644 --- a/src/docs/src/config/couchdb.rst +++ b/src/docs/src/config/couchdb.rst @@ -258,6 +258,19 @@ Base CouchDB Options [couchdb] js_engine = spidermonkey + .. config:option:: validate_vdu :: Enable checking of ``validate_doc_update`` + + .. versionadded:: TODO + + When set to ``true``, the ``validate_doc_update`` field will be + validated when design documents are updated. For ``javascript`` design + docs, the field must contain a well-formed JavaScript function, and for + ``query`` design docs it must contain a Mango selector that is correctly + structured to validate document updates. :: + + [couchdb] + validate_vdu = true + .. config:option:: time_seq_min_time :: Minimum time-seq threshold .. versionchanged:: 3.6 diff --git a/src/docs/src/ddocs/ddocs.rst b/src/docs/src/ddocs/ddocs.rst index 501501857ce..8006d5c320a 100644 --- a/src/docs/src/ddocs/ddocs.rst +++ b/src/docs/src/ddocs/ddocs.rst @@ -955,7 +955,8 @@ To use Mango selectors for validation, the design document must have the containing the following fields: * ``newDoc``: New version of document that will be stored. -* ``oldDoc``: Previous version of document that is already stored. +* ``oldDoc``: Previous version of document that is already stored; this field is + absent if the doc is being created for the first time. For example, to check that all docs contain a ``title`` which is a string, and a ``year`` which is a number: @@ -1012,3 +1013,72 @@ this design document: } } } + +By using the ``oldDoc`` field, we can create rules that say a document can only +be updated if it is currently in a certain state. For example, this rule would +enforce that only documents describing actors can be updated: + +.. code-block:: json + + { + "language": "query", + + "validate_doc_update": { + "oldDoc": { "type": "actor" } + } + } + +This also makes it so that no new documents can be created, because a write is +only accepted if a previous version of the doc already exists. To relax this +constraint, allow ``oldDoc`` not to exist: + +.. code-block:: json + + { + "language": "query", + + "validate_doc_update": { + "oldDoc": { + "$or": [ + { "$exists": false }, + { "type": "actor" } + ] + } + } + } + +This validator will allow any new document creation, and updates to docs where +the ``type`` field is ``"actor"``. We can also have multiple rules for new +document states that depend on the current state, by combining ``$or`` with +several sets of ``{ oldDoc, newDoc }`` rules: + +.. code-block:: json + + { + "language": "query", + + "validate_doc_update": { + "$or": [ + // allow creation of docs with an acceptable type + { + "oldDoc": { "$exists": false }, + "newDoc": { + "type": { "$in": ["movie", "actor"] } + } + }, + // if a doc currently has "type": "actor", make sure its "movies" + // field is a non-empty list of strings + { + "oldDoc": { "type": "actor" }, + "newDoc": { + "movies": { + "$type": "array", + "$not": { "$size": 0 }, + "$allMatch": { "$type": "string" } + } + } + }, + // etc. + ] + } + } diff --git a/src/mango/src/mango_native_proc.erl b/src/mango/src/mango_native_proc.erl index edcecd4b6fb..68ddd88c14c 100644 --- a/src/mango/src/mango_native_proc.erl +++ b/src/mango/src/mango_native_proc.erl @@ -46,7 +46,12 @@ set_timeout(Pid, TimeOut) when is_integer(TimeOut), TimeOut > 0 -> gen_server:call(Pid, {set_timeout, TimeOut}). prompt(Pid, Data) -> - gen_server:call(Pid, {prompt, Data}). + case gen_server:call(Pid, {prompt, Data}) of + {error, Error} -> + throw(Error); + Other -> + Other + end. init(_) -> {ok, #st{}}. @@ -95,6 +100,17 @@ handle_call({prompt, [<<"nouveau_index_doc">>, Doc]}, _From, St) -> Else end, {reply, Vals, St}; +handle_call({prompt, [<<"validate_fun">>, Selector0 | _Rest]}, _From, St) -> + try mango_selector:normalize(Selector0) of + Selector -> + case validate_vdu(Selector) of + ok -> {reply, true, St}; + Error -> {reply, {error, Error}, St} + end + catch + throw:{mango_error, mango_selector, Error} -> + {reply, {error, Error}, St} + end; handle_call({prompt, [<<"ddoc">>, <<"new">>, DDocId, {DDoc}]}, _From, St) -> NewSt = case couch_util:get_value(<<"validate_doc_update">>, DDoc) of @@ -112,18 +128,39 @@ handle_call({prompt, [<<"ddoc">>, DDocId, [<<"validate_doc_update">>], Args]}, _ Msg = [<<"validate_doc_update">>, DDocId], {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}; Selector -> - [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">>}]} - end, - {reply, Reply, St} + case validate_vdu(Selector) of + {_, Error} -> + {stop, {invalid_call, Error}, {invalid_call, Error}, St}; + ok -> + [NewDoc, OldDoc, _Ctx, _SecObj] = Args, + Struct = + case OldDoc of + null -> {[{<<"newDoc">>, NewDoc}]}; + Doc -> {[{<<"newDoc">>, NewDoc}, {<<"oldDoc">>, Doc}]} + end, + Reply = + case mango_selector:match_failures(Selector, Struct) of + [] -> + true; + Failures -> + {[{<<"forbidden">>, {[{<<"failures">>, Failures}]}}]} + end, + {reply, Reply, St} + end end; handle_call(Msg, _From, St) -> {stop, {invalid_call, Msg}, {invalid_call, Msg}, St}. +validate_vdu(VDU) -> + case mango_selector:has_allowed_fields(VDU, [<<"newDoc">>, <<"oldDoc">>]) of + true -> + ok; + false -> + Msg = + <<"'validate_doc_update' may only contain 'newDoc' and 'oldDoc' as top-level fields">>, + {compilation_error, Msg} + end. + handle_cast(garbage_collect, St) -> garbage_collect(), {noreply, St}; diff --git a/src/mango/src/mango_selector.erl b/src/mango/src/mango_selector.erl index e53820c4a02..a63c1909e60 100644 --- a/src/mango/src/mango_selector.erl +++ b/src/mango/src/mango_selector.erl @@ -15,7 +15,9 @@ -export([ normalize/1, match/2, + match_failures/2, has_required_fields/2, + has_allowed_fields/2, is_constant_field/2, fields/1 ]). @@ -23,6 +25,20 @@ -include_lib("couch/include/couch_db.hrl"). -include("mango.hrl"). +-record(ctx, { + cmp, + verbose = false, + negate = false, + path = [] +}). + +-record(failure, { + op, + type = mismatch, + params = [], + ctx +}). + % Validate and normalize each operator. This translates % every selector operator into a consistent version that % we can then rely on for all other selector functions. @@ -53,13 +69,19 @@ match(Selector, D) -> couch_stats:increment_counter([mango, evaluate_selector]), match_int(Selector, D). -% An empty selector matches any value. -match_int({[]}, _) -> - true; -match_int(Selector, #doc{body = Body}) -> - match(Selector, Body, fun mango_json:cmp/2); -match_int(Selector, {Props}) -> - match(Selector, {Props}, fun mango_json:cmp/2). +match_failures(Selector, D) -> + couch_stats:increment_counter([mango, evaluate_selector]), + [format_failure(F) || F <- match_int(Selector, D, true)]. + +match_int(Selector, D) -> + match_int(Selector, D, false). + +match_int(Selector, D, Verbose) -> + Ctx = #ctx{cmp = fun mango_json:cmp/2, verbose = Verbose}, + case D of + #doc{body = Body} -> match(Selector, Body, Ctx); + Other -> match(Selector, Other, Ctx) + end. % Convert each operator into a normalized version as well % as convert an implicit operators into their explicit @@ -299,9 +321,16 @@ norm_negations({[{<<"$or">>, Args}]}) -> norm_negations({[{<<"$elemMatch">>, Arg}]}) -> {[{<<"$elemMatch">>, norm_negations(Arg)}]}; norm_negations({[{<<"$allMatch">>, Arg}]}) -> - {[{<<"$allMatch">>, norm_negations(Arg)}]}; + Out = norm_negations(Arg), + {[{<<"$allMatch">>, Out}]}; norm_negations({[{<<"$keyMapMatch">>, Arg}]}) -> {[{<<"$keyMapMatch">>, norm_negations(Arg)}]}; +% Sometimes a top-level key will be a doc field because it +% can't be pushed down through its corresponding operator, +% e.g. {x: {$allMatch: S}}. Negation inside the $allMatch +% should still be normalized. +norm_negations({[{Field, Cond}]}) when is_list(Field) -> + {[{Field, norm_negations(Cond)}]}; % All other conditions can't introduce negations anywhere % further down the operator tree. norm_negations(Cond) -> @@ -361,26 +390,62 @@ negate({[{<<"$", _/binary>>, _}]} = Cond) -> negate({[{Field, Cond}]}) -> {[{Field, negate(Cond)}]}. +% An empty selector matches any value. +match({[]}, _, #ctx{verbose = false}) -> + true; +match({[]}, _, #ctx{verbose = true}) -> + []; % We need to treat an empty array as always true. This will be applied % for $or, $in, $all, $nin as well. -match({[{<<"$and">>, []}]}, _, _) -> +match({[{<<"$and">>, []}]}, _, #ctx{verbose = false}) -> true; -match({[{<<"$and">>, Args}]}, Value, Cmp) -> - Pred = fun(SubSel) -> match(SubSel, Value, Cmp) end, +match({[{<<"$and">>, []}]}, _, #ctx{negate = false}) -> + []; +match({[{<<"$and">>, []}]}, _, Ctx) -> + [#failure{op = 'and', type = empty_list, params = [[]], ctx = Ctx}]; +match({[{<<"$and">>, Args}]}, Value, #ctx{verbose = false} = Ctx) -> + Pred = fun(SubSel) -> match(SubSel, Value, Ctx) end, lists:all(Pred, Args); -match({[{<<"$or">>, []}]}, _, _) -> +match({[{<<"$and">>, Args}]}, Value, #ctx{negate = true} = Ctx) -> + NotArgs = [{[{<<"$not">>, A}]} || A <- Args], + PosCtx = Ctx#ctx{negate = false}, + match({[{<<"$or">>, NotArgs}]}, Value, PosCtx); +match({[{<<"$and">>, Args}]}, Value, Ctx) -> + MatchSubSel = fun(SubSel) -> match(SubSel, Value, Ctx) end, + lists:flatmap(MatchSubSel, Args); +match({[{<<"$or">>, []}]}, _, #ctx{verbose = false}) -> true; -match({[{<<"$or">>, Args}]}, Value, Cmp) -> - Pred = fun(SubSel) -> match(SubSel, Value, Cmp) end, +match({[{<<"$or">>, []}]}, _, #ctx{negate = false}) -> + []; +match({[{<<"$or">>, []}]}, _, Ctx) -> + [#failure{op = 'or', type = empty_list, params = [[]], ctx = Ctx}]; +match({[{<<"$or">>, Args}]}, Value, #ctx{verbose = false} = Ctx) -> + Pred = fun(SubSel) -> match(SubSel, Value, Ctx) end, lists:any(Pred, Args); -match({[{<<"$not">>, Arg}]}, Value, Cmp) -> - not match(Arg, Value, Cmp); -match({[{<<"$all">>, []}]}, _, _) -> +match({[{<<"$or">>, Args}]}, Value, #ctx{negate = true} = Ctx) -> + NotArgs = [{[{<<"$not">>, A}]} || A <- Args], + PosCtx = Ctx#ctx{negate = false}, + match({[{<<"$and">>, NotArgs}]}, Value, PosCtx); +match({[{<<"$or">>, Args}]}, Value, Ctx) -> + SubSelFailures = [match(A, Value, Ctx) || A <- Args], + case lists:member([], SubSelFailures) of + true -> []; + false -> lists:flatten(SubSelFailures) + end; +match({[{<<"$not">>, Arg}]}, Value, #ctx{verbose = false} = Ctx) -> + not match(Arg, Value, Ctx); +match({[{<<"$not">>, Arg}]}, Value, #ctx{negate = Neg} = Ctx) -> + match(Arg, Value, Ctx#ctx{negate = not Neg}); +match({[{<<"$all">>, []}]}, _, #ctx{verbose = false}) -> false; +match({[{<<"$all">>, []}]}, _, #ctx{negate = false} = Ctx) -> + [#failure{op = all, type = empty_list, params = [[]], ctx = Ctx}]; +match({[{<<"$all">>, []}]}, _, #ctx{negate = true}) -> + []; % All of the values in Args must exist in Values or % Values == hd(Args) if Args is a single element list % that contains a list. -match({[{<<"$all">>, Args}]}, Values, _Cmp) when is_list(Values) -> +match({[{<<"$all">>, Args}]}, Values, #ctx{verbose = false}) when is_list(Values) -> Pred = fun(A) -> lists:member(A, Values) end, HasArgs = lists:all(Pred, Args), IsArgs = @@ -391,29 +456,31 @@ match({[{<<"$all">>, Args}]}, Values, _Cmp) when is_list(Values) -> false end, HasArgs orelse IsArgs; -match({[{<<"$all">>, _Args}]}, _Values, _Cmp) -> +match({[{<<"$all">>, _Args}]}, _Values, #ctx{verbose = false}) -> false; +match({[{<<"$all">>, Args}]} = Expr, Values, Ctx) -> + match_with_failure(Expr, Values, all, [Args], Ctx); %% This is for $elemMatch, $allMatch, and possibly $in because of our normalizer. %% A selector such as {"field_name": {"$elemMatch": {"$gte": 80, "$lt": 85}}} %% gets normalized to: %% {[{<<"field_name">>, %% {[{<<"$elemMatch">>, %% {[{<<"$and">>, [ -%% {[{<<>>,{[{<<"$gte">>,80}]}}]}, -%% {[{<<>>,{[{<<"$lt">>,85}]}}]} +%% {[{[],{[{<<"$gte">>,80}]}}]}, +%% {[{[],{[{<<"$lt">>,85}]}}]} %% ]}]} %% }]} %% }]}. -%% So we filter out the <<>>. -match({[{<<>>, Arg}]}, Values, Cmp) -> - match(Arg, Values, Cmp); +%% So we filter out the []. +match({[{[], Arg}]}, Values, Ctx) -> + match(Arg, Values, Ctx); % Matches when any element in values matches the % sub-selector Arg. -match({[{<<"$elemMatch">>, Arg}]}, Values, Cmp) when is_list(Values) -> +match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{verbose = false} = Ctx) when is_list(Values) -> try lists:foreach( fun(V) -> - case match(Arg, V, Cmp) of + case match(Arg, V, Ctx) of true -> throw(matched); _ -> ok end @@ -427,15 +494,33 @@ match({[{<<"$elemMatch">>, Arg}]}, Values, Cmp) when is_list(Values) -> _:_ -> false end; -match({[{<<"$elemMatch">>, _Arg}]}, _Value, _Cmp) -> +match({[{<<"$elemMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) -> false; +match({[{<<"$elemMatch">>, _Arg}]}, [], #ctx{negate = false} = Ctx) -> + [#failure{op = elemMatch, type = empty_list, ctx = Ctx}]; +match({[{<<"$elemMatch">>, _Arg}]}, [], #ctx{negate = true}) -> + []; +match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{negate = true} = Ctx) -> + PosCtx = Ctx#ctx{negate = false}, + match({[{<<"$allMatch">>, {[{<<"$not">>, Arg}]}}]}, Values, PosCtx); +match({[{<<"$elemMatch">>, Arg}]}, Values, #ctx{path = Path} = Ctx) when is_list(Values) -> + ValueFailures = [ + match(Arg, V, Ctx#ctx{path = [Idx | Path]}) + || {Idx, V} <- lists:enumerate(0, Values) + ], + case lists:member([], ValueFailures) of + true -> []; + false -> lists:flatten(ValueFailures) + end; +match({[{<<"$elemMatch">>, _Arg}]}, Values, #ctx{} = Ctx) -> + [#failure{op = elemMatch, type = bad_value, params = [Values], ctx = Ctx}]; % Matches when all elements in values match the % sub-selector Arg. -match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, Cmp) -> +match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, #ctx{verbose = false} = Ctx) -> try lists:foreach( fun(V) -> - case match(Arg, V, Cmp) of + case match(Arg, V, Ctx) of false -> throw(unmatched); _ -> ok end @@ -447,15 +532,28 @@ match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, Cmp) -> _:_ -> false end; -match({[{<<"$allMatch">>, _Arg}]}, _Value, _Cmp) -> +match({[{<<"$allMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) -> false; +match({[{<<"$allMatch">>, _Arg}]}, [], #ctx{negate = false} = Ctx) -> + [#failure{op = allMatch, type = empty_list, ctx = Ctx}]; +match({[{<<"$allMatch">>, _Arg}]}, [], #ctx{negate = true}) -> + []; +match({[{<<"$allMatch">>, Arg}]}, Values, #ctx{negate = true} = Ctx) -> + PosCtx = Ctx#ctx{negate = false}, + match({[{<<"$elemMatch">>, {[{<<"$not">>, Arg}]}}]}, Values, PosCtx); +match({[{<<"$allMatch">>, Arg}]}, [_ | _] = Values, #ctx{path = Path} = Ctx) -> + MatchValue = fun({Idx, V}) -> match(Arg, V, Ctx#ctx{path = [Idx | Path]}) end, + EnumValues = lists:enumerate(0, Values), + lists:flatmap(MatchValue, EnumValues); +match({[{<<"$allMatch">>, _Arg}]}, Values, #ctx{} = Ctx) -> + [#failure{op = allMatch, type = bad_value, params = [Values], ctx = Ctx}]; % Matches when any key in the map value matches the % sub-selector Arg. -match({[{<<"$keyMapMatch">>, Arg}]}, Value, Cmp) when is_tuple(Value) -> +match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{verbose = false} = Ctx) when is_tuple(Value) -> try lists:foreach( fun(V) -> - case match(Arg, V, Cmp) of + case match(Arg, V, Ctx) of true -> throw(matched); _ -> ok end @@ -469,24 +567,43 @@ match({[{<<"$keyMapMatch">>, Arg}]}, Value, Cmp) when is_tuple(Value) -> _:_ -> false end; -match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, _Cmp) -> +match({[{<<"$keyMapMatch">>, _Arg}]}, _Value, #ctx{verbose = false}) -> false; +match({[{<<"$keyMapMatch">>, _Arg}]}, {[]}, #ctx{negate = false} = Ctx) -> + [#failure{op = keyMapMatch, type = empty_list, ctx = Ctx}]; +match({[{<<"$keyMapMatch">>, _Arg}]}, {[]}, #ctx{negate = true}) -> + []; +match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{negate = true, path = Path} = Ctx) when + is_tuple(Value) +-> + Keys = [Key || {Key, _} <- element(1, Value)], + MatchKey = fun(K) -> match(Arg, K, Ctx#ctx{path = [K | Path]}) end, + lists:flatmap(MatchKey, Keys); +match({[{<<"$keyMapMatch">>, Arg}]}, Value, #ctx{path = Path} = Ctx) when is_tuple(Value) -> + Keys = [Key || {Key, _} <- element(1, Value)], + KeyFailures = [match(Arg, K, Ctx#ctx{path = [K | Path]}) || K <- Keys], + case lists:member([], KeyFailures) of + true -> []; + false -> lists:flatten(KeyFailures) + end; +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, Cmp) -> - Cmp(Value, Arg) < 0; -match({[{<<"$lte">>, Arg}]}, Value, Cmp) -> - Cmp(Value, Arg) =< 0; -match({[{<<"$eq">>, Arg}]}, Value, Cmp) -> - Cmp(Value, Arg) == 0; -match({[{<<"$ne">>, Arg}]}, Value, Cmp) -> - Cmp(Value, Arg) /= 0; -match({[{<<"$gte">>, Arg}]}, Value, Cmp) -> - Cmp(Value, Arg) >= 0; -match({[{<<"$gt">>, Arg}]}, Value, Cmp) -> - Cmp(Value, Arg) > 0; -match({[{<<"$in">>, []}]}, _, _) -> +match({[{<<"$lt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(lt, Arg, Ctx, Cmp(Value, Arg) < 0); +match({[{<<"$lte">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(lte, Arg, Ctx, Cmp(Value, Arg) =< 0); +match({[{<<"$eq">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(eq, Arg, Ctx, Cmp(Value, Arg) == 0); +match({[{<<"$ne">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(ne, Arg, Ctx, Cmp(Value, Arg) /= 0); +match({[{<<"$gte">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(gte, Arg, Ctx, Cmp(Value, Arg) >= 0); +match({[{<<"$gt">>, Arg}]}, Value, #ctx{cmp = Cmp} = Ctx) -> + compare(gt, Arg, Ctx, Cmp(Value, Arg) > 0); +match({[{<<"$in">>, []}]}, _, #ctx{verbose = false}) -> false; -match({[{<<"$in">>, Args}]}, Values, Cmp) when is_list(Values) -> +match({[{<<"$in">>, Args}]}, Values, #ctx{verbose = false, cmp = Cmp}) when is_list(Values) -> Pred = fun(Arg) -> lists:foldl( fun(Value, Match) -> @@ -497,48 +614,66 @@ match({[{<<"$in">>, Args}]}, Values, Cmp) when is_list(Values) -> ) end, lists:any(Pred, Args); -match({[{<<"$in">>, Args}]}, Value, Cmp) -> +match({[{<<"$in">>, Args}]}, Value, #ctx{verbose = false, cmp = Cmp}) -> Pred = fun(Arg) -> Cmp(Value, Arg) == 0 end, lists:any(Pred, Args); -match({[{<<"$nin">>, []}]}, _, _) -> +match({[{<<"$in">>, Args}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, in, [Args], Ctx); +match({[{<<"$nin">>, []}]}, _, #ctx{verbose = false}) -> true; -match({[{<<"$nin">>, Args}]}, Values, Cmp) when is_list(Values) -> - not match({[{<<"$in">>, Args}]}, Values, Cmp); -match({[{<<"$nin">>, Args}]}, Value, Cmp) -> +match({[{<<"$nin">>, Args}]}, Values, #ctx{verbose = false} = Ctx) when is_list(Values) -> + not match({[{<<"$in">>, Args}]}, Values, Ctx); +match({[{<<"$nin">>, Args}]}, Value, #ctx{verbose = false, cmp = Cmp}) -> Pred = fun(Arg) -> Cmp(Value, Arg) /= 0 end, lists:all(Pred, Args); +match({[{<<"$nin">>, Args}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, nin, [Args], Ctx); % This logic is a bit subtle. Basically, if value is % not undefined, then it exists. -match({[{<<"$exists">>, ShouldExist}]}, Value, _Cmp) -> +match({[{<<"$exists">>, ShouldExist}]}, Value, #ctx{verbose = false}) -> Exists = Value /= undefined, ShouldExist andalso Exists; -match({[{<<"$type">>, Arg}]}, Value, _Cmp) when is_binary(Arg) -> +match({[{<<"$exists">>, ShouldExist}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, exists, [ShouldExist], Ctx); +match({[{<<"$type">>, Arg}]}, Value, #ctx{verbose = false}) when is_binary(Arg) -> Arg == mango_json:type(Value); -match({[{<<"$mod">>, [D, R]}]}, Value, _Cmp) when is_integer(Value) -> +match({[{<<"$type">>, Arg}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, type, [Arg], Ctx); +match({[{<<"$mod">>, [D, R]}]}, Value, #ctx{verbose = false}) when is_integer(Value) -> Value rem D == R; -match({[{<<"$mod">>, _}]}, _Value, _Cmp) -> +match({[{<<"$mod">>, _}]}, _Value, #ctx{verbose = false}) -> false; -match({[{<<"$beginsWith">>, Prefix}]}, Value, _Cmp) when is_binary(Prefix), is_binary(Value) -> +match({[{<<"$mod">>, [D, R]}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, mod, [D, R], Ctx); +match({[{<<"$beginsWith">>, Prefix}]}, Value, #ctx{verbose = false}) when + is_binary(Prefix), is_binary(Value) +-> string:prefix(Value, Prefix) /= nomatch; % When Value is not a string, do not match -match({[{<<"$beginsWith">>, Prefix}]}, _, _Cmp) when is_binary(Prefix) -> +match({[{<<"$beginsWith">>, Prefix}]}, _, #ctx{verbose = false}) when is_binary(Prefix) -> false; -match({[{<<"$regex">>, Regex}]}, Value, _Cmp) when is_binary(Value) -> +match({[{<<"$beginsWith">>, Prefix}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, beginsWith, [Prefix], Ctx); +match({[{<<"$regex">>, Regex}]}, Value, #ctx{verbose = false}) when is_binary(Value) -> try match == re:run(Value, Regex, [{capture, none}]) catch _:_ -> false end; -match({[{<<"$regex">>, _}]}, _Value, _Cmp) -> +match({[{<<"$regex">>, _}]}, _Value, #ctx{verbose = false}) -> false; -match({[{<<"$size">>, Arg}]}, Values, _Cmp) when is_list(Values) -> +match({[{<<"$regex">>, Regex}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, regex, [Regex], Ctx); +match({[{<<"$size">>, Arg}]}, Values, #ctx{verbose = false}) when is_list(Values) -> length(Values) == Arg; -match({[{<<"$size">>, _}]}, _Value, _Cmp) -> +match({[{<<"$size">>, _}]}, _Value, #ctx{verbose = false}) -> false; +match({[{<<"$size">>, Arg}]} = Expr, Value, Ctx) -> + match_with_failure(Expr, Value, size, [Arg], Ctx); % We don't have any choice but to believe that the text % index returned valid matches -match({[{<<"$default">>, _}]}, _Value, _Cmp) -> +match({[{<<"$default">>, _}]}, _Value, #ctx{verbose = false}) -> true; % All other operators are internal assertion errors for % matching because we either should've removed them during @@ -548,22 +683,136 @@ match({[{<<"$", _/binary>> = Op, _}]}, _, _) -> % We need to traverse value to find field. The call to % mango_doc:get_field/2 may return either not_found or % bad_path in which case matching fails. -match({[{Field, Cond}]}, Value, Cmp) -> +match({[{Field, Cond}]}, Value, #ctx{verbose = Verb, path = Path} = Ctx) -> + InnerPath = extend_path(Field, Path), + InnerCtx = Ctx#ctx{path = InnerPath}, case mango_doc:get_field(Value, Field) of not_found when Cond == {[{<<"$exists">>, false}]} -> - true; + case Verb of + true -> []; + false -> true + end; not_found -> - false; + case Verb of + true -> [#failure{op = field, type = not_found, ctx = InnerCtx}]; + false -> false + end; bad_path -> - false; + case Verb of + true -> [#failure{op = field, type = bad_path, ctx = InnerCtx}]; + false -> false + end; SubValue when Field == <<"_id">> -> - match(Cond, SubValue, fun mango_json:cmp_raw/2); + match(Cond, SubValue, InnerCtx#ctx{cmp = fun mango_json:cmp_raw/2}); SubValue -> - match(Cond, SubValue, Cmp) + match(Cond, SubValue, InnerCtx) end; -match({[_, _ | _] = _Props} = Sel, _Value, _Cmp) -> +match({[_, _ | _] = _Props} = Sel, _Value, _Ctx) -> error({unnormalized_selector, Sel}). +extend_path(Field, Path) when is_binary(Field) -> + [Field | Path]; +extend_path(Field, Path) when is_list(Field) -> + lists:foldl(fun(F, Acc) -> [F | Acc] end, Path, Field). + +match_with_failure(Expr, Value, Op, Params, #ctx{negate = Neg} = Ctx) -> + case not match(Expr, Value, Ctx#ctx{verbose = false}) of + Neg -> []; + _ -> [#failure{op = Op, params = Params, ctx = Ctx}] + end. + +compare(_, _, #ctx{verbose = false}, Cond) -> + Cond; +compare(Op, Arg, #ctx{negate = Neg} = Ctx, Cond) -> + case not Cond of + Neg -> []; + _ -> [#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, jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy: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", [jiffy:encode(X)]); +format_op(all, true, mismatch, [X]) -> + io_lib:format("must not contain at least one of the values in ~s", [jiffy: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. @@ -639,6 +888,39 @@ has_required_fields_int([{[{Field, Cond}]} | Rest], RequiredFields) -> has_required_fields_int(Rest, lists:delete(Field, RequiredFields)) end. +has_allowed_fields(Selector, AllowedFields) -> + Paths = lists:map( + fun(Field) -> + {ok, Path} = mango_util:parse_field(Field), + Path + end, + AllowedFields + ), + has_allowed_fields_int(Selector, Paths). + +has_allowed_fields_int({[{Field, Cond}]}, Paths) when is_list(Field) -> + Stemmed = [match_prefix(Field, Path) || Path <- Paths], + Matched = [Path || Path <- Stemmed, Path /= nil], + case Matched of + [] -> false; + M -> has_allowed_fields_int(Cond, M) + end; +has_allowed_fields_int({[{_Op, Conds}]}, Paths) when is_list(Conds) -> + lists:all(fun(Cond) -> has_allowed_fields_int(Cond, Paths) end, Conds); +has_allowed_fields_int({[{_Op, Cond}]}, Paths) -> + has_allowed_fields_int(Cond, Paths); +has_allowed_fields_int(_, _) -> + true. + +match_prefix([A | Rest1], [A | Rest2]) -> + match_prefix(Rest1, Rest2); +match_prefix([], Rest) -> + Rest; +match_prefix(_, []) -> + []; +match_prefix(_, _) -> + nil. + % Returns true if a field in the selector is a constant value e.g. {a: {$eq: 1}} is_constant_field(Selector, Field) when not is_list(Field) -> {ok, Path} = mango_util:parse_field(Field), @@ -1018,6 +1300,23 @@ has_required_fields_or_nested_or_false_test() -> Normalized = normalize(Selector), ?assertEqual(false, has_required_fields(Normalized, RequiredFields)). +has_allowed_fields_test() -> + Sel1 = normalize({[{<<"a">>, 1}]}), + ?assertEqual(has_allowed_fields(Sel1, [<<"a">>]), true), + ?assertEqual(has_allowed_fields(Sel1, [<<"a">>, <<"b">>]), true), + ?assertEqual(has_allowed_fields(Sel1, [<<"b">>]), false), + + Sel2 = normalize({[{<<"$or">>, [{[{<<"a.b">>, 1}]}, {[{<<"c.d">>, 2}]}]}]}), + ?assertEqual(has_allowed_fields(Sel2, [<<"a">>, <<"c">>]), true), + ?assertEqual(has_allowed_fields(Sel2, [<<"a.b">>, <<"c.d">>]), true), + ?assertEqual(has_allowed_fields(Sel2, [<<"a">>]), false), + + Sel3 = normalize({[{<<"a">>, {[{<<"$or">>, [{[{<<"b">>, 1}]}, {[{<<"c">>, 2}]}]}]}}]}), + ?assertEqual(has_allowed_fields(Sel3, [<<"a">>]), true), + ?assertEqual(has_allowed_fields(Sel3, [<<"a.b">>, <<"a.c">>]), true), + ?assertEqual(has_allowed_fields(Sel3, [<<"a.c">>]), false), + ?assertEqual(has_allowed_fields(Sel3, [<<"b">>]), false). + check_match(Selector) -> % Call match_int/2 to avoid ERROR for missing metric; this is confusing % in the middle of test output. @@ -1100,10 +1399,21 @@ check_selector(Selector, Results) -> SelPos = normalize({[{<<"x">>, Selector}]}), SelNeg = normalize({[{<<"x">>, {[{<<"$not">>, Selector}]}}]}), + ListToBool = fun(List) -> + case List of + [] -> true; + [_ | _] -> false + end + end, + Check = fun({Result, Value}) -> Doc = {[{<<"x">>, Value}]}, - ?assertEqual(Result, match_int(SelPos, Doc)), - ?assertEqual(not Result, match_int(SelNeg, Doc)) + + ?assertEqual(Result, match_int(SelPos, Doc, false)), + ?assertEqual(Result, ListToBool(match_int(SelPos, Doc, true))), + + ?assertEqual(not Result, match_int(SelNeg, Doc, false)), + ?assertEqual(not Result, ListToBool(match_int(SelNeg, Doc, true))) end, lists:foreach(Check, Results). @@ -1123,7 +1433,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}]), @@ -1140,7 +1464,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}]), @@ -1157,7 +1495,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}]), @@ -1174,7 +1526,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}]), @@ -1192,7 +1558,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}]), @@ -1220,7 +1600,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">>, []}]}, [ @@ -1262,6 +1656,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() -> @@ -1304,6 +1722,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() -> @@ -1354,7 +1801,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}]}}]}, [ @@ -1392,6 +1888,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() -> @@ -1434,7 +1949,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]+$">>}]}, [ @@ -1442,7 +1971,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">>}]}, [ @@ -1452,7 +2000,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]}]}, [ @@ -1461,7 +2023,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}]}, [ @@ -1470,7 +2056,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 @@ -1488,7 +2088,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}]}}]}, [ @@ -1496,7 +2110,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]+$">>}]}}]}, [ @@ -1505,7 +2136,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 = {[]}, @@ -1574,7 +2224,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 @@ -1632,7 +2288,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 @@ -1685,7 +2374,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 @@ -1710,6 +2425,344 @@ 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( + {[ + {<<"x">>, + {[ + {<<"$nor">>, [ + {[{<<"$lt">>, 3}]}, {[{<<"$gt">>, 5}]} + ]} + ]}} + ]} + ), + ?assertEqual( + {[ + {<<"$and">>, [ + {[{[<<"x">>], {[{<<"$gte">>, 3}]}}]}, + {[{[<<"x">>], {[{<<"$lte">>, 5}]}}]} + ]} + ]}, + Sel1 + ), + ?assertEqual(true, match_int(Sel1, {[{<<"x">>, 4}]})), + ?assertEqual(false, match_int(Sel1, {[{<<"x">>, 40}]})), + + Sel2 = normalize( + {[ + {<<"x">>, + {[ + {<<"$allMatch">>, + {[ + {<<"y">>, + {[ + {<<"$nor">>, [ + {[{<<"$lt">>, 3}]}, {[{<<"$gt">>, 5}]} + ]} + ]}} + ]}} + ]}} + ]} + ), + ?assertEqual( + {[ + { + [<<"x">>], + {[ + {<<"$allMatch">>, + {[ + {<<"$and">>, [ + {[{[<<"y">>], {[{<<"$gte">>, 3}]}}]}, + {[{[<<"y">>], {[{<<"$lte">>, 5}]}}]} + ]} + ]}} + ]} + } + ]}, + Sel2 + ), + ?assertEqual(true, match_int(Sel2, {[{<<"x">>, [{[{<<"y">>, 4}]}]}]})), + ?assertEqual(false, match_int(Sel2, {[{<<"x">>, [{[{<<"y">>, 40}]}]}]})). + +match_failures_object_test() -> + Selector = normalize( + {[ + {<<"a">>, 1}, + {<<"b">>, {[{<<"c">>, 3}]}} + ]} + ), + + Fails0 = match_failures( + Selector, + {[ + {<<"a">>, 1}, + {<<"b">>, {[{<<"c">>, 3}]}} + ]} + ), + ?assertEqual([], Fails0), + + Fails1 = match_failures( + Selector, + {[ + {<<"a">>, 0}, + {<<"b">>, {[{<<"c">>, 3}]}} + ]} + ), + ?assertMatch( + [{[{<<"path">>, [<<"a">>]}, {<<"message">>, <<"must be equal to 1">>}]}], + Fails1 + ), + + Fails2 = match_failures( + Selector, + {[ + {<<"a">>, 1}, + {<<"b">>, {[{<<"c">>, 4}]}} + ]} + ), + ?assertMatch( + [{[{<<"path">>, [<<"b">>, <<"c">>]}, {<<"message">>, <<"must be equal to 3">>}]}], + Fails2 + ), + + Fails3 = match_failures( + Selector, + {[ + {<<"a">>, 2}, + {<<"b">>, {[{<<"c">>, 4}]}} + ]} + ), + ?assertMatch( + [ + {[{<<"path">>, [<<"a">>]}, {<<"message">>, <<"must be equal to 1">>}]}, + {[{<<"path">>, [<<"b">>, <<"c">>]}, {<<"message">>, <<"must be equal to 3">>}]} + ], + Fails3 + ). + +match_failures_elemmatch_test() -> + SelElemMatch = normalize( + {[ + {<<"a">>, + {[ + {<<"$elemMatch">>, {[{<<"$gt">>, 4}]}} + ]}} + ]} + ), + + Fails0 = match_failures(SelElemMatch, {[{<<"a">>, [5, 3, 2]}]}), + ?assertEqual([], Fails0), + + Fails1 = match_failures(SelElemMatch, {[{<<"a">>, []}]}), + ?assertMatch( + [ + {[ + {<<"path">>, [<<"a">>]}, + {<<"message">>, <<"operator $elemMatch was invoked with an empty list">>} + ]} + ], + Fails1 + ), + + Fails2 = match_failures(SelElemMatch, {[{<<"a">>, [3, 2]}]}), + ?assertMatch( + [ + {[{<<"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() -> + SelAllMatch = normalize( + {[ + {<<"a">>, + {[ + {<<"$allMatch">>, {[{<<"$gt">>, 4}]}} + ]}} + ]} + ), + + Fails0 = match_failures(SelAllMatch, {[{<<"a">>, [5]}]}), + ?assertEqual([], Fails0), + + Fails1 = match_failures(SelAllMatch, {[{<<"a">>, [4]}]}), + ?assertMatch( + [{[{<<"path">>, [<<"a">>, 0]}, {<<"message">>, <<"must be greater than 4">>}]}], + Fails1 + ), + + Fails2 = match_failures(SelAllMatch, {[{<<"a">>, [5, 6, 3, 7, 0]}]}), + ?assertMatch( + [ + {[{<<"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() -> + SelAllMatch = normalize( + {[ + {<<"a.b">>, + {[ + {<<"$allMatch">>, {[{<<"c">>, {[{<<"$gt">>, 4}]}}]}} + ]}} + ]} + ), + + Fails0 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}]}]}}]}), + ?assertEqual([], Fails0), + + Fails1 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 4}]}]}]}}]}), + ?assertMatch( + [ + {[ + {<<"path">>, [<<"a">>, <<"b">>, 0, <<"c">>]}, + {<<"message">>, <<"must be greater than 4">>} + ]} + ], + Fails1 + ), + + Fails2 = match_failures( + SelAllMatch, + {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 5}]}, {[{<<"c">>, 6}]}, {[{<<"c">>, 3}]}]}]}}]} + ), + ?assertMatch( + [ + {[ + {<<"path">>, [<<"a">>, <<"b">>, 2, <<"c">>]}, + {<<"message">>, <<"must be greater than 4">>} + ]} + ], + Fails2 + ), + + Fails3 = match_failures(SelAllMatch, {[{<<"a">>, {[{<<"b">>, [{[{<<"c">>, 1}]}, {[]}]}]}}]}), + ?assertMatch( + [ + {[ + {<<"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">>])). + +% The following benchmarks can be run by adding the following dependencies to +% rebar.config.script: +% +% {argparse, {url, "https://github.com/max-au/argparse"}, "1.2.4"}, +% {erlperf, {url, "https://github.com/max-au/erlperf"}, "2.3.0"} +% +% and then uncommenting the following functions and running the unit tests for +% this module via this command: +% +% make eunit apps=mango suites=mango_selector +% +% bench(Name, Selector, Doc) -> +% Sel1 = normalize(Selector), +% [Normal, Verbose] = erlperf:compare( +% [ +% #{runner => fun() -> match_int(Sel1, Doc, V) end} +% || V <- [false, true] +% ], +% #{} +% ), +% ?debugFmt("~nbench[~s: normal ] = ~p~n", [Name, Normal]), +% ?debugFmt("~nbench[~s: verbose] = ~p~n", [Name, Verbose]). +% +% bench_and_test() -> +% Sel = +% {[ +% {<<"x">>, +% {[ +% {<<"$and">>, [{[{<<"$gt">>, V}]} || V <- [100, 200, 300, 400, 500]]} +% ]}} +% ]}, +% Doc = {[{<<"x">>, 25}]}, +% bench("$and", Sel, Doc). +% +% bench_allmatch_test() -> +% Sel = +% {[ +% {<<"x">>, +% {[ +% {<<"$allMatch">>, {[{<<"$gt">>, 10}]}} +% ]}} +% ]}, +% Doc = +% {[ +% {<<"x">>, [0, 23, 45, 67, 89, 12, 34, 56, 78]} +% ]}, +% bench("$allMatch", Sel, Doc). -endif. diff --git a/test/elixir/test/config/suite.elixir b/test/elixir/test/config/suite.elixir index 21bfe3dc03b..1ea321414cf 100644 --- a/test/elixir/test/config/suite.elixir +++ b/test/elixir/test/config/suite.elixir @@ -526,12 +526,16 @@ "JavaScript VDU rejects an invalid document", "JavaScript VDU accepts a valid change", "JavaScript VDU rejects an invalid change", + "invalid JavaScript VDU is detected on doc update", + "invalid JavaScript VDU is rejected on design doc update", "Mango VDU accepts a valid document", "Mango VDU rejects an invalid document", "updating a Mango VDU updates its effects", "converting a Mango VDU to JavaScript updates its effects", "deleting a Mango VDU removes its effects", "Mango VDU rejects a doc if any existing ddoc fails to match", + "invalid Mango VDU is detected on doc update", + "Mango VDU rejects a design doc if it contains unknown fields", ], "SecurityValidationTest": [ "Author presence and user security", diff --git a/test/elixir/test/validate_doc_update_test.exs b/test/elixir/test/validate_doc_update_test.exs index 93ed8f177cf..253f16754c2 100644 --- a/test/elixir/test/validate_doc_update_test.exs +++ b/test/elixir/test/validate_doc_update_test.exs @@ -77,6 +77,36 @@ defmodule ValidateDocUpdateTest do assert resp.status_code == 403 end + @tag :with_db + test "invalid JavaScript VDU is detected on doc update", context do + Couch.put("/_node/_local/_config/couchdb/validate_vdu", body: "\"false\"") + db = context[:db_name] + + resp = Couch.put("/#{db}/_design/js-test", body: %{ + language: "javascript", + validate_doc_update: "function () {" + }) + + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc", body: %{a: 1}) + assert resp.status_code == 500 + end + + @tag :with_db + test "invalid JavaScript VDU is rejected on design doc update", context do + Couch.put("/_node/_local/_config/couchdb/validate_vdu", body: "\"true\"") + db = context[:db_name] + + resp = Couch.put("/#{db}/_design/js-test", body: %{ + language: "javascript", + validate_doc_update: "function () {" + }) + + assert resp.status_code == 400 + assert resp.body["error"] == "compilation_error" + end + @mango_type_check %{ language: "query", @@ -105,6 +135,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 @@ -209,4 +244,42 @@ defmodule ValidateDocUpdateTest do assert resp.status_code == 403 assert resp.body["error"] == "forbidden" end + + @tag :with_db + test "invalid Mango VDU is detected on doc update", context do + Couch.put("/_node/_local/_config/couchdb/validate_vdu", body: "\"false\"") + db = context[:db_name] + + ddoc = %{ + language: "query", + + validate_doc_update: %{ + "wrongField" => %{"year" => %{"$lt" => 2026}} + } + } + + resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc) + assert resp.status_code == 201 + + resp = Couch.put("/#{db}/doc", body: %{a: 1}) + assert resp.status_code == 500 + end + + @tag :with_db + test "Mango VDU rejects a design doc if it contains unknown fields", context do + Couch.put("/_node/_local/_config/couchdb/validate_vdu", body: "\"true\"") + db = context[:db_name] + + ddoc = %{ + language: "query", + + validate_doc_update: %{ + "wrongField" => %{"year" => %{"$lt" => 2026}} + } + } + + resp = Couch.put("/#{db}/_design/mango-test-2", body: ddoc) + assert resp.status_code == 400 + assert resp.body["error"] == "compilation_error" + end end