Is it possible to define a type as follows:
defmodule Role do
use Exnumerator, values: ["admin", "regular", "restricted"]
@type t :: "admin" | "regular" | "restricted"
@spec default() :: t
def default() do
"regular"
end
end
to make a better analyze for the code like:
@type valid_attributes :: %{optional(:email) => String.t,
optional(:password) => String.t,
optional(:role) => Role.t}
@spec changeset(User.t, valid_attributes) :: Ecto.Changeset.t
def changeset(%User{} = user, attrs = %{}) do
# ...
end
# ...
User.changeset(%User{}, %{role: "superadmin"}) |> Repo.insert()
I know that I can define this type as @type t :: String.t
, but then, Dialyzer won't complain about using a different value than possible (possible from the application point of view).
I didn't saw any hints about this use case in the documentation for the Typespecs, but maybe I'm missing something.
It is not possible to use binary values in the described way. However, similar behavior can be achieved using atoms and - in my case - a custom Ecto type:
defmodule Role do
@behaviour Ecto.Type
@type t :: :admin | :regular | :restricted
@valid_binary_values ["admin", "regular", "restricter"]
@spec default() :: t
def default(), do: :regular
@spec valid_values() :: list(t)
def valid_values(), do: Enum.map(@valid_values, &String.to_existing_atom/1)
@spec type() :: atom()
def type(), do: :string
@spec cast(term()) :: {:ok, atom()} | :error
def cast(value) when is_atom(value), do: {:ok, value}
def cast(value) when value in @valid_binary_values, do: {:ok, String.to_existing_atom(value)}
def cast(_value), do: :error
@spec load(String.t) :: {:ok, atom()}
def load(value), do: {:ok, String.to_existing_atom(value)}
@spec dump(term()) :: {:ok, String.t} | :error
def dump(value) when is_atom(value), do: {:ok, Atom.to_string(value)}
def dump(_), do: :error
end
It allows to use the following code:
defmodule User do
use Ecto.Schema
import Ecto.Changeset
@type t :: %User{}
@type valid_attributes :: %{optional(:email) => String.t,
optional(:password) => String.t,
optional(:role) => Role.t}
@derive {Poison.Encoder, only: [:email, :id, :role]}
schema "users" do
field :email, :string
field :password, :string, virtual: true
field :password_hash, :string
field :role, Role, default: Role.default()
timestamps()
end
@spec changeset(User.t, valid_attributes) :: Ecto.Changeset.t
def changeset(%User{} = user \\ %User{}, attrs = %{}) do
# ...
end
This way, Dialyzer will catch an invalid user's role:
User.changeset(%User{}, %{role: :superadmin}) |> Repo.insert()
Unfortunately, it forces using atoms in place of strings in the application. It can be problematic if we already have a big code base or if we need plenty of possible values (the limit of atoms in the system and the the fact that they are not garbage collected).
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With