defmodule Pageantry.Input do @moduledoc """ User input for paging/sorting/filtering. ## Fields * `off` : Page offset start; `0` to start with the first item. * `max` : Maximum items per page, eg `10`. * `sort` : Keyword list of fields to sort by, eg `[asc: :name, desc: :created]`. * `filter` : Keyword list of fields to filter by, eg `[name: "foo", active: true]`. `ALL` is a special field that means filter across all fields, eg `[ALL: "today"]`. """ alias Pageantry.{Cast, Prefs} defstruct off: 0, max: 10, sort: [], filter: [] @type t :: %__MODULE__{off: integer, max: integer, sort: keyword(atom), filter: keyword(atom)} @doc """ Creates new Input struct. ## Examples iex> import Pageantry.Input iex> new(100) %Pageantry.Input{off: 0, max: 100, sort: [], filter: []} """ @spec new(integer) :: __MODULE__.t() def new(max \\ 10) do %__MODULE__{max: max} end @doc """ Creates new Input struct. ## Examples iex> import Pageantry.Input iex> new(100, [desc: :created], [created: "today"]) %Pageantry.Input{off: 0, max: 100, sort: [desc: :created], filter: [created: "today"]} """ @spec new(integer, keyword, keyword) :: __MODULE__.t() def new(max, sort \\ [], filter) do %__MODULE__{max: max, sort: sort, filter: filter} end @doc """ Parses request parameters into Input struct. ## Examples iex> import Pageantry.Input iex> parse(%{"off" => "100"}) %Pageantry.Input{off: 100, max: 10, sort: [], filter: []} iex> parse(%{"sort" => "name-created", "q" => "today"}) %Pageantry.Input{off: 0, max: 10, sort: [asc: :name, desc: :created], filter: [ALL: "today"]} """ @spec parse(map, Prefs.t()) :: __MODULE__.t() def parse(params, prefs \\ %Prefs{}) do parse(%__MODULE__{}, params, prefs) end @doc """ Parses request parameters into Input struct with defaults. ## Examples iex> import Pageantry.Input iex> parse(%Pageantry.Input{max: 20}, %{"off" => "100"}, %Pageantry.Prefs{}) %Pageantry.Input{off: 100, max: 20, sort: [], filter: []} iex> input = %Pageantry.Input{max: 20, sort: [desc: :created]} iex> params = %{"max" => "100", "field" => "name", "q" => "foo"} iex> parse(input, params, %Pageantry.Prefs{}) %Pageantry.Input{off: 0, max: 100, sort: [desc: :created], filter: [name: "foo"]} """ @spec parse(__MODULE__.t(), map, Prefs.t()) :: __MODULE__.t() def parse(input, params, prefs) do %__MODULE__{ off: parse_off(params, input.off, prefs), max: parse_max(params, input.max, prefs), sort: parse_sort(params, input.sort, prefs), filter: parse_filter(params, input.filter, prefs) } end @doc """ Extracts "off" parameter as non-negative integer. ## Examples iex> import Pageantry.Input iex> parse_off(%{"off" => "100"}) 100 iex> parse_off(%{"off" => ""}) 0 """ @spec parse_off(map, integer, Prefs.t()) :: integer def parse_off(params, default \\ 0, prefs \\ %Prefs{}) do case Cast.cast_to_integer(params["off"], prefs) do number when is_integer(number) and number >= 0 -> number _ -> default end end @doc """ Extracts "max" parameter as positive integer. ## Examples iex> import Pageantry.Input iex> parse_max(%{"max" => "100"}) 100 iex> parse_max(%{"max" => ""}, 10) 10 """ @spec parse_max(map, integer, Prefs.t()) :: integer def parse_max(params, default \\ 10, prefs \\ %Prefs{}) do case Cast.cast_to_integer(params["max"], prefs) do number when is_integer(number) and number > 0 -> number _ -> default end end @doc """ Extracts "sort" parameter as sort keyword list. ## Examples iex> import Pageantry.Input iex> parse_sort(%{"sort" => "name"}) [asc: :name] iex> parse_sort(%{"sort" => "-name"}) [desc: :name] iex> parse_sort(%{"sort" => ""}) [] """ @spec parse_sort(map, keyword(atom), Prefs.t()) :: keyword(atom) def parse_sort(params, default \\ [], prefs \\ %Prefs{}) do case params["sort"] do nil -> default "" -> [] x -> parse_sort_value(x, prefs) end end @spec parse_sort_value(String.t(), Prefs.t()) :: keyword(atom) defp parse_sort_value(x, _prefs) do Regex.scan(~r/([- ])?([^- ]+)/, x) |> Enum.map(fn [_, "-", field] -> {:desc, String.to_existing_atom(field)} [_, _, field] -> {:asc, String.to_existing_atom(field)} end) end @doc """ Extracts "field" and "q" parameters as filter keyword list. ## Examples iex> import Pageantry.Input iex> parse_filter(%{"field" => "created", "q" => "today"}) [created: "today"] iex> parse_filter(%{"q" => "today"}) [ALL: "today"] iex> parse_filter(%{"q" => ""}) [] """ @spec parse_filter(map, keyword(String.t()), Prefs.t()) :: keyword(String.t()) def parse_filter(params, default \\ [], prefs \\ %Prefs{}) do case params["q"] do nil -> default "" -> [] x -> parse_filter_value(x, params["field"], prefs) end end @spec parse_filter_value(String.t(), String.t(), Prefs.t()) :: keyword(String.t()) defp parse_filter_value(x, nil, _prefs), do: [ALL: x] defp parse_filter_value(x, field, _prefs), do: [{String.to_existing_atom(field), x}] @doc """ Builds URL with paging params in query string. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> to_url(%Input{off: 10, max: 20, sort: [asc: :name], filter: [ALL: "today"]}) "?max=20&off=10&q=today&sort=name" iex> to_url(%Input{}) "" """ @spec to_url(__MODULE__.t(), integer) :: String.t() def to_url(input, default_max \\ 10) do case to_params(input, default_max) do params when map_size(params) == 0 -> "" params -> "?#{URI.encode_query(params)}" end end @doc """ Builds paging params map. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> to_params(%Input{off: 10, max: 20, sort: [asc: :name], filter: [ALL: "today"]}) %{"max" => "20", "off" => "10", "q" => "today", "sort" => "name"} iex> to_params(%Input{}) %{} """ @spec to_params(__MODULE__.t(), integer) :: map def to_params(input, default_max \\ 10) do add_params(%{}, input, default_max) end @doc """ Adds paging to existing params map. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> add_params(%{}, %Input{off: 10, max: 20, sort: [asc: :name], filter: [ALL: "today"]}) %{"max" => "20", "off" => "10", "q" => "today", "sort" => "name"} iex> add_params(%{}, %Input{}) %{} """ @spec add_params(map, __MODULE__.t(), integer) :: map def add_params(params, input, default_max \\ 10) do params |> add_off_param(input) |> add_max_param(input, default_max) |> add_sort_param(input) |> add_filter_param(input) end @doc """ Adds offset parameter to map of paging params. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> add_off_param(%{}, %Input{off: 10}) %{"off" => "10"} iex> add_off_param(%{}, %Input{off: 0}) %{} """ @spec add_off_param(map, __MODULE__.t()) :: map def add_off_param(params, %{off: 0}), do: Map.delete(params, "off") def add_off_param(params, %{off: off}), do: Map.put(params, "off", to_string(off)) @doc """ Adds items-per-page parameter to map of paging params. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> add_max_param(%{}, %Input{max: 100}, 10) %{"max" => "100"} iex> add_max_param(%{}, %Input{max: 100}, 100) %{} """ @spec add_max_param(map, __MODULE__.t(), integer) :: map def add_max_param(params, %{max: max}, default_max) when max == default_max do Map.delete(params, "max") end def add_max_param(params, %{max: max}, _), do: Map.put(params, "max", to_string(max)) @doc """ Adds sort parameter to map of paging params. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> add_sort_param(%{}, %Input{sort: [asc: :name]}) %{"sort" => "name"} iex> add_sort_param(%{}, %Input{sort: [desc: :name]}) %{"sort" => "-name"} iex> add_sort_param(%{}, %Input{}) %{} """ @spec add_sort_param(map, __MODULE__.t()) :: map def add_sort_param(params, %{sort: []}), do: Map.delete(params, "sort") def add_sort_param(params, %{sort: sort}) do value = add_sort_param_value("", sort) Map.put(params, "sort", value) end @spec add_sort_param_value(String.t(), keyword) :: String.t() defp add_sort_param_value(value, []), do: value defp add_sort_param_value("", [{:asc, field} | rest]) do add_sort_param_value("#{field}", rest) end defp add_sort_param_value(value, [{:asc, field} | rest]) do add_sort_param_value("#{value} #{field}", rest) end defp add_sort_param_value(value, [{:desc, field} | rest]) do add_sort_param_value("#{value}-#{field}", rest) end @doc """ Adds filter parameter to map of paging params. ## Examples iex> import Pageantry.Input iex> alias Pageantry.Input iex> add_filter_param(%{}, %Input{filter: [created: "today"]}) %{"field" => "created", "q" => "today"} iex> add_filter_param(%{}, %Input{filter: [ALL: "today"]}) %{"q" => "today"} iex> add_filter_param(%{}, %Input{}) %{} """ @spec add_filter_param(map, __MODULE__.t()) :: map def add_filter_param(params, %{filter: []}), do: Map.drop(params, ["field", "q"]) def add_filter_param(params, %{filter: [ALL: value]}) do params |> Map.delete("field") |> Map.put("q", value) end def add_filter_param(params, %{filter: [{field, value}]}) do Map.merge(params, %{"field" => to_string(field), "q" => value}) end end