Skip to content

Instantly share code, notes, and snippets.

@esdras
Last active June 17, 2019 03:47
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save esdras/9059de7c285f401f0385b5746742092c to your computer and use it in GitHub Desktop.
Save esdras/9059de7c285f401f0385b5746742092c to your computer and use it in GitHub Desktop.
defmodule MyApp.Users.Input do
@moduledoc false
@type params :: map() | Keyword.t()
alias MyApp.Utils
alias MyApp.Users.{User}
defmodule Create do
@moduledoc """
Input for `MyApp.Users.create/1`
"""
use MyApp.Schema
alias MyApp.Users.Input
embedded_schema do
field(:name, :string)
field(:email, :string)
field(:password, :string)
end
@doc """
Validate params and create a new input object
## Examples
iex> empty_params = []
...> {:error, cs} = MyApp.Users.Input.Create.validate(empty_params)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[name: "can't be blank", email: "can't be blank", password: "can't be blank"]
iex> invalid_email_params = [name: "Foo Bar", email: "invalid123.com", password: "123abc"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(invalid_email_params)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[email: "has invalid format"]
iex> password_with_no_digit = [name: "Foo Bar", email: "foo@bar.com", password: "abcdefg"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(password_with_no_digit)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[password: "should have at least one digit"]
iex> password_with_no_letters = [name: "Foo Bar", email: "foo@bar.com", password: "1234567"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(password_with_no_letters)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[password: "should have at least one non digit character"]
iex> valid_params = [name: "Foo Bar", email: "foo@bar.com", password: "123abc"]
...> MyApp.Users.Input.Create.validate(valid_params)
{:ok, %MyApp.Users.Input.Create{
name: "Foo Bar",
email: "foo@bar.com",
password: "123abc"
}}
"""
@spec validate(Input.params()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
def validate(params) do
params = normalize_params(params)
cast(%__MODULE__{}, params, [:name, :email, :password, :invitation_token])
|> validate_required([:name, :email, :password])
|> validate_password()
|> validate_format(:email, Utils.email_regex())
|> case do
%Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
cs -> {:error, cs}
end
end
@spec changeset(map() | Keyword.t()) :: Ecto.Changeset.t()
def changeset(params) do
params = normalize_params(params)
cast(%__MODULE__{}, params, [:name, :email, :password, :password_confirmation])
end
def validate_password(cs) do
cs
|> validate_format(:password, ~r/\d+/, message: "should have at least one digit")
|> validate_format(:password, ~r/\D+/, message: "should have at least one non digit character")
|> validate_length(:password, min: 6)
|> validate_confirmation(:password)
end
end
defmodule Update do
@moduledoc """
Input for `MyApp.Users.update/2`
"""
use MyApp.Schema
alias MyApp.Users.Input
embedded_schema do
field(:name, :string)
end
@doc """
Validate params and create a new input object for
`MyApp.Users.update/2`
## Examples
iex> empty_params = []
...> alias MyApp.Users.{Input, User}
...> {:error, cs} = Input.Update.validate(%User{}, empty_params)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[name: "can't be blank"]
iex> valid_params = [name: "Foo Bar"]
...> alias MyApp.Users.{Input, User}
...> Input.Update.validate(%User{}, valid_params)
{:ok, %MyApp.Users.Input.Update{
name: "Foo Bar"
}}
"""
@spec validate(%User{}, Input.params()) ::
{:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
def validate(record, params) do
existing_values =
record
|> Map.from_struct()
|> Map.take(__MODULE__.__schema__(:fields))
|> normalize_params
params =
existing_values
|> Map.merge(normalize_params(params))
cast(%__MODULE__{}, params, [:name])
|> validate_required([:name])
|> case do
%Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
cs -> {:error, cs}
end
end
end
defmodule ResetPassword do
@moduledoc """
Input for `MyApp.Users.reset_password/3`
"""
use MyApp.Schema
alias MyApp.Users.Input
alias MyApp.Users.Input.Commons
embedded_schema do
field(:password, :string)
field(:password_confirmation, :string)
end
@doc """
Validate params and create a new input object
## Examples
iex> empty_params = []
...> {:error, cs} = MyApp.Users.Input.Create.validate(empty_params)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[name: "can't be blank", email: "can't be blank", password: "can't be blank"]
iex> invalid_email_params = [name: "Foo Bar", email: "invalid123.com", password: "123abc"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(invalid_email_params)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[email: "has invalid format"]
iex> password_with_no_digit = [name: "Foo Bar", email: "foo@bar.com", password: "abcdefg"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(password_with_no_digit)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[password: "should have at least one digit"]
iex> password_with_no_letters = [name: "Foo Bar", email: "foo@bar.com", password: "1234567"]
...> {:error, cs} = MyApp.Users.Input.Create.validate(password_with_no_letters)
...> Enum.map(cs.errors, fn {k, {msg, _}} -> {k, msg} end)
[password: "should have at least one non digit character"]
iex> valid_params = [name: "Foo Bar", email: "foo@bar.com", password: "123abc"]
...> MyApp.Users.Input.Create.validate(valid_params)
{:ok, %MyApp.Users.Input.Create{
name: "Foo Bar",
email: "foo@bar.com",
password: "123abc"
}}
"""
@spec validate(Input.params()) :: {:ok, %__MODULE__{}} | {:error, Ecto.Changeset.t()}
def validate(params) do
params = normalize_params(params)
cast(%__MODULE__{}, params, [:password, :password_confirmation])
|> validate_required([:password, :password_confirmation])
|> Commons.validate_password()
|> case do
%Changeset{valid?: true} = cs -> {:ok, apply_changes(cs)}
cs -> {:error, cs}
end
end
@spec changeset(Input.params()) :: Ecto.Changeset.t()
def changeset(params) do
params = normalize_params(params)
cast(%__MODULE__{}, params, [:password, :password_confirmation])
end
end
end
# User Schema, this maps to a table in the database.
defmodule MyApp.Users.User do
use MyApp.Schema
schema "users" do
field(:name, :string)
field(:email, :string)
end
def changeset(params) do
changeset(%__MODULE__{}, params)
end
def changeset(struct, params) do
struct
|> change(params)
|> validate_required([:name, :email])
end
end
defmodule MyApp.Users do
@doc """
Creates a user
Check `MyApp.Users.Input.Create` for the expected params
This function will:
1. Create a user
4. Return `{:ok, %User{}}` if the provided arguments are valid and everything went well
5. Return `{:error, Ecto.Changeset{}}` if the provided arguments are invalid
6. Raise an exception if an error occurs after the validation step
## Examples
invalid = %{name: "Foo", email: "foo@bar.com", password: nil}
Users.create(session, vendor, invalid)
{:error, %Ecto.Changeset{}}
params = %{name: "Foo", email: "foo@bar.com", password: "password42", password_confirmation: "password42"}
Users.create(params)
{:ok, %User{}}
"""
@spec create(
map() | Keyword.t
) :: {:ok, %User{}} | {:error, Ecto.Changeset.t()} | no_return()
def create(params) do
with {:validate_params, {:ok, input}} <- {:validate_params, Input.Create.validate(params)},
{:validate_schema, %{valid?: true} = cs} <-
{:validate_schema, User.changeset(Map.from_struct(input))},
{:insert, {:ok, user}} <-
{:insert, Repo.insert(cs) do
{:ok, user}
else
{:validate_params, {:error, cs}} ->
{:error, cs}
error ->
error
end
|> case do
{:ok, result} -> {:ok, result}
{:error, cs} -> {:error, cs}
error -> raise(inspect(error))
end
end
@doc """
Updates a user
Check `MyApp.Users.Input.Update` for the expected params
This function should not be used to update the user's email.
This function will:
1. Update a user
2. Return `{:ok, %User{}}` if the provided arguments are valid and everything went well
3. Return `{:error, Ecto.Changeset{}}` if the provided arguments are invalid
4. Raise an exception if an error occurs after the validation step
## Examples
Users.update(name: nil)
{:error, %Ecto.Changeset{}}
Users.update(name: "Foo Bar")
{:ok, %User{}}
"""
@type update_option :: {:tx_opts, Datomish.options()}
@spec update(
entity() | id_param(),
map() | Keyword.t
) ::
{:ok, %User{}}
| {:error, :not_found | Ecto.Changeset.t()}
| no_return()
def update(user_or_id, params) do
with {:get_record, %User{} = member} <- {:get_record, get(member_or_id)},
{:validate_params, {:ok, input}} <-
{:validate_params, Input.Update.validate(member, params)},
{:prepare_schema_input, input} <- {:prepare_schema_input, Map.from_struct(input)},
{:validate_schema, %{valid?: true} = cs} <-
{:validate_schema, User.changeset(member, input)},
{:update, {:ok, record}} <- {:update, Repo.update(cs)} do
{:ok, record}
else
{:validate_params, {:error, cs}} ->
{:error, cs}
{:get_record, nil} ->
{:error, :not_found}
error ->
error
end
|> case do
{:ok, result} -> {:ok, result}
{:error, error_or_cs} -> {:error, error_or_cs}
error -> raise(inspect(error))
end
end
end
@esdras
Copy link
Author

esdras commented Jun 16, 2019

Este Gist está incompleto, é só um exemplo de como eu valido cada action...

@giovanicascaes
Copy link

De onde vem a funcão normalize_params?

@esdras
Copy link
Author

esdras commented Jun 17, 2019

Esqueci de excluir esta função, ela é um helper que torna todas as keys de um Map em string. Pra casos onde o Map venha com algumas keys de tipos diferentes (Atom e String) por exemplo. Não é importante pro exemplo, mas aqui está ela:

def normalize_params(params, opts \\ []) do
    as = Keyword.get(opts, :as, :map)

    params =
      params
      |> struct_to_enum
      |> Enum.into(%{})
      |> stringify_keys

    case as do
      :map -> params
      :list -> params |> Enum.into([])
    end
  end

  def atomize_keys(map) when is_map(map) do
    Enum.reduce(map, %{}, fn {k, v}, acc -> Map.put(acc, to_atom(k), v) end)
  end

  def stringify_keys(map) when is_map(map) do
    Enum.reduce(map, %{}, fn {k, v}, acc -> Map.put(acc, to_string(k), v) end)
  end

  def struct_to_enum(%{__struct__: _} = struct) do
    if Enumerable.impl_for(struct) do
    else
      Map.from_struct(struct)
    end
  end

  def struct_to_enum(other), do: other

  def to_atom(v) when is_binary(v), do: String.to_atom(v)
  def to_atom(v) when is_atom(v), do: v


Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment