Current section
Files
Jump to
Current section
Files
lib/pageantry/input.ex
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