EnumType

Generates Enumerated type modules that can be used as values and matched in code. Creates proper types so Dialyzer will be able to check bad calls.

Why?

Something we have often wanted in Elixir was an enumerated type. The benefits we wanted are:

  • compile time type checking
  • easy pattern matching
  • not Ecto specific, but has Ecto support

Installation

The package can be installed by adding enum_type to your list of dependencies in mix.exs:

def deps do
  [
    {:enum_type, "~> 1.1.0"}
  ]
end

Changelog

  • v1.1.2 Replace @behaviour Ecto.Type with use Ecto.Type
  • v1.1.1 Fix warning for Elixir 1.10
  • v1.1.0 Adds types to the enum. Dialyzer might start complaining.

Creating and using an Enum

An enum type is created as its own module with each value of an an enum being a child module. The enum reference can then be used as you would use any module name in Elixir. Since actual modules are created, this also means the module names are valid references that can be called. Enum types can be defined anywhere defmodule can be used.

defmodule MyApp do
  use EnumType

  defenum Color do
    value Red, "red"
    value Blue, "blue"
    value Green, "green"

    default Blue
  end

  @spec do_something(color :: Color.t) :: String.t
  def do_something(Color.Red), do: "got red"
  def do_something(Color.Blue), do: "got blue"
  def do_something(Color.Green), do: "got green"
end

MyApp.Color.Blue == MyApp.Color.default
"green" == MyApp.Color.Green.value
"got red" == MyApp.do_something(MyApp.Color.Red)

Enum Type Functions

  • values - List of all enum values in the order they are defined. ["red", "blue"]
  • enums - List of all enum value modules that are defined in the order defined. [MyApp.Color.Red, MyApp.Color.Blue]
  • options - List of tuples with the module name and the value. [{MyApp.Color.Red, "red}, {MyApp.Color.Blue, "blue"}]
  • from - Converts a value to an option module name. MyApp.Color.Red == MyApp.Color.from("red")
  • value - Converts a option module into the value. "red" == MyApp.Color.value(MyApp.Color.Red)

Enum Option Functions

  • value - The value of the enum option. MyApp.Color.Red.value

Custom Functions

Since both the enum type and options are both modules, any custom code that can be added to a module can also be added to these code blocks.

import EnumType

defenum Color do
  value Red, "red" do
    def statement, do: "I'm red"
  end

  value Blue, "blue" do
    def statement, do: "I'm blue"
  end

  value Green, "green" do
    def statement, do: "I'm green"
  end

  default Blue

  @spec do_something(color :: Color.t)
  def do_something(Color.Red), do: "got red"
  def do_something(Color.Blue), do: "got blue"
  def do_something(Color.Green), do: "got green"

  def statement(color), do: "I'm #{color.value}"
end

"got blue" == Color.do_something(Color.Blue)
"I'm green" == Color.Green.statement
"I'm red" == Color.statement(Color.Red)

Ecto Type Support

If Ecto is included in your project, additional helpers functions will be compiled in that implement the Ecto.Type behaviour callbacks. When using Ecto, a type must be specified for the enum values that is supported by Ecto. All values provided by the Enum Type must be the same Ecto basic type defined. By default, the type is :string.

defmodule Subscriber do
  use Ecto.Schema
  use EnumType

  import Ecto.Changeset

  # For database field defined as a string.
  defenum Level do
    value Basic, "basic"
    value Premium, "premium"

    default Basic
  end

  # For database field defined as an integer.
  defenum AgeGroup, :integer do
    value Minor, 0
    value Adult, 1
    value NotSpecified, 2

    default NotSpecified
  end

  schema "subscribers" do
    field :name,            :string
    field :level,           Level,        default: Level.default
    field :age_group,       AgeGroup,     default: AgeGroup.default
  end

  changeset(schema, params) do
    schema
    |> cast(params, [:name, :level, :age_group])
    |> Level.validate(:level, message: "Invalid subscriber type")
    |> AgeGroup.validate(:age_group)
  end
end

When working with an enum type in an Ecto schema, always use the module name of the value you wish to use. The name can be included in any query or changeset params.

Using with Absinthe

Absinthe provides a means to define enums and map to other values. When using Absinthe with an Ecto schema that also uses EnumType, you will need to map to the enum option module name and not the underlying value that will be stored in the database.

enum :subscriber_level do
  value :basic, as: Subscriber.Level.Basic
  value :premium, as: Subscriber.Level.Premium
end

object :subscriber do
  field :level, :subscriber_level
end

Absinthe will produce an upper case value based upon its own enum through the graphql interface. “BASIC” or “PREMIUM”.

Outbound or inbound will be mapped correctly to and from the EnumType module name value.

Dialyzer

Enum types and values are modules under the hood. Since module names are just atoms in Elixir, we don’t get compile-time type checks. However, Dialyzer will be able to spot type errors. For example, code like the following:

defenum MyEnum do
    value One, "one"
    value Two, "two"
    value Three, "three"
end

@spec foo(thing :: MyEnum.t()) :: atom()
def foo(MyEnum.One), do: :one_here
def foo(MyEnum.Two), do: :two_here
def foo(MyEnum.Three), do: :three_here

def bad_func() do
  foo(MyEnum.NotASubtype)
end

Dialyzer will complain that bad_func() doesn’t return because foo(MyEnum.NotASubtype) breaks the contract.