Указание строкового значения в определении типа для спецификаций типов Elixir

Можно ли определить тип следующим образом:

defmodule Role do
  use Exnumerator, values: ["admin", "regular", "restricted"]

  @type t :: "admin" | "regular" | "restricted"

  @spec default() :: t
  def default() do
    "regular"
  end
end

чтобы лучше проанализировать код, например:

@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()

Я знаю, что могу определить этот тип как @type t :: String.tно тогда Dialyzer не будет жаловаться на использование значения, отличного от возможного (возможно с точки зрения приложения).

Я не видел намеков на этот вариант использования в документации к Typespecs, но, возможно, я что-то упустил.

1 ответ

Решение

Невозможно использовать двоичные значения описанным способом. Однако, подобное поведение может быть достигнуто с использованием атомов и - в моем случае - пользовательского типа Ecto:

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

Это позволяет использовать следующий код:

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

Таким образом, Dialyzer поймает роль недопустимого пользователя:

User.changeset(%User{}, %{role: :superadmin}) |> Repo.insert()

К сожалению, это заставляет использовать атомы вместо строк в приложении. Это может быть проблематично, если у нас уже есть большая кодовая база или нам нужно много возможных значений ( ограничение количества атомов в системе и тот факт, что они не являются сборщиком мусора).

Другие вопросы по тегам