# Copyright (c) 2021 Anand Panchapakesan # # This software is released under the MIT License. # https://opensource.org/licenses/MIT defmodule EnumHelper do @moduledoc """ This module is `use`d to create an Enum like so: ```elixir use Helper.Enum, :name, :ready, :set, :go ``` It is heavily inspired (copied) from Ecto.Enum, but pruned to meet our needs, which did not include Ecto integration The use statement takes a keyword list as arguments. The options include 1. name : either an atom as a singular name or a tuple with singular and plural names. If only a singular name is given, then it is used for both singular and plural 2. values : a list of valid values for the enum. This can either be a list of atoms or a keyword list of atom and integer Below are examples of a valid argument: ```elixir use Helper.Enum, name: :status, values: [:registered, :active, :inactive, :archived] use Helper.Enum, name: [:state, :states], values: [registered: 0, active: 1, inactive: 2, archived: 3] ``` This helper provides functions for inspecting the enum at runtime. `valid_/0` - returns a list of valid values (either atoms or atoms and integers) `valid_?/1` - returns boolean indicating whether the supplied value is valid Enum also generates a typespec for use with dialyzer, available as `()` type """ import Keyword, only: [keys: 1, values: 1, keyword?: 1] @doc """ Define the enum. """ @spec __using__(keyword()) :: Macro.output() defmacro __using__(name: name, values: values) do {s_name, p_name} = if is_tuple(name), do: name, else: {name, name} # coveralls-ignore-start unless is_atom(s_name) and is_atom(p_name), do: raise(CompileError, description: "name should be an atom") unless (is_list(values) and Enum.all?(values, &is_atom/1)) or (keyword?(values) and (values |> keys() |> Enum.all?(&is_atom/1) and values |> values() |> Enum.all?(&is_integer/1))), do: raise(CompileError, description: "#{values}: atoms or atom/integer keyword list expected" ) # coveralls-ignore-stop value_list = if(keyword?(values), do: keys(values) ++ values(values), else: values) typespec = value_list |> Enum.reverse() |> Enum.reduce(&{:|, [], [&1, &2]}) |> Macro.escape() fun_valid_values = :"valid_#{p_name}" fun_valid? = :"valid_#{s_name}?" quote bind_quoted: binding() do @type unquote(s_name)() :: unquote(typespec) @spec unquote(fun_valid_values)() :: list() def unquote(fun_valid_values)(), do: unquote(value_list) @spec unquote(fun_valid?)(unquote(s_name)()) :: boolean() def unquote(fun_valid?)(value) when is_atom(value) or is_integer(value), do: Enum.member?(unquote(value_list), value) def unquote(fun_valid?)(_), do: false end end end