在 Elixir 类型规范的类型定义中指定一个字符串值
Specifying a string value in the type definition for the Elixir typespecs
是否可以定义一个类型如下:
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 的文档中没有看到关于此用例的任何提示,但也许我遗漏了什么。
不可能以描述的方式使用二进制值。但是,使用原子可以实现类似的行为 - 在我的例子中 - 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
它允许使用以下代码:
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()
不幸的是,它强制在应用程序中使用原子代替字符串。如果我们已经有一个很大的代码库,或者如果我们需要大量可能的值 (the limit of atoms in the system and the the fact that they are not garbage collected)。
,这可能会有问题
是否可以定义一个类型如下:
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 的文档中没有看到关于此用例的任何提示,但也许我遗漏了什么。
不可能以描述的方式使用二进制值。但是,使用原子可以实现类似的行为 - 在我的例子中 - 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
它允许使用以下代码:
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()
不幸的是,它强制在应用程序中使用原子代替字符串。如果我们已经有一个很大的代码库,或者如果我们需要大量可能的值 (the limit of atoms in the system and the the fact that they are not garbage collected)。
,这可能会有问题