Skip to content

Instantly share code, notes, and snippets.

@davydog187
Created August 12, 2020 13:35
Show Gist options
  • Save davydog187/0bec728c8f61218e9390aa43266b2acf to your computer and use it in GitHub Desktop.
Save davydog187/0bec728c8f61218e9390aa43266b2acf to your computer and use it in GitHub Desktop.
Enum type
defmodule App.Ecto.Atom do
@moduledoc """
Generates an Ecto custom type for atoms.
e.g.
defmodule MyType do
use App.Ecto.Atom, values: [:one, :two, :three]
end
Then you can use it an an Ecto Custom type
defmodule MySchema do
schema :some_table do
field(:foo, MyType)
end
end
Ecto.Atom will make sure Strings are properly casted
"""
alias App.Ecto.Atom, as: Parent
defmacro __using__(opts) do
values = Keyword.fetch!(opts, :values)
quote location: :keep do
use Ecto.Type
@values unquote(values)
@before_compile Parent
@impl Ecto.Type
def type, do: :string
@impl Ecto.Type
def cast(data), do: Parent.cast_atom(__MODULE__, transform(data))
@impl Ecto.Type
def load(data), do: Parent.load_atom(__MODULE__, data)
@impl Ecto.Type
def dump(data), do: Parent.dump_atom(__MODULE__, data)
def transform(data), do: data
@spec values() :: [__MODULE__.t()]
def values, do: @values
defoverridable transform: 1
end
end
defmacro __before_compile__(env) do
values = Module.get_attribute(env.module, :values)
Enum.each(values, fn value ->
unless is_atom(value) do
raise "#{__MODULE__} expects :values to be atoms, got: #{inspect(value)}"
end
end)
if Enum.count(values) != Enum.count(MapSet.new(values)) do
description = """
Ecto.Atom cannot contain duplicate values!
#{inspect(env.module)}
#{inspect(values)}
"""
raise CompileError, description: description
end
casting_ast =
for value <- values do
quote do
def __cast_atom__(unquote(value)), do: {:ok, unquote(value)}
def __cast_atom__(unquote(to_string(value))), do: {:ok, unquote(value)}
end
end
quote location: :keep do
@type t :: unquote(Enum.reduce(values, &{:|, [], [&1, &2]}))
unquote(casting_ast)
def __cast_atom__(_), do: :error
end
end
def cast_atom(module, data) do
case module.__cast_atom__(data) do
{:ok, atom} -> {:ok, atom}
:error -> {:error, [invalid_atom: data, valid_options: module.values()]}
end
end
@spec load_atom(atom, any) :: any
def load_atom(module, data) do
module.__cast_atom__(data)
end
def dump_atom(module, data) do
case module.__cast_atom__(data) do
{:ok, atom} -> {:ok, to_string(atom)}
:error -> :error
end
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment