enum.ex (6933B)
1 defmodule Ecto.Enum do 2 @moduledoc """ 3 A custom type that maps atoms to strings or integers. 4 5 `Ecto.Enum` must be used whenever you want to keep atom values in a field. 6 Since atoms cannot be persisted to the database, `Ecto.Enum` converts them 7 to a string or an integer when writing to the database and converts them back 8 to atoms when loading data. It can be used in your schemas as follows: 9 10 # Stored as strings 11 field :status, Ecto.Enum, values: [:foo, :bar, :baz] 12 13 or 14 15 # Stored as integers 16 field :status, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5] 17 18 Therefore, the type to be used in your migrations for enum fields depend 19 on the choice above. For the cases above, one would do, respectively: 20 21 add :status, :string 22 23 or 24 25 add :status, :integer 26 27 Some databases also support enum types, which you could use in combination 28 with the above. 29 30 Composite types, such as `:array`, are also supported which allow selecting 31 multiple values per record: 32 33 field :roles, {:array, Ecto.Enum}, values: [:author, :editor, :admin] 34 35 Overall, `:values` must be a list of atoms or a keyword list. Values will be 36 cast to atoms safely and only if the atom exists in the list (otherwise an 37 error will be raised). Attempting to load any string/integer not represented 38 by an atom in the list will be invalid. 39 40 The helper function `mappings/2` returns the mappings for a given schema and 41 field, which can be used in places like form drop-downs. For example, given 42 the following schema: 43 44 defmodule EnumSchema do 45 use Ecto.Schema 46 47 schema "my_schema" do 48 field :my_enum, Ecto.Enum, values: [:foo, :bar, :baz] 49 end 50 end 51 52 You can call `mappings/2` like this: 53 54 Ecto.Enum.mappings(EnumSchema, :my_enum) 55 #=> [foo: "foo", bar: "bar", baz: "baz"] 56 57 If you want the values only, you can use `Ecto.Enum.values/2`, and if you want 58 the dump values only, you can use `Ecto.Enum.dump_values/2`. 59 60 ## Embeds 61 62 `Ecto.Enum` allows to customize how fields are dumped within embeds through the 63 `:embed_as` option. Two alternatives are supported: `:values`, which will save 64 the enum keys (and not their respective mapping), and `:dumped`, which will save 65 the dumped value. The default is `:values`. For example, assuming the following 66 schema: 67 68 defmodule EnumSchema do 69 use Ecto.Schema 70 71 schema "my_schema" do 72 embeds_one :embed, Embed do 73 field :embed_as_values, Ecto.Enum, values: [foo: 1, bar: 2], embed_as: :values 74 field :embed_as_dump, Ecto.Enum, values: [foo: 1, bar: 2], embed_as: :dump 75 end 76 end 77 end 78 79 The `:embed_as_values` field value will save `:foo | :bar`, while the 80 `:embed_as_dump` field value will save as `1 | 2`. 81 """ 82 83 use Ecto.ParameterizedType 84 85 @impl true 86 def type(params), do: params.type 87 88 @impl true 89 def init(opts) do 90 values = opts[:values] 91 92 {type, mappings} = 93 cond do 94 is_list(values) and Enum.all?(values, &is_atom/1) -> 95 validate_unique!(values) 96 {:string, Enum.map(values, fn atom -> {atom, to_string(atom)} end)} 97 98 type = Keyword.keyword?(values) and infer_type(Keyword.values(values)) -> 99 validate_unique!(Keyword.keys(values)) 100 validate_unique!(Keyword.values(values)) 101 {type, values} 102 103 true -> 104 raise ArgumentError, """ 105 Ecto.Enum types must have a values option specified as a list of atoms or a 106 keyword list with a mapping from atoms to either integer or string values. 107 108 For example: 109 110 field :my_field, Ecto.Enum, values: [:foo, :bar] 111 112 or 113 114 field :my_field, Ecto.Enum, values: [foo: 1, bar: 2, baz: 5] 115 """ 116 end 117 118 on_load = Map.new(mappings, fn {key, val} -> {val, key} end) 119 on_dump = Map.new(mappings) 120 on_cast = Map.new(mappings, fn {key, _} -> {Atom.to_string(key), key} end) 121 122 embed_as = 123 case Keyword.get(opts, :embed_as, :values) do 124 :values -> 125 :self 126 127 :dumped -> 128 :dump 129 130 other -> 131 raise ArgumentError, """ 132 the `:embed_as` option for `Ecto.Enum` accepts either `:values` or `:dumped`, 133 received: `#{inspect(other)}` 134 """ 135 end 136 137 %{ 138 on_load: on_load, 139 on_dump: on_dump, 140 on_cast: on_cast, 141 mappings: mappings, 142 embed_as: embed_as, 143 type: type 144 } 145 end 146 147 defp validate_unique!(values) do 148 if length(Enum.uniq(values)) != length(values) do 149 raise ArgumentError, """ 150 Ecto.Enum type values must be unique. 151 152 For example: 153 154 field :my_field, Ecto.Enum, values: [:foo, :bar, :foo] 155 156 is invalid, while 157 158 field :my_field, Ecto.Enum, values: [:foo, :bar, :baz] 159 160 is valid 161 """ 162 end 163 end 164 165 defp infer_type(values) do 166 cond do 167 Enum.all?(values, &is_integer/1) -> :integer 168 Enum.all?(values, &is_binary/1) -> :string 169 true -> nil 170 end 171 end 172 173 @impl true 174 def cast(nil, _params), do: {:ok, nil} 175 176 def cast(data, params) do 177 case params do 178 %{on_load: %{^data => as_atom}} -> {:ok, as_atom} 179 %{on_dump: %{^data => _}} -> {:ok, data} 180 %{on_cast: %{^data => as_atom}} -> {:ok, as_atom} 181 _ -> :error 182 end 183 end 184 185 @impl true 186 def load(nil, _, _), do: {:ok, nil} 187 188 def load(data, _loader, %{on_load: on_load}) do 189 case on_load do 190 %{^data => as_atom} -> {:ok, as_atom} 191 _ -> :error 192 end 193 end 194 195 @impl true 196 def dump(nil, _, _), do: {:ok, nil} 197 198 def dump(data, _dumper, %{on_dump: on_dump}) do 199 case on_dump do 200 %{^data => as_string} -> {:ok, as_string} 201 _ -> :error 202 end 203 end 204 205 @impl true 206 def equal?(a, b, _params), do: a == b 207 208 @impl true 209 def embed_as(_, %{embed_as: embed_as}), do: embed_as 210 211 @doc "Returns the possible values for a given schema and field" 212 @spec values(module, atom) :: [atom()] 213 def values(schema, field) do 214 schema 215 |> mappings(field) 216 |> Keyword.keys() 217 end 218 219 @doc "Returns the possible dump values for a given schema and field" 220 @spec dump_values(module, atom) :: [String.t()] | [integer()] 221 def dump_values(schema, field) do 222 schema 223 |> mappings(field) 224 |> Keyword.values() 225 end 226 227 @doc "Returns the mappings for a given schema and field" 228 @spec mappings(module, atom) :: Keyword.t() 229 def mappings(schema, field) do 230 try do 231 schema.__changeset__() 232 rescue 233 _ in UndefinedFunctionError -> 234 raise ArgumentError, "#{inspect(schema)} is not an Ecto schema" 235 else 236 %{^field => {:parameterized, Ecto.Enum, %{mappings: mappings}}} -> mappings 237 %{^field => {_, {:parameterized, Ecto.Enum, %{mappings: mappings}}}} -> mappings 238 %{^field => _} -> raise ArgumentError, "#{field} is not an Ecto.Enum field" 239 %{} -> raise ArgumentError, "#{field} does not exist" 240 end 241 end 242 end