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 the following 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.Params` - an abstract module for parsing plug-like params from a query string into a filter
* `Filtrex.Fragment` - simple struct to hold generated expressions and values to be used when generating queries for ecto
* `Filtrex.Type.Config` - struct to hold various configuration and validation options for creating a filter
"""
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 `configs` option is a list of type configs (See `Filtrex.Type.Config`)
Example:
```
[%Filtrex.Type.Config{type: :text, keys: ~w(title comments)}]
```
"""
@spec parse([Filtrex.Type.Config.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(configs, %{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(configs, to_parse) do
{:ok, filter} -> {:cont, acc ++ [filter]}
{:errors, errors} -> {:halt, {:errors, errors}}
end
end
case parsed_filters do
{:errors, _} -> parsed_filters
_ -> parse_conditions(configs, type, conditions)
|> parse_condition_results(type, parsed_filters)
end
end
def parse(configs, %{filter: %{type: type, conditions: conditions}}) when is_list(conditions) do
parse(configs, %{filter: %{type: type, conditions: conditions, sub_filters: []}})
end
def parse(_, _), do: {:error, "Invalid filter structure"}
defp parse_conditions(configs, type, conditions) do
Enum.reduce(conditions, %{errors: [], conditions: []}, fn (map, acc) ->
case Filtrex.Condition.parse(configs, 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}, _, _) do
{:errors, errors}
end
@doc """
This function converts Plug-decoded params like the example below into a filtrex struct based on options in the configs.
```
%{"comments_contains" => "love",
"title" => "My Blog Post",
"created_at_between" => %{"start" => "2014-01-01", "end" => "2016-01-01"}}
```
"""
def parse_params(configs, params) do
case Filtrex.Params.parse_conditions(configs, params) do
{:ok, conditions} ->
{:ok, %Filtrex{type: "all", conditions: conditions}}
{:error, reason} ->
{:error, reason}
end
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) :: Ecto.Query.t
defmacro query(filter, model) do
quote do
Filtrex.AST.build_query(unquote(filter), unquote(model))
|> Code.eval_quoted([], __ENV__)
|> elem(0)
end
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