Current section
Files
Jump to
Current section
Files
lib/fuzzy_cast.ex
defmodule FuzzyCast do
@moduledoc """
Composing introspective %like% queries for Ecto.Schema fields.
```
FuzzyCast.compose(User, ~w(gmail yahoo bob))
```
FuzzyCast simply returns an `Ecto.Query` but can also accept an `Ecto.Query`.
This means we can pipe mulitple `FuzzyCast` calls.
For example, We might want to look for the match of "m" across all fields, and match for emails that include gmail and yahoo.
```
from(u in User)
|> FuzzyCast.compose(["gmail", "yahoo"], fields: [:email])
|> FuzzyCast.compose("m")
|> Repo.all
```
"""
import Ecto.Query, warn: false
alias __MODULE__
@enforce_keys [:schema, :terms]
defstruct base_query: nil,
fields: nil,
field_casts: [],
schema: nil,
search_query: nil,
terms: []
@doc """
`FuzzyCast.compose` returns an`Ecto.Query` for composition.
```
FuzzyCast.compose(User, ~w(gmail yahoo bob)) |> Repo.all
from(u in User, select: [:id, :email])
|> FuzzyCast.compose("gmail")
|> Repo.aggregate(:count, :id)
500
```
Pass fields to specifcy columns to search
```
FuzzyCast.compose(User, ["gmail", "yahoo"], fields: [:email])
```
"""
def compose(schema_or_query, terms, opts \\ [])
def compose(%Ecto.Query{} = ecto_q, terms, opts) do
{_table, schema} = ecto_q.from
build(schema, terms, opts ++ [base_query: ecto_q])
|> search_query()
end
def compose(schema, terms, opts) do
build(schema, terms, opts)
|> search_query()
end
def compose(%FuzzyCast{search_query: search_query}) when not is_nil(search_query) do
search_query
end
def compose(%FuzzyCast{search_query: search_query} = fuzzycast) when is_nil(search_query) do
do_build(fuzzycast) |> search_query
end
def build(schema, terms, opts \\ [])
def build(%Ecto.Query{} = ecto_q, terms, opts) do
{_table, schema} = ecto_q.from
build(schema, terms, opts ++ [base_query: ecto_q])
end
def build(schema, terms, opts) when is_list(terms) do
base_fuzzy(schema, terms, opts)
|> do_build()
end
def build(schema, term, opts) do
build(schema, [term], opts)
end
defp base_fuzzy(schema, terms, opts) do
%FuzzyCast{terms: terms, schema: schema, fields: opts[:fields], base_query: opts[:base_query]}
end
defp do_build(%FuzzyCast{} = fuzzycast) do
fuzzycast
|> gen_base_query()
|> query_fields()
|> build_search_query()
end
def search_query(%__MODULE__{} = fuzzycast) do
fuzzycast.search_query
end
defp gen_base_query(%FuzzyCast{schema: schema, base_query: base_query} = fuzzycast) do
case base_query do
nil -> %{fuzzycast | base_query: from(x in schema)}
_ -> fuzzycast
end
end
defp query_fields(%FuzzyCast{terms: terms} = fuzzycast) do
%{fuzzycast | field_casts: Enum.flat_map(terms, &map_fields_to_term(&1, fuzzycast))}
end
defp map_fields_to_term(term, fuzzycast) do
fuzzycast
|> map_query_fields(to_string(term))
|> Enum.reject(&(&1 == :error))
end
defp map_query_fields(fuzzycast, search_term) do
fields =
case fuzzycast.fields do
nil -> fuzzycast.schema.__schema__(:fields)
_ -> fuzzycast.fields
end
fields
|> Enum.filter(&strip_protected/1)
|> Enum.map(fn field ->
type = fuzzycast.schema.__schema__(:type, field)
case type do
nil ->
:error
_ ->
with {:ok, value} <- Ecto.Type.cast(type, search_term) do
[field: field, value: value, type: type]
else
:error -> :error
end
end
end)
end
defp strip_protected(field) do
unless to_string(field) =~ "password", do: field
end
defp build_search_query(%FuzzyCast{base_query: base_query, field_casts: fields} = fuzzycast) do
query =
case fields do
[] ->
base_query
_ ->
case Enum.count(base_query.wheres) do
0 ->
[head | tail] = fields
where_query = compose_where_query(head, base_query)
Enum.reduce(tail, where_query, &compose_or_where_query(&1, &2))
_ ->
Enum.reduce(fields, base_query, &compose_or_where_query(&1, &2))
end
end
%{fuzzycast | search_query: query}
end
defp compose_where_query(field_item, query) do
case field_item[:type] do
:string ->
value = "%" <> field_item[:value] <> "%"
from(q in query, where: ilike(field(q, ^field_item[:field]), ^value))
_ ->
from(q in query, where: field(q, ^field_item[:field]) == ^field_item[:value])
end
end
defp compose_or_where_query(field_item, query) do
case field_item[:type] do
:string ->
value = "%" <> field_item[:value] <> "%"
from(q in query, or_where: ilike(field(q, ^field_item[:field]), ^value))
_ ->
from(q in query, or_where: field(q, ^field_item[:field]) == ^field_item[:value])
end
end
end