@romik if I understood your problem correctly, one thing you can use is to define Ecto.Types, instead of strings, and map the type to atoms. Underneath they’ll still be strings in the db but they’ll allow you to automatically cast between them, when retrieving them from the database and when querying. The Ecto.Type will have some similarities with what @kip mentioned but it will be a proper Ecto.Type so more consolidated.
It will then raise if you use a non valid atom/string in the query.
You would probably define one ecto type for each table statuses unless they are the same. So you would have a type User.Statuses.Type
, Article.Statuses.Type
, etc.
An example would be:
defmodule User.Activity.Type do
@behaviour Ecto.Type
@type t() :: :open_duel | :tournament | :pod_queue | :starting_pod | :pod | :enqueued | :duel | :starting_duel
def type, do: :string
@valid_types [:open_duel, :tournament, :pod_queue, :starting_pod, :pod, :enqueued, :duel, :starting_duel]
@valid_string Enum.reduce(@valid_types, [], fn(t, acc) -> [Atom.to_string(t) | acc] end)
@valid_map Enum.reduce(@valid_types, %{}, fn(t, acc) -> Map.put(acc, Atom.to_string(t), t) end)
def load(data) when is_binary(data) and data in @valid_string, do: {:ok, @valid_map[data]}
def load(data) when is_atom(data) and data in @valid_types, do: {:ok, data}
def load(_), do: :error
def cast(data) when is_atom(data) and data in @valid_types, do: {:ok, data}
def cast(data) when is_binary(data) and data in @valid_string, do: {:ok, String.to_atom(data)}
def cast(_), do: :error
def dump(data) when is_atom(data) and data in @valid_types, do: {:ok, Atom.to_string(data)}
def dump(data) when is_binary(data) and data in @valid_string, do: {:ok, data}
def embed_as(_), do: :dump
def equal?(term_1, term_2) when is_binary(term_1) and is_atom(term_2) do
Atom.to_string(term_2) === term_1
end
def equal?(term_1, term_2) when is_binary(term_2) and is_atom(term_1) do
Atom.to_string(term_1) === term_2
end
def equal?(term_1, term_2), do: term_1 === term_2
end
But changed to your needs.
This allows you to cast/validate types before hand, like by calling User.Activity.Type.cast("some_value")
either as an atom or string. It will return :error
if it’s not a valid type.
And it allows you to use either the atom or string form in queries, so for instance
User.Activity |> where([a], a.type == ^:open_game) |> Db.Repo.all
(you need to pin atoms when writing them literally in the query, but if it’s in a variable you’ll pin the variable anyway).
If the value isn’t acceptable in the query it will raise, so you’ll get an error such as:
** (Ecto.Query.CastError) iex:10: value
"PLAYGROUND"in
where cannot be cast to type User.Activity.Type in query:
You can get fancier with using some more meta programming and generating function names that give you the correct type, like you have with Article.status_active()
but I’m not sure there’s anything to gain there, unless you plan/or want to be able to change the status that corresponds to status_active
.
Like using the previous type and adding something such as:
Enum.each(@valid_types, fn(value) ->
name = String.to_atom("status_#{value}")
def unquote(name)(), do: unquote(value)
end)
In that module that would translated to functions name status_open_duel
, etc. So you could use User.Activity.Type.status_open_duel
to get back :open_duel
. To make it more useful you would probably want to change the @valid_types to be instead
@valid_types [{:open_duel, :open_duel}, {:tournament, :tournament}, {:pod_queue, :pod_queue}, {:starting_pod, :starting_pod}, {:pod, :pod}, {:enqueued, :enqueued}, {:duel, :duel}, {:starting_duel, :starting_duel}]
@valid_string Enum.reduce(@valid_types, [], fn({_, t}, acc) -> [Atom.to_string(t) | acc] end)
@valid_map Enum.reduce(@valid_types, %{}, fn({_, t}, acc) -> Map.put(acc, Atom.to_string(t), t) end)
Enum.each(@valid_types, fn({description, value}) ->
name = String.to_atom("status_#{description}")
def unquote(name)(), do: unquote(value)
end)
This would then allow you to change :open_duel
, to be instead :waiting_duel
, while keeping User.Activity.Type.status_open_duel()
throughout the codebase. But I kinda prefer to forget that and just use directly the status.