summaryrefslogtreecommitdiff
path: root/deps/goldrush/src/glc_lib.erl
diff options
context:
space:
mode:
Diffstat (limited to 'deps/goldrush/src/glc_lib.erl')
-rw-r--r--deps/goldrush/src/glc_lib.erl409
1 files changed, 409 insertions, 0 deletions
diff --git a/deps/goldrush/src/glc_lib.erl b/deps/goldrush/src/glc_lib.erl
new file mode 100644
index 0000000..c0ac3e5
--- /dev/null
+++ b/deps/goldrush/src/glc_lib.erl
@@ -0,0 +1,409 @@
+%% Copyright (c) 2012, Magnus Klaar <klaar@ninenines.eu>
+%%
+%% Permission to use, copy, modify, and/or distribute this software for any
+%% purpose with or without fee is hereby granted, provided that the above
+%% copyright notice and this permission notice appear in all copies.
+%%
+%% THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
+%% WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
+%% MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
+%% ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
+%% WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
+%% ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
+%% OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
+
+%% @doc Query processing functions.
+-module(glc_lib).
+
+-export([
+ reduce/1,
+ matches/2,
+ onoutput/1,
+ onoutput/2
+]).
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+-undef(LET).
+-ifdef(PROPER).
+-include_lib("proper/include/proper.hrl").
+-endif.
+-endif.
+
+%% @doc Return a reduced version of a query.
+%%
+%% The purpose of this function is to ensure that a query filter
+%% is in the simplest possible form. The query filter returned
+%% from this function is functionally equivalent to the original.
+reduce(Query) ->
+ repeat(Query, fun(Q0) ->
+ Q1 = repeat(Q0, fun flatten/1),
+ Q2 = required(Q1),
+ Q3 = repeat(Q2, fun flatten/1),
+ Q4 = common(Q3),
+ Q5 = repeat(Q4, fun flatten/1),
+ Q6 = constants(Q5),
+ Q6
+ end).
+
+
+%% @doc Test if an event matches a query.
+%% This function is only intended to be used for testing purposes.
+matches({any, Conds}, Event) ->
+ lists:any(fun(Cond) -> matches(Cond, Event) end, Conds);
+matches({all, Conds}, Event) ->
+ lists:all(fun(Cond) -> matches(Cond, Event) end, Conds);
+matches({null, Const}, _Event) ->
+ Const;
+matches({Key, '<', Term}, Event) ->
+ case gre:find(Key, Event) of
+ {true, Term2} -> Term2 < Term;
+ false -> false
+ end;
+matches({Key, '=', Term}, Event) ->
+ case gre:find(Key, Event) of
+ {true, Term2} -> Term2 =:= Term;
+ false -> false
+ end;
+matches({Key, '>', Term}, Event) ->
+ case gre:find(Key, Event) of
+ {true, Term2} -> Term2 > Term;
+ false -> false
+ end;
+matches({Key, '*'}, Event) ->
+ case gre:find(Key, Event) of
+ {true, _} -> true;
+ false -> false
+ end;
+matches({Key, '!'}, Event) ->
+ not matches({Key, '*'}, Event).
+
+%% @private Repeatedly apply a function to a query.
+%% This is used for query transformation functions that must be applied
+%% multiple times to yield the simplest possible version of a query.
+repeat(Query, Fun) ->
+ case Fun(Query) of
+ Query -> Query;
+ Query2 -> repeat(Query2, Fun)
+ end.
+
+
+%% @doc Return the output action of a query.
+-spec onoutput(glc_ops:op()) -> output | no_return().
+onoutput({_, '<', _}) ->
+ output;
+onoutput({_, '=', _}) ->
+ output;
+onoutput({_, '>', _}) ->
+ output;
+onoutput({_, '*'}) ->
+ output;
+onoutput({_, '!'}) ->
+ output;
+onoutput(Query) ->
+ erlang:error(badarg, [Query]).
+
+%% @doc Modify the output action of a query.
+-spec onoutput(Action :: any(), Query :: glc_ops:op()) -> no_return().
+onoutput(Action, Query) ->
+ erlang:error(badarg, [Action, Query]).
+
+
+%% @private Flatten a condition tree.
+flatten({all, [Cond]}) ->
+ Cond;
+flatten({any, [Cond]}) ->
+ Cond;
+flatten({all, Conds}) ->
+ flatten_all([flatten(Cond) || Cond <- Conds]);
+flatten({any, [_|_]=Conds}) ->
+ flatten_any([flatten(Cond) || Cond <- Conds]);
+flatten({with, Cond, Action}) ->
+ {with, flatten(Cond), Action};
+flatten([{with, _Cond, _Action}|_] = I) ->
+ [{with, flatten(Cond), Action} || {with, Cond, Action} <- I];
+flatten(Other) ->
+ valid(Other).
+
+
+%% @private Flatten and remove duplicate members of an "all" filter.
+flatten_all(Conds) ->
+ {all, lists:usort(flatten_tag(all, Conds))}.
+
+%% @private Flatten and remove duplicate members of an "any" filter.
+flatten_any(Conds) ->
+ {any, lists:usort(flatten_tag(any, Conds))}.
+
+%% @private Common function for flattening "all" or "and" filters.
+flatten_tag(Tag, [{Tag, Conds}|T]) ->
+ Conds ++ flatten_tag(Tag, T);
+flatten_tag(Tag, [H|T]) ->
+ [H|flatten_tag(Tag, T)];
+flatten_tag(_Tag, []) ->
+ [].
+
+%% @private Factor out required filters.
+%%
+%% Identical conditions may occur in all sub-filters of an "any" filter. These
+%% filters can be tested once before testing the conditions that are unique to
+%% each sub-filter.
+%%
+%% Assume that the input has been flattened first in order to eliminate all
+%% occurances of an "any" filters being "sub-filters" of "any" filters.
+required({any, [H|_]=Conds}) ->
+ Init = ordsets:from_list(case H of {all, Init2} -> Init2; H -> [H] end),
+ case required(Conds, Init) of
+ nonefound ->
+ Conds2 = [required(Cond) || Cond <- Conds],
+ {any, Conds2};
+ {found, Req} ->
+ Conds2 = [required(deleteall(Cond, Req)) || Cond <- Conds],
+ {all, [{all, Req}, {any, Conds2}]}
+ end;
+required({all, Conds}) ->
+ {all, [required(Cond) || Cond <- Conds]};
+required(Other) ->
+ Other.
+
+required([{all, Conds}|T], Acc) ->
+ required(T, ordsets:intersection(ordsets:from_list(Conds), Acc));
+required([{any, _}|_]=Cond, Acc) ->
+ erlang:error(badarg, [Cond, Acc]);
+required([H|T], Acc) ->
+ required(T, ordsets:intersection(ordsets:from_list([H]), Acc));
+required([], [_|_]=Req) ->
+ {found, Req};
+required([], []) ->
+ nonefound.
+
+%% @private Factor our common filters.
+%%
+%% Identical conditions may occur in some sub-filters of an "all" filter. These
+%% filters can be tested once before testing the conditions that are unique to
+%% each sub-filter. This is different from factoring out common sub-filters
+%% in an "any" filter where the only those sub-filters that exist in all
+%% members.
+%%
+%% Assume that the input has been flattened first in order to eliminate all
+%% occurances of an "any" filters being "sub-filters" of "any" filters.
+common({all, Conds}) ->
+ case common_(Conds, []) of
+ {found, Found} ->
+ {all, [Found|[delete(Cond, Found) || Cond <- Conds]]};
+ nonefound ->
+ {all, [common(Cond) || Cond <- Conds]}
+ end;
+common({any, Conds}) ->
+ {any, [common(Cond) || Cond <- Conds]};
+common(Other) ->
+ Other.
+
+
+common_([{any, Conds}|T], Seen) ->
+ Set = ordsets:from_list(Conds),
+ case ordsets:intersection(Set, Seen) of
+ [] -> common_(T, ordsets:union(Set, Seen));
+ [H|_] -> {found, H}
+ end;
+common_([H|T], Seen) ->
+ case ordsets:is_element(H, Seen) of
+ false -> common_(T, ordsets:union(ordsets:from_list([H]), Seen));
+ true -> {found, H}
+ end;
+common_([], _Seen) ->
+ nonefound.
+
+%% @private Delete all occurances of constants.
+%%
+%% An "all" or "any" filter may be reduced to a constant outcome when all
+%% sub-filters has been factored out from the filter. In these cases the
+%% filter can be removed from the query.
+constants(Query) ->
+ delete(Query, {null, true}).
+
+
+
+%% @private Delete all occurances of a filter.
+%%
+%% Assume that the function is called because a filter is tested
+%% by a parent filter. It is therefore safe to replace occurances
+%% with a null filter that always returns true.
+delete({all, Conds}, Filter) ->
+ {all, [delete(Cond, Filter) || Cond <- Conds, Cond =/= Filter]};
+delete({any, Conds}, Filter) ->
+ {any, [delete(Cond, Filter) || Cond <- Conds, Cond =/= Filter]};
+delete(Filter, Filter) ->
+ {null, true};
+delete(Cond, _Filter) ->
+ Cond.
+
+%% @private Delete all occurances of multiple filters.
+deleteall(Filter, [H|T]) ->
+ deleteall(delete(Filter, H), T);
+deleteall(Filter, []) ->
+ Filter.
+
+
+
+%% @private Test if a term is a valid filter.
+-spec is_valid(glc_ops:op()) -> boolean().
+is_valid({Field, '<', _Term}) when is_atom(Field) ->
+ true;
+is_valid({Field, '=<', _Term}) when is_atom(Field) ->
+ true;
+is_valid({Field, '=', _Term}) when is_atom(Field) ->
+ true;
+is_valid({Field, '>=', _Term}) when is_atom(Field) ->
+ true;
+is_valid({Field, '>', _Term}) when is_atom(Field) ->
+ true;
+is_valid({Field, '*'}) when is_atom(Field) ->
+ true;
+is_valid({Field, '!'}) when is_atom(Field) ->
+ true;
+is_valid({null, true}) ->
+ true;
+is_valid({null, false}) ->
+ true;
+is_valid(_Other) ->
+ false.
+
+%% @private Assert that a term is a valid filter.
+%% If the term is a valid filter. The original term will be returned.
+%% If the term is not a valid filter. A `badarg' error is thrown.
+valid(Term) ->
+ is_valid(Term) orelse erlang:error(badarg, [Term]),
+ Term.
+
+
+-ifdef(TEST).
+-include_lib("eunit/include/eunit.hrl").
+
+all_one_test() ->
+ ?assertEqual(glc:eq(a, 1),
+ glc_lib:reduce(glc:all([glc:eq(a, 1)]))
+ ).
+
+all_sort_test() ->
+ ?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc_lib:reduce(glc:all([glc:eq(b, 2), glc:eq(a, 1)]))
+ ).
+
+any_one_test() ->
+ ?assertEqual(glc:eq(a, 1),
+ glc_lib:reduce(glc:any([glc:eq(a, 1)]))
+ ).
+
+all_two_test() ->
+ ?assertEqual(glc_lib:reduce(glc:all([glc:wc(a), glc:nf(b)])),
+ glc_lib:reduce(glc:any([
+ glc:all([glc:wc(a)]),
+ glc:all([glc:wc(a), glc:nf(b)])]))
+ ).
+
+any_sort_test() ->
+ ?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc_lib:reduce(glc:any([glc:eq(b, 2), glc:eq(a, 1)]))
+ ).
+
+all_nest_test() ->
+ ?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc_lib:reduce(glc:all([glc:eq(a, 1), glc:all([glc:eq(b, 2)])]))
+ ),
+ ?assertEqual(glc:all([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]),
+ glc_lib:reduce(glc:all([glc:eq(c, 3),
+ glc:all([glc:eq(a, 1),
+ glc:all([glc:eq(b, 2)])])]))
+ ).
+
+any_nest_test() ->
+ ?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc_lib:reduce(glc:any([glc:eq(a, 1), glc:any([glc:eq(b, 2)])]))
+ ),
+ ?assertEqual(glc:any([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]),
+ glc_lib:reduce(glc:any([glc:eq(c, 3),
+ glc:any([glc:eq(a, 1),
+ glc:any([glc:eq(b, 2)])])]))
+ ).
+
+all_equiv_test() ->
+ ?assertEqual(glc:eq(a, 1),
+ glc_lib:reduce(glc:all([glc:eq(a, 1), glc:eq(a, 1)]))
+ ).
+
+any_equiv_test() ->
+ ?assertEqual(glc:eq(a, 1),
+ glc_lib:reduce(glc:any([glc:eq(a, 1), glc:eq(a, 1)]))
+ ).
+
+any_required_test() ->
+ ?assertEqual(
+ glc:all([
+ glc:any([glc:nf(d), glc:eq(b, 2), glc:eq(c, 3)]),
+ glc:eq(a, 1)
+ ]),
+ glc_lib:reduce(
+ glc:any([
+ glc:all([glc:eq(a, 1), glc:nf(d)]),
+ glc:all([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc:all([glc:eq(a, 1), glc:eq(c, 3)])]))
+ ).
+
+
+all_common_test() ->
+ ?assertEqual(
+ glc:all([glc:eq(a, 1), glc:eq(b, 2), glc:eq(c, 3)]),
+ glc_lib:reduce(
+ glc:all([
+ glc:any([glc:eq(a, 1), glc:eq(b, 2)]),
+ glc:any([glc:eq(a, 1), glc:eq(c, 3)])]))
+ ).
+
+delete_from_all_test() ->
+ ?assertEqual(
+ glc:all([glc:eq(b,2)]),
+ deleteall(
+ glc:all([glc:eq(a, 1),glc:eq(b,2)]), [glc:eq(a, 1), glc:nf(a)])
+ ).
+
+delete_from_any_test() ->
+ ?assertEqual(
+ glc:any([glc:eq(b,2)]),
+ deleteall(
+ glc:any([glc:eq(a, 1),glc:eq(b,2)]), [glc:eq(a, 1), glc:wc(a)])
+ ).
+
+default_is_output_test_() ->
+ [?_assertEqual(output, glc_lib:onoutput(glc:lt(a, 1))),
+ ?_assertEqual(output, glc_lib:onoutput(glc:eq(a, 1))),
+ ?_assertEqual(output, glc_lib:onoutput(glc:gt(a, 1))),
+ ?_assertEqual(output, glc_lib:onoutput(glc:wc(a))),
+ ?_assertEqual(output, glc_lib:onoutput(glc:nf(a)))
+ ].
+
+-ifdef(PROPER).
+
+
+prop_reduce_returns() ->
+ ?FORALL(Query, glc_ops:op(),
+ returns(fun() -> glc_lib:reduce(Query) end)).
+
+reduce_returns_test() ->
+ ?assert(proper:quickcheck(prop_reduce_returns())).
+
+prop_matches_returns_boolean() ->
+ ?FORALL({Query, Event}, {glc_ops:op(), [{atom(), term()}]},
+ is_boolean(glc_lib:matches(Query, gre:make(Event, [list])))).
+
+matches_returns_boolean_test() ->
+ ?assert(proper:quickcheck(prop_matches_returns_boolean())).
+
+returns(Fun) ->
+ try Fun(),
+ true
+ catch _:_ ->
+ false
+ end.
+
+-endif.
+-endif.