Packages

A library for performing and validating complex filters from a client (e.g. smart filters)

Current section

Files

Jump to
filtrex lib filtrex.ex
Raw

lib/filtrex.ex

defmodule Filtrex do
@moduledoc """
Filtrex consists of three primary components:
* `Filtrex` - handles the overall parsing of filters and delegates to
`Filtrex.AST` to build an ecto query expression
* `Filtrex.Condition` - an abstract module built to delegate to specific condition modules in the format of `Filtrex.Condition.Type` where the type is converted to CamelCase (See `Filtrex.Condition.Text.parse/2`)
* `Filtrex.Fragment` - simple struct to hold generated expressions and values to be used when generating queries for ecto
"""
defstruct type: nil, conditions: [], sub_filters: []
@type t :: Filtrex.t
@doc """
Parses a filter expression and returns errors or the parsed filter with
the appropriate parsed sub-structures.
The `config` option is a map of the acceptable types and the configuration
options to pass to each condition type.
Example:
```
%{
text: %{keys: ~w(title comments)}
}
```
"""
@spec parse(Map.t, Map.t) :: {:errors, List.t} | {:ok, Filtrex.t}
def parse(_, %{filter: %{type: type}}) when not type in ~w(all any none) do
{:errors, ["Invalid filter type #{type}"]}
end
def parse(_, %{filter: %{conditions: []}}) do
{:errors, ["One or more conditions required to filter"]}
end
def parse(config, %{filter: %{type: type, conditions: conditions, sub_filters: sub_filters}}) when is_list(conditions) do
parsed_filters = Enum.reduce_while sub_filters, [], fn (to_parse, acc) ->
case parse(config, to_parse) do
{:ok, filter} -> {:cont, acc ++ [filter]}
{:errors, errors} -> {:halt, {:errors, errors}}
end
end
case parsed_filters do
{:errors, _} -> parsed_filters
_ -> parse_conditions(config, type, conditions)
|> parse_condition_results(type, parsed_filters)
end
end
def parse(config, %{filter: %{type: type, conditions: conditions}}) when is_list(conditions) do
parse(config, %{filter: %{type: type, conditions: conditions, sub_filters: []}})
end
def parse(_, _), do: {:error, "Invalid filter structure"}
defp parse_conditions(config, type, conditions) do
Enum.reduce(conditions, %{errors: [], conditions: []}, fn (map, acc) ->
case Filtrex.Condition.parse(config, Map.put(map, :inverse, inverse_for(type))) do
{:error, error} ->
update_list_in_map(acc, :errors, error)
{:ok, condition} ->
update_list_in_map(acc, :conditions, condition)
end
end)
end
defp parse_condition_results(%{errors: [], conditions: conditions}, type, parsed_filters) do
{:ok, %Filtrex{type: type, conditions: conditions, sub_filters: parsed_filters}}
end
defp parse_condition_results(%{errors: errors, conditions: []}, _, _) do
{:errors, errors}
end
@doc """
Converts a filter with the specified ecto module name into a valid ecto query
expression that is compiled when called.
"""
@spec query(Filter.t, module, Macro.Env.t) :: Ecto.Query.t
def query(filter, model, env) do
Filtrex.AST.build_query(filter, model)
|> Code.eval_quoted([], env)
|> elem(0)
end
defp inverse_for("none"), do: true
defp inverse_for(_), do: false
defp update_list_in_map(map, key, value) do
values = Map.get(map, key)
Map.put(map, key, values ++ [value])
end
end