Current section

Files

Jump to
scrypath lib scrypath phoenix.ex
Raw

lib/scrypath/phoenix.ex

defmodule Scrypath.Phoenix do
@moduledoc """
Optional request-edge helpers for Phoenix controllers and LiveView.
This module stays pure and data-only. It delegates browser-param normalization to
`Scrypath.QueryParams`, projects attempted values plus issues for rendering, and
round-trips normalized state back into Plug-friendly query params.
It does not execute searches, call application contexts, or own socket lifecycle.
"""
alias Scrypath.QueryParams
alias Scrypath.QueryParams.Error
@type form_error :: %{
optional(:field) => String.t(),
code: atom(),
message: String.t(),
path: [String.t()],
meta: map()
}
@type form_data :: %{
values: map(),
params: map(),
form_errors: [form_error()],
field_errors: %{optional(String.t()) => [form_error()]},
errors: [form_error()]
}
@doc """
Delegates request-edge normalization to `Scrypath.QueryParams.normalize/1`.
"""
@spec from_params(map()) :: {:ok, QueryParams.t()} | {:error, QueryParams.normalize_error_map()}
def from_params(params) when is_map(params) do
QueryParams.normalize(params)
end
@doc """
Converts normalized query params back into canonical Plug-friendly params.
This helper supports only the browser grammar accepted by
`Scrypath.QueryParams.normalize/1`.
"""
@spec to_query_params(QueryParams.t()) :: map()
def to_query_params(%{text: _text} = query_params) do
if Map.get(query_params, :per_query, %{}) != %{} do
raise ArgumentError, "per_query is not part of the Phoenix query param grammar"
end
%{}
|> maybe_put("q", blank_to_nil(Map.get(query_params, :text, "")))
|> maybe_put("page", page_to_params(Map.get(query_params, :page, [])))
|> maybe_put("facets", facets_to_params(Map.get(query_params, :facets, [])))
|> maybe_put("filter", field_map_to_params(Map.get(query_params, :filter, [])))
|> maybe_put("facet_filter", field_map_to_params(Map.get(query_params, :facet_filter, [])))
|> maybe_put("sort", sort_to_params(Map.get(query_params, :sort, [])))
end
@doc """
Projects normalized query params into attempted values plus renderable issues.
"""
@spec to_form_data(QueryParams.t()) :: form_data()
def to_form_data(%{text: _text} = query_params) do
values = canonicalize_values(to_query_params(query_params))
%{
values: values,
params: values,
form_errors: [],
field_errors: %{},
errors: []
}
end
@doc """
Projects attempted request params plus structured request-edge issues.
"""
@spec to_form_data(map(), QueryParams.normalize_error_map()) :: form_data()
def to_form_data(params, %{form_errors: form_errors, field_errors: field_errors, errors: errors})
when is_map(params) do
values = canonicalize_values(params)
%{
values: values,
params: values,
form_errors: Enum.map(form_errors, &project_error/1),
field_errors:
Map.new(field_errors, fn {field, issues} ->
{Atom.to_string(field), Enum.map(issues, &project_error/1)}
end),
errors: Enum.map(errors, &project_error/1)
}
end
defp page_to_params([]), do: nil
defp page_to_params(page) when is_list(page) do
page
|> Enum.into(%{}, fn {key, value} -> {Atom.to_string(key), stringify_scalar(value)} end)
|> empty_map_to_nil()
end
defp facets_to_params([]), do: nil
defp facets_to_params(facets) when is_list(facets), do: Enum.map(facets, &stringify_scalar/1)
defp field_map_to_params([]), do: nil
defp field_map_to_params(fields) when is_list(fields) do
fields
|> Enum.into(%{}, fn {key, value} -> {Atom.to_string(key), stringify_value(value)} end)
|> empty_map_to_nil()
end
defp sort_to_params([]), do: nil
defp sort_to_params([{dir, field}]) when dir in [:asc, :desc] do
%{"field" => stringify_scalar(field), "dir" => Atom.to_string(dir)}
end
defp sort_to_params(sort) when is_list(sort) do
raise ArgumentError,
"sort must contain at most one entry for Phoenix query params: #{inspect(sort)}"
end
defp canonicalize_values(params) when is_map(params) do
%{}
|> Map.put("q", fetch_attempted_query(params))
|> maybe_put("page", nested_owned_value(params, :page))
|> maybe_put("facets", list_owned_value(params, :facets))
|> maybe_put("filter", nested_owned_value(params, :filter))
|> maybe_put("facet_filter", nested_owned_value(params, :facet_filter))
|> maybe_put("sort", nested_owned_value(params, :sort))
end
defp fetch_attempted_query(params) do
params
|> fetch_value(:q, "q", fetch_value(params, :text, "text", ""))
|> normalize_attempted_scalar()
end
defp nested_owned_value(params, atom_key) do
string_key = Atom.to_string(atom_key)
case fetch_value(params, atom_key, string_key, nil) do
nil -> nil
value -> stringify_nested(value)
end
end
defp list_owned_value(params, atom_key) do
string_key = Atom.to_string(atom_key)
case fetch_value(params, atom_key, string_key, nil) do
nil -> nil
value -> stringify_nested(value)
end
end
defp stringify_nested(value) when is_map(value) do
Map.new(value, fn {key, nested} -> {to_string(key), stringify_nested(nested)} end)
end
defp stringify_nested(value) when is_list(value), do: Enum.map(value, &stringify_nested/1)
defp stringify_nested(value), do: normalize_attempted_scalar(value)
defp normalize_attempted_scalar(nil), do: ""
defp normalize_attempted_scalar(value) when is_binary(value), do: value
defp normalize_attempted_scalar(value), do: to_string(value)
defp stringify_value(value) when is_list(value), do: Enum.map(value, &stringify_scalar/1)
defp stringify_value(value), do: stringify_scalar(value)
defp stringify_scalar(value) when is_atom(value), do: Atom.to_string(value)
defp stringify_scalar(value) when is_binary(value), do: value
defp stringify_scalar(value), do: to_string(value)
defp blank_to_nil(""), do: nil
defp blank_to_nil(value), do: value
defp maybe_put(map, _key, nil), do: map
defp maybe_put(map, key, value), do: Map.put(map, key, value)
defp empty_map_to_nil(map) when map == %{}, do: nil
defp empty_map_to_nil(map), do: map
defp fetch_value(params, atom_key, string_key, default) do
cond do
Map.has_key?(params, atom_key) -> Map.get(params, atom_key)
Map.has_key?(params, string_key) -> Map.get(params, string_key)
true -> default
end
end
defp project_error(%Error{} = error) do
base = %{
code: error.code,
message: error.message,
path: Enum.map(error.path, &to_string/1),
meta: error.meta
}
case error.field do
nil -> base
field -> Map.put(base, :field, Atom.to_string(field))
end
end
end