Current section
Files
Jump to
Current section
Files
lib/pharams.ex
defmodule Pharams do
@moduledoc """
Functions and macros for validating requests to Phoenix
Controllers.
"""
alias Pharams.Utils
@doc """
Takes a nested struct data structure and turns it into a map with
atoms as keys.
"""
def schema_to_atom_map(map) when is_map(map) do
map
|> Map.delete(:__struct__)
|> Map.delete(:__meta__)
|> Enum.map(fn
{key, %{__struct__: _} = value} ->
{key, schema_to_atom_map(value)}
{key, val} when is_list(val) ->
{key,
Enum.map(val, fn entry ->
schema_to_atom_map(entry)
end)}
key_val ->
key_val
end)
|> Map.new()
end
def schema_to_atom_map(val) do
val
end
@doc """
Takes a nested struct data structure and turns it into a map with
strings as keys
"""
def schema_to_string_map(map) when is_map(map) do
map
|> Map.delete(:__struct__)
|> Map.delete(:__meta__)
|> Enum.map(fn
{key, %{__struct__: _} = value} ->
{Atom.to_string(key), schema_to_string_map(value)}
{key, val} when is_list(val) ->
{Atom.to_string(key),
Enum.map(val, fn entry ->
schema_to_string_map(entry)
end)}
{key, val} ->
{Atom.to_string(key), val}
end)
|> Map.new()
end
def schema_to_string_map(val) do
val
end
@doc """
Go through the map of values and remove all fields which are nil
"""
def drop_nil_fields(map) when is_map(map) do
map
|> Enum.reduce(
%{},
fn {key, val}, acc ->
val = drop_nil_fields(val)
if empty?(val) do
acc
else
Map.put(acc, key, val)
end
end
)
end
def drop_nil_fields(list) when is_list(list) do
list
|> Enum.reduce([], fn elem, acc ->
elem = drop_nil_fields(elem)
if empty?(elem) do
acc
else
[elem | acc]
end
end)
|> Enum.reverse()
end
def drop_nil_fields(val), do: val
defp empty?(val), do: val in [nil, %{}, "", []]
defmacro __using__(opts) do
key_type = Keyword.get(opts, :key_type, :atom)
drop_nil_fields = Keyword.get(opts, :drop_nil_fields, false)
error_module = Keyword.get(opts, :view_module, Pharams.ErrorView)
error_template = Keyword.get(opts, :view_template, "errors.json")
error_status = Keyword.get(opts, :error_status, :unprocessable_entity)
quote do
import Pharams, only: [pharams: 2, pharams: 3]
# TODO: This is a bit hacky, move these over to module attributes
def pharams_key_type, do: unquote(key_type)
def pharams_drop_nil_fields?, do: unquote(drop_nil_fields)
def pharams_error_view_module, do: unquote(error_module)
def pharams_error_view_template, do: unquote(error_template)
def pharams_error_status, do: unquote(error_status)
end
end
defp generate_plug(validation_module, controller_module) do
quote do
use Phoenix.Controller
import Plug.Conn
import Ecto.Changeset
def init(opts), do: opts
def call(conn, opts \\ []) do
validator = unquote(validation_module)
controller = unquote(controller_module)
# Get the individual route options, or fallback to controller options
key_type = Keyword.get(opts, :key_type, controller.pharams_key_type())
drop_nil_fields = Keyword.get(opts, :drop_nil_fields, controller.pharams_drop_nil_fields?())
error_view_module = Keyword.get(opts, :view_module, controller.pharams_error_view_module())
error_view_template = Keyword.get(opts, :view_template, controller.pharams_error_view_template())
error_status = Keyword.get(opts, :error_status, controller.pharams_error_status())
changeset =
validator
|> struct()
|> validator.changeset(conn.params)
if changeset.valid? do
new_params =
changeset
|> apply_changes()
|> convert_key_type(key_type)
|> prune_empty_fields(drop_nil_fields)
%{conn | params: new_params}
else
conn
|> put_status(error_status)
|> put_view(error_view_module)
|> render(error_view_template, changeset)
|> halt()
end
end
defp convert_key_type(data, :atom = _key_type), do: Pharams.schema_to_atom_map(data)
defp convert_key_type(data, :string = _key_type), do: Pharams.schema_to_string_map(data)
defp convert_key_type(_data, _invalid_key_type) do
raise "Pharams: Invalid key_type. Valid options are :string and :atom"
end
defp prune_empty_fields(data, false = _drop_nil_fields), do: data
defp prune_empty_fields(data, true = _drop_nil_fields), do: Pharams.drop_nil_fields(data)
defp prune_empty_fields(data, _invalid_drop_nil_fields) do
raise "Pharams: Invalid drop_nil_fields. Valid options are true and false"
end
end
end
defp generate_validation({:__block__, [], block_contents}, caller) do
root_field_declarations = Utils.generate_basic_field_schema_definitions(block_contents, caller)
root_fields = Utils.get_all_basic_fields(block_contents)
root_required_fields = Utils.get_required_basic_fields(block_contents)
root_validations = Utils.generate_basic_field_validations(block_contents, caller)
root_group_declarations = Utils.generate_group_field_schema_definitions(block_contents, caller)
root_sub_schema_casts = Utils.generate_group_field_schema_casts(block_contents, nil)
group_schema_changesets = Utils.generate_group_field_schema_changesets(block_contents, nil, caller)
module =
[
"use Ecto.Schema",
"import Ecto.Changeset",
"",
"@primary_key false",
"embedded_schema do",
root_field_declarations,
root_group_declarations,
"end",
"",
"def changeset(schema, params) do",
"schema",
"|> cast(params, #{inspect(root_fields)})",
"|> validate_required(#{inspect(root_required_fields)})",
root_validations,
root_sub_schema_casts,
"end",
"",
group_schema_changesets
]
|> List.flatten()
formatted_module =
module
|> Enum.join("\n")
|> Code.format_string!()
module =
(module ++
[
"",
"def dump do",
"\"\"\"",
"#{formatted_module}",
"\"\"\"",
"end"
])
|> Enum.join("\n")
Code.string_to_quoted!(module)
end
defp generate_validation(ast, caller) do
generate_validation({:__block__, [], [ast]}, caller)
end
@doc """
This macro provides the ability to define validation schemas for use in Phoenix controllers
## Example
```elixir
use Pharams, view_module: Pharams.ErrorView, view_template: "errors.json", error_status: :unprocessable_entity
pharams :index do
required :terms_conditions, :boolean
required :password, :string
required :password_confirmation, :string
optional :age, :integer
end
def index(conn, params) do
# You will only get into this function if the request
# parameters have passed the above validator. The params
# variable is now just a map with atoms as keys.
render(conn, "index.html")
end
```
"""
defmacro pharams(controller_action, do: block) do
camel_action =
controller_action
|> Atom.to_string()
|> Macro.camelize()
calling_module = __CALLER__.module
# Create validation module
validation_module_name = Module.concat([calling_module, PharamsValidator, camel_action])
validation_module_ast = generate_validation(block, __CALLER__)
Module.create(validation_module_name, validation_module_ast, Macro.Env.location(__ENV__))
# Create plug module
plug_module_name = Module.concat([calling_module, PharamsPlug, camel_action])
plug_module_ast = generate_plug(validation_module_name, calling_module)
Module.create(plug_module_name, plug_module_ast, Macro.Env.location(__ENV__))
# Insert the validation plug into the controller
quote do
plug(unquote(plug_module_name) when var!(action) == unquote(controller_action))
end
end
defmacro pharams(controller_action, opts, do: block) do
camel_action =
controller_action
|> Atom.to_string()
|> Macro.camelize()
calling_module = __CALLER__.module
# Create validation module
validation_module_name = Module.concat([calling_module, PharamsValidator, camel_action])
validation_module_ast = generate_validation(block, __CALLER__)
Module.create(validation_module_name, validation_module_ast, Macro.Env.location(__ENV__))
# Create plug module
plug_module_name = Module.concat([calling_module, PharamsPlug, camel_action])
plug_module_ast = generate_plug(validation_module_name, calling_module)
Module.create(plug_module_name, plug_module_ast, Macro.Env.location(__ENV__))
# Insert the validation plug into the controller
quote do
plug(
unquote(plug_module_name),
unquote(opts) when var!(action) == unquote(controller_action)
)
end
end
end