defmodule EctoPostgresEnum do @moduledoc false defmacro __using__(opts) do quote(bind_quoted: [opts: opts], unquote: false) do default_type = __MODULE__ |> Module.split() |> List.last() |> Macro.underscore() |> String.to_atom() schema = opts[:schema] type = opts[:type] || default_type is_atom(type) || raise "Type needs to be an atom" values = opts[:values] || raise "Option values (list) is required" values == Enum.uniq(values) || raise "Duplicates are not allowed in enum values" Enum.count(values) > 1 || raise "Valid enums requires at least 2 different values" Enum.all?(values, &is_atom/1) || raise "All values must be atoms" schema && (is_atom(schema) || raise "Option schema must be atom") alias Ecto.Migration @behaviour Ecto.Type @__input_values__ Enum.map(values, &Atom.to_string/1) @__output_values__ values @__schema__ schema @__type__ if is_nil(@__schema__), do: type, else: :"#{@__schema__}.#{type}" @__valid_values__ @__input_values__ ++ @__output_values__ types = Enum.map_join(values, ", ", &"'#{&1}'") @__create_sql__ "CREATE TYPE #{type} AS ENUM (#{types})" @__drop_sql__ "DROP TYPE #{@__type__}" list = Enum.zip(@__output_values__, @__input_values__) [head | tail] = Enum.reverse(@__output_values__) ast = Enum.reduce(tail, head, &{:|, [], [&1, &2]}) @type t :: unquote(ast) @doc "Casts the given input to the custom type" @spec cast(value) :: value when value: t() @spec cast(String.t()) :: t() | :error @spec cast(term) :: :error for {atom, string} <- list do def cast(unquote(atom)), do: {:ok, unquote(atom)} def cast(unquote(string)), do: {:ok, unquote(atom)} end def cast(_term), do: :error @doc "Creates database enum" @spec create_db_enum :: :ok def create_db_enum, do: Migration.execute(@__create_sql__) @doc "Drops database enum" @spec drop_db_enum :: :ok def drop_db_enum, do: Migration.execute(@__drop_sql__) @doc "Dumps the given term into an Ecto native type" @spec dump(t()) :: {:ok, String.t()} @spec dump(value) :: value | :error when value: String.t() @spec dump(term) :: :error for {atom, string} <- list do def dump(unquote(atom)), do: {:ok, unquote(string)} def dump(unquote(string)), do: {:ok, unquote(string)} end def dump(_term), do: :error @doc "Loads the given term into a custom type" @spec load(String.t()) :: {:ok, t()} | :error @spec load(term) :: :error for {atom, string} <- list do def load(unquote(string)), do: {:ok, unquote(atom)} end def load(_value), do: :error @doc """ Returns database enum type """ @spec type :: atom def type, do: @__type__ end end end