Skip to content

Commit 8c667b8

Browse files
committed
[wip] normalise $data operators
1 parent 1c23788 commit 8c667b8

2 files changed

Lines changed: 160 additions & 14 deletions

File tree

src/mango/src/mango_selector.erl

Lines changed: 130 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,13 @@ match_int(Selector, D, Verbose) ->
8585
% Convert each operator into a normalized version as well
8686
% as convert an implicit operators into their explicit
8787
% versions.
88+
% {$Op: {$data: Path}}
89+
norm_ops({[{<<"$", _/binary>>, {[{<<"$data">>, Path}]}}]} = Cond) when is_binary(Path) ->
90+
norm_data(Cond);
91+
% {Field: {$data: Path}}
92+
norm_ops({[{Field, {[{<<"$data">>, Path}]}}]}) when is_binary(Path) ->
93+
Eq = norm_data({[{<<"$eq">>, {[{<<"$data">>, Path}]}}]}),
94+
{[{Field, Eq}]};
8895
norm_ops({[{<<"$and">>, Args}]}) when is_list(Args) ->
8996
{[{<<"$and">>, [norm_ops(A) || A <- Args]}]};
9097
norm_ops({[{<<"$and">>, Arg}]}) ->
@@ -128,8 +135,8 @@ norm_ops({[{<<"$regex">>, Regex}]} = Cond) when is_binary(Regex) ->
128135
_ ->
129136
?MANGO_ERROR({bad_arg, '$regex', Regex})
130137
end;
131-
norm_ops({[{<<"$all">>, Args}]}) when is_list(Args) ->
132-
{[{<<"$all">>, Args}]};
138+
norm_ops({[{<<"$all">>, Args}]} = Cond) when is_list(Args) ->
139+
Cond;
133140
norm_ops({[{<<"$all">>, Arg}]}) ->
134141
?MANGO_ERROR({bad_arg, '$all', Arg});
135142
norm_ops({[{<<"$elemMatch">>, {_} = Arg}]}) ->
@@ -144,8 +151,8 @@ norm_ops({[{<<"$keyMapMatch">>, {_} = Arg}]}) ->
144151
{[{<<"$keyMapMatch">>, norm_ops(Arg)}]};
145152
norm_ops({[{<<"$keyMapMatch">>, Arg}]}) ->
146153
?MANGO_ERROR({bad_arg, '$keyMapMatch', Arg});
147-
norm_ops({[{<<"$size">>, Arg}]}) when is_integer(Arg), Arg >= 0 ->
148-
{[{<<"$size">>, Arg}]};
154+
norm_ops({[{<<"$size">>, Arg}]} = Cond) when is_integer(Arg), Arg >= 0 ->
155+
Cond;
149156
norm_ops({[{<<"$size">>, Arg}]}) ->
150157
?MANGO_ERROR({bad_arg, '$size', Arg});
151158
norm_ops({[{<<"$text">>, Arg}]}) when
@@ -205,6 +212,37 @@ norm_ops({[_, _ | _] = Props}) ->
205212
norm_ops(Value) ->
206213
{[{<<"$eq">>, Value}]}.
207214

215+
% {$data: Path} may only be used as an argument to "leaf" operators that expect
216+
% a literal value as input. If it were combined with combinators like $and or
217+
% $allMatch it would allow the input document to inject its own selectors.
218+
-define(DATA_OPS, [
219+
<<"$eq">>,
220+
<<"$ne">>,
221+
<<"$lt">>,
222+
<<"$lte">>,
223+
<<"$gt">>,
224+
<<"$gte">>,
225+
<<"$in">>,
226+
<<"$nin">>,
227+
<<"$all">>,
228+
<<"$type">>,
229+
<<"$size">>,
230+
<<"$mod">>,
231+
<<"$regex">>,
232+
<<"$beginsWith">>
233+
]).
234+
235+
norm_data({[{Op, {[{<<"$data">>, Field}]}}]}) when is_binary(Field) ->
236+
case lists:member(Op, ?DATA_OPS) of
237+
true ->
238+
{ok, Path} = mango_util:parse_field(Field, relative),
239+
{[{Op, {[{<<"$data">>, Path}]}}]};
240+
false ->
241+
?MANGO_ERROR({bad_arg, '$data', Op})
242+
end;
243+
norm_data({[{_, _}]} = Cond) ->
244+
Cond.
245+
208246
% This takes a selector and normalizes all of the
209247
% field names as far as possible. For instance:
210248
%
@@ -899,6 +937,94 @@ fields({[]}) ->
899937
]}
900938
).
901939

940+
normalize_data_basic_test() ->
941+
Selector = normalize({[{<<"a">>, {[{<<"$data">>, <<"b">>}]}}]}),
942+
?assertEqual(
943+
{[
944+
{
945+
<<"a">>, {[{<<"$eq">>, {[{<<"$data">>, [<<"b">>]}]}}]}
946+
}
947+
]},
948+
Selector
949+
).
950+
951+
normalize_data_path_test() ->
952+
Selector = normalize({[{<<"a">>, {[{<<"$data">>, <<"b.c.42.d">>}]}}]}),
953+
?assertEqual(
954+
{[
955+
{
956+
<<"a">>, {[{<<"$eq">>, {[{<<"$data">>, [<<"b">>, <<"c">>, <<"42">>, <<"d">>]}]}}]}
957+
}
958+
]},
959+
Selector
960+
).
961+
962+
normalize_data_sibling_test() ->
963+
Selector = normalize({[{<<"a">>, {[{<<"$data">>, <<".b">>}]}}]}),
964+
?assertEqual(
965+
{[
966+
{
967+
<<"a">>, {[{<<"$eq">>, {[{<<"$data">>, [{[{<<"parent">>, 1}]}, <<"b">>]}]}}]}
968+
}
969+
]},
970+
Selector
971+
).
972+
973+
normalize_data_parent_test() ->
974+
Selector = normalize({[{<<"a">>, {[{<<"$data">>, <<"..b">>}]}}]}),
975+
?assertEqual(
976+
{[
977+
{
978+
<<"a">>, {[{<<"$eq">>, {[{<<"$data">>, [{[{<<"parent">>, 2}]}, <<"b">>]}]}}]}
979+
}
980+
]},
981+
Selector
982+
).
983+
984+
normalize_data_allowed_operator_test() ->
985+
Selector = normalize({[{<<"a">>, {[{<<"$lt">>, {[{<<"$data">>, <<"b">>}]}}]}}]}),
986+
?assertEqual(
987+
{[
988+
{
989+
<<"a">>, {[{<<"$lt">>, {[{<<"$data">>, [<<"b">>]}]}}]}
990+
}
991+
]},
992+
Selector
993+
).
994+
995+
normalize_data_allowed_operator_all_test() ->
996+
Selector = normalize({[{<<"a">>, {[{<<"$all">>, {[{<<"$data">>, <<"b">>}]}}]}}]}),
997+
?assertEqual(
998+
{[
999+
{
1000+
<<"a">>, {[{<<"$all">>, {[{<<"$data">>, [<<"b">>]}]}}]}
1001+
}
1002+
]},
1003+
Selector
1004+
).
1005+
1006+
normalize_data_disallowed_operator_test() ->
1007+
Selector = {[{<<"a">>, {[{<<"$allMatch">>, {[{<<"$data">>, <<"b">>}]}}]}}]},
1008+
Error = {mango_error, mango_selector, {bad_arg, '$data', <<"$allMatch">>}},
1009+
?assertException(throw, Error, normalize(Selector)).
1010+
1011+
normalize_data_multi_field_test() ->
1012+
Selector = normalize(
1013+
{[
1014+
{<<"a">>, {[{<<"$data">>, <<"c">>}]}},
1015+
{<<"b">>, {[{<<"$data">>, <<"c">>}]}}
1016+
]}
1017+
),
1018+
?assertEqual(
1019+
{[
1020+
{<<"$and">>, [
1021+
{[{<<"a">>, {[{<<"$eq">>, {[{<<"$data">>, [<<"c">>]}]}}]}}]},
1022+
{[{<<"b">>, {[{<<"$eq">>, {[{<<"$data">>, [<<"c">>]}]}}]}}]}
1023+
]}
1024+
]},
1025+
Selector
1026+
).
1027+
9021028
is_constant_field_basic_test() ->
9031029
Selector = normalize({[{<<"A">>, <<"foo">>}]}),
9041030
Field = <<"A">>,

src/mango/src/mango_util.erl

Lines changed: 30 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
join/2,
4242

4343
parse_field/1,
44+
parse_field/2,
4445

4546
cached_re/2
4647
]).
@@ -350,25 +351,44 @@ cached_re(Name, RE) ->
350351
end.
351352

352353
parse_field(Field) ->
353-
case binary:match(Field, <<"\\">>, []) of
354-
nomatch ->
355-
% Fast path, no regex required
356-
{ok, check_non_empty(Field, binary:split(Field, <<".">>, [global]))};
357-
_ ->
358-
parse_field_slow(Field)
359-
end.
354+
parse_field(Field, absolute).
355+
356+
parse_field(Field, Mode) ->
357+
Parts =
358+
case binary:match(Field, <<"\\">>, []) of
359+
nomatch ->
360+
% Fast path, no regex required
361+
binary:split(Field, <<".">>, [global]);
362+
_ ->
363+
parse_field_slow(Field)
364+
end,
365+
{Prefix, Path} =
366+
case Mode of
367+
absolute ->
368+
{[], Parts};
369+
relative ->
370+
lists:foldl(
371+
fun
372+
(<<>>, {[], []}) -> {[{[{<<"parent">>, 1}]}], []};
373+
(<<>>, {[{[{<<"parent">>, N}]}], []}) -> {[{[{<<"parent">>, N + 1}]}], []};
374+
(Pt, {Pre, Pts}) -> {Pre, Pts ++ [Pt]}
375+
end,
376+
{[], []},
377+
Parts
378+
)
379+
end,
380+
{ok, check_non_empty(Field, Prefix ++ Path)}.
360381

361382
parse_field_slow(Field) ->
362-
Path = lists:map(
383+
lists:map(
363384
fun
364385
(P) when P =:= <<>> ->
365386
?MANGO_ERROR({invalid_field_name, Field});
366387
(P) ->
367388
re:replace(P, <<"\\\\">>, <<>>, [global, {return, binary}])
368389
end,
369390
re:split(Field, <<"(?<!\\\\)\\.">>)
370-
),
371-
{ok, Path}.
391+
).
372392

373393
check_non_empty(Field, Parts) ->
374394
case lists:member(<<>>, Parts) of

0 commit comments

Comments
 (0)