Last active
April 14, 2022 11:08
-
-
Save imranismail/eb60c709b230c1cbf344553888b9387d to your computer and use it in GitHub Desktop.
Ecto as Parameter Validator
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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