Skip to content

Instantly share code, notes, and snippets.

@imranismail
Last active April 14, 2022 11:08
Show Gist options
  • Save imranismail/eb60c709b230c1cbf344553888b9387d to your computer and use it in GitHub Desktop.
Save imranismail/eb60c709b230c1cbf344553888b9387d to your computer and use it in GitHub Desktop.
Ecto as Parameter Validator
defmodule Web.InvalidParamsError do
defexception [:changeset, plug_status: 422]
def message(value) do
"""
Invalid parameters
#{inspect(error_messages(value.changeset))}
"""
end
defp error_messages(changeset) do
Ecto.Changeset.traverse_errors(changeset, fn {msg, opts} ->
Enum.reduce(opts, msg, fn {key, value}, acc ->
String.replace(acc, "%{#{key}}", to_string(value))
end)
end)
end
end
defmodule Web.Params do
defmacro __using__(_) do
quote do
import unquote(__MODULE__)
Module.register_attribute(__MODULE__, :params, accumulate: true)
@before_compile unquote(__MODULE__)
end
end
def from_schema(schema) do
schema
|> Map.from_struct()
|> Map.delete(:__struct__)
|> Enum.reduce(%{}, fn
{key, %_struct{} = schema}, acc ->
Map.put(acc, key, from_schema(schema))
{key, val}, acc ->
Map.put(acc, key, val)
end)
end
defmacro __before_compile__(env) do
for {action, module} <- Module.get_attribute(env.module, :params) do
quote do
defp __changeset__(action, schema, params) do
groups = schema.__struct__.__params__(:groups)
optional = schema.__struct__.__params__(:optional)
required = schema.__struct__.__params__(:required)
changeset =
schema
|> Ecto.Changeset.cast(params, optional ++ required)
|> Ecto.Changeset.validate_required(required)
Enum.reduce(groups, changeset, fn {key, opts}, changeset ->
opts = Keyword.put_new(opts, :with, fn schema, params ->
__changeset__(action, schema, params)
end)
Ecto.Changeset.cast_embed(changeset, key, opts)
end)
end
defp __params__(conn, unquote(action)) do
if conn.private.phoenix_action == unquote(action) do
changeset = __changeset__(unquote(action), struct(unquote(module)), conn.params)
if changeset.valid? do
params =
changeset
|> Ecto.Changeset.apply_changes()
|> Web.Params.from_schema()
%{conn | params: params}
else
raise Web.InvalidParamsError, changeset: %{changeset | action: :params}
end
else
conn
end
end
end
end
end
defmacro params(action, do: block) do
quote do
module = Module.concat(__MODULE__, Macro.camelize("#{unquote(action)}"))
@action module
@params {unquote(action), module}
plug :__params__, unquote(action)
defmodule module do
use Ecto.Schema
use Web.Params
@primary_key false
Module.register_attribute __MODULE__, :required, accumulate: true
Module.register_attribute __MODULE__, :optional, accumulate: true
Module.register_attribute __MODULE__, :groups, accumulate: true
embedded_schema do
unquote(block)
end
def __params__(:required), do: @required
def __params__(:optional), do: @optional
def __params__(:groups), do: @groups
end
end
end
defmacro requires(field, type) do
quote do
@required unquote(field)
field unquote(field), unquote(type)
end
end
defmacro optional(field, type) do
quote do
@optional unquote(field)
field unquote(field), unquote(type)
end
end
defmacro group(field, do: block) do
quote do
@groups {unquote(field), []}
params unquote(field) do
unquote(block)
end
embeds_one unquote(field), Module.concat(__MODULE__, Macro.camelize("#{unquote(field)}"))
end
end
defmacro group(field, opts, do: block) when is_list(opts) do
quote do
@groups {unquote(field), unquote(opts)}
params unquote(field) do
unquote(block)
end
embeds_one unquote(field), Module.concat(__MODULE__, Macro.camelize("#{unquote(field)}"))
end
end
end
defmodule Blog.Post.Query do
def build(params) do
Post
|> by_author_id(params[:author_id])
|> by_status(params[:status])
end
def by_author_id(queryable, nil), do: queryable
def by_author_id(queryable, author_id) do
from q in queryable, where: [author_id: ^author_id]
end
def by_status(queryable, nil), do: queryable
def by_status(queryable, status) do
from q in queryable, where: [status: ^status]
end
end
defmodule Web.PostController do
use Web, :controller
alias Blog.Repo
alias Blog.Post
params :index do
requires :author_id, :integer
optional :status, :string, one_of: ~w(published draft)
group :page do
optional :number, :integer
requires :size, :integer
end
end
def index(conn, params) do
render(conn, page: Repo.paginate(Post.Query.build(params), params))
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment