Skip to content

Instantly share code, notes, and snippets.

@andreasknoepfle
Last active January 24, 2023 09:37
Show Gist options
  • Save andreasknoepfle/794f9c3c8917f119f44a42b849c05bd6 to your computer and use it in GitHub Desktop.
Save andreasknoepfle/794f9c3c8917f119f44a42b849c05bd6 to your computer and use it in GitHub Desktop.
Custom Flop filters
defmodule Flop.CustomFilters do
@default_op :==
@type filter :: atom() | {field :: atom(), op :: atom()}
@type filter_value :: binary() | nil
@type filter_values :: %{required(filter()) => filter_value()}
@doc """
Remove a filter from the params for flop, so flop does not handle them
anymore. This can be used to implement missing features in flop filters.
pop_filter(%{"filters" => %{"0" => %{"field" => "search", "value" => "foo", "op" => "ilike"}
"1" => %{"field" => "other", "value" => "bar"}},
"order_by" => ["reference"], ...},
{:search, :ilike})
=> {"foo", %{"filters" => %{"1" => %{"field" => "other", "value" => "bar"}},
"order_by" => ["reference"], ...}}
"""
@spec pop_filter(params :: map(), filter :: filter()) ::
{filter_value(), params_without_filter :: map()}
def pop_filter(params, field) when is_atom(field) or is_binary(field) do
pop_filter(params, {field, @default_op})
end
def pop_filter(%{"filters" => map_of_indexed_filters} = params, field_and_op) do
{value, updated_list_of_indexed_filters} =
map_of_indexed_filters
|> Enum.into([])
|> do_pop_filter(field_and_op, fn {_index, filter}, key ->
filter[to_string(key)]
end)
{value, %{params | "filters" => Map.new(updated_list_of_indexed_filters)}}
end
def pop_filter(%{filters: list_of_filters} = params, field_and_op) do
{value, updated_list_of_filters} =
do_pop_filter(list_of_filters, field_and_op, fn filter, key ->
filter[key]
end)
{value, %{params | filters: updated_list_of_filters}}
end
def pop_filter(params, _field_and_op) do
{nil, params}
end
defp do_pop_filter([], _field_and_op, _filter_get_fun), do: {nil, []}
defp do_pop_filter([filter | rest], field_and_op, filter_get_fun) do
if filter_matches?(filter, field_and_op, filter_get_fun) do
{filter_get_fun.(filter, :value), rest}
else
{result, rest} = do_pop_filter(rest, field_and_op, filter_get_fun)
{result, [filter | rest]}
end
end
defp filter_matches?(filter, {field, op}, filter_get_fun) do
field_in_filter = to_string(filter_get_fun.(filter, :field))
op_in_filter = to_string(filter_get_fun.(filter, :op) || "==")
to_string(field) == field_in_filter && to_string(op) == op_in_filter
end
@doc """
Uses pop_filter/2 on multiple filters and returns a map of {field, op} => values for
the poped filters.
"""
@spec pop_filters(params :: map(), [filter()]) ::
{filter_values(), params_without_filter :: map()}
def pop_filters(all_params, filters) do
Enum.reduce(filters, {%{}, all_params}, fn
filter, {filter_values, params} ->
{value, params} = pop_filter(params, filter)
if value in [nil, ""] do
{filter_values, params}
else
{Map.put(filter_values, filter, value), params}
end
end)
end
@doc """
This can be used to push a filter that was poped before onto the metadata of flop,
so the form field value is still filled out even though the filter is handled customly.
"""
@spec push_filter(flop :: Flop.Meta.t(), filter(), filter_value()) :: Flop.Meta.t()
def push_filter(meta, {field, op}, value) do
Map.update!(meta, :flop, fn flop ->
Map.update!(flop, :filters, fn filters ->
[%Flop.Filter{field: field, op: op, value: value} | filters]
end)
end)
end
def push_filter(meta, field, value), do: push_filter(meta, {field, @default_op}, value)
@doc """
Same as push_filter but for multiple values. Can be called with the filter_values
result of `pop_filters/2`, to push back the poped filters.
"""
@spec push_filters(flop :: Flop.Meta.t(), filter_values()) :: Flop.Meta.t()
def push_filters(meta, filter_values) do
Enum.reduce(filter_values, meta, fn {filter, value}, meta ->
push_filter(meta, filter, value)
end)
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment