lib/bitmask.ex

defmodule Bitmask do
  @moduledoc """
    A use macro for automatically generating a Bitmask from a collection of atoms, with support for saving them to a database via the provided Ecto.Type.

    You can create a bitmask like so:
      defmodule MyBitmask do
        use Bitmask, [
          :flag_1,
          :flag_2,
          :flag_3,
          :flag_4,
        ]
      end

    Or, you can define specific values for each field:
      defmodule MyBitmask do
        import Bitwise
        use Bitmask, [
          flag_1: 1 <<< 0,
          flag_2: 1 <<< 1,
          flag_3: 1 <<< 2,
          flag_4: 1 <<< 3,
        ]
      end

    The generated bitmask can also optinally be used with ecto. It stores the flags in the DB as a bigint:
      defmodule SomeEctoSchema do
        use Ecto.Schema
        schema "bitmasks" do
          field :my_bitmask, MyBitmask
        end
      end
  """

  defp validate_bit_vals({{atom, value}, _i}) do
    {atom, value}
  end

  defp validate_bit_vals({atom, i}) do
    {atom, Bitwise.<<<(1, i)}
  end

  defmacro __using__(bit_vals) do
    bit_vals =
      Enum.with_index(bit_vals)
      |> Enum.map(&validate_bit_vals/1)

    atom_to_bitmask =
      Enum.reduce(bit_vals, [], fn({atom, value}, acc) ->
        acc ++ [
        quote do
          def atom_to_bitmask(unquote(atom)) do
            unquote(value)
          end
        end]
      end)


    all_bitmask =
    Enum.reduce(bit_vals, %{bitmask: 0, flags: []}, fn({atom, value}, %{bitmask: bitmask, flags: flags}) ->
      {val, _} = Code.eval_quoted(value, [], __CALLER__)
      %{bitmask: bitmask + val, flags: flags ++ [atom]}
    end)

    ecto_code =
      if Code.ensure_loaded?(Ecto.Type) do
        quote do
          use Ecto.Type
          def type, do: :bigint

          def cast(bitmask) when is_integer(bitmask) do
            {:ok, int_to_bitmask(bitmask)}
          end

          def cast(%__MODULE__{} = card_type) do
            {:ok, card_type}
          end

          def cast(_), do: :error

          def load(bitmask) when is_integer(bitmask) do
            {:ok, int_to_bitmask(bitmask)}
          end

          def load(_) do
            :error
          end

          def dump(%__MODULE__{bitmask: bitmask, flags: _atom_flags}) do
            {:ok, bitmask}
          end

          def dump(bitmask) when is_integer(bitmask) do
            {:ok, bitmask}
          end

          def dump(_), do: :error
        end
      else
        nil
      end

    ast =
    quote do
      @behaviour Bitmask

      defstruct bitmask: 0, flags: []

      @type t() :: %__MODULE__{
        bitmask: integer(),
        flags: list(:atom)
      }

      Module.put_attribute(__MODULE__, :bit_values, unquote(bit_vals))

      def none() do
        %__MODULE__{bitmask: 0, flags: []}
      end

      def all() do
        %__MODULE__{bitmask: unquote(all_bitmask.bitmask), flags: unquote(all_bitmask.flags)}
      end

      def get_all_values() do
        @bit_values
      end

      unquote(atom_to_bitmask)

      def atom_to_bitmask(_) do
        0
      end

      def int_to_bitmask(bitmask) when is_integer(bitmask) do
        flags =
        Enum.filter(@bit_values, fn({_keyword, val}) -> Bitwise.band(val, bitmask) > 0 end)
        |> Keyword.keys()

        %__MODULE__{bitmask: bitmask, flags: flags}
      end

      def atom_flags_to_bitmask(atom_flags) when is_list(atom_flags) do
        bitmask =
        Keyword.take(@bit_values, atom_flags)
        |> Enum.reduce(0, fn({_flag_name, value}, acc) ->
          acc + value
        end)

        %__MODULE__{bitmask: bitmask, flags: atom_flags}
      end

      def has_flag(%__MODULE__{bitmask: bitmask, flags: _}, flag) when is_atom(flag) do
        Bitwise.band(bitmask, atom_to_bitmask(flag)) > 0
      end

      def has_flag(bitmask, flag) when is_integer(bitmask) and is_atom(flag) do
        Bitwise.band(bitmask, atom_to_bitmask(flag)) > 0
      end

      unquote(ecto_code)
    end

    #Macro.to_string(ast)

    #|> IO.puts


    ast
  end

  @doc """
    Returns the bitmask with no flags set
      iex> MyBitmask.none()
      %MyBitmask{bitmask: 0, flags: []}

  """
  @doc since: "0.3.0"
  @callback none() :: %{bitmask: integer(), flags: list(atom())}

  @doc """
    Returns the bitmask with all flags set
      iex> MyBitmask.all()
      %MyBitmask{bitmask: 15, flags: [:flag_1, :flag_2, :flag_3, :flag_4]}

  """
  @doc since: "0.3.0"
  @callback all() :: %{bitmask: integer(), flags: list(atom())}

  @doc """
    Converts an atom from the bitmask into its underlying value
      iex> MyBitmask.atom_to_bitmask(:flag_3)
      4

  """
  @doc since: "0.1.0"
  @doc group: "Generated Functions"
  @callback atom_to_bitmask(atom()) :: integer()

  @doc """
    Converts a list of atoms into a bitmask
      iex> MyBitmask.atom_flags_to_bitmask([:flag_1, :flag_3])
      %MyBitmask{bitmask: 5, flags: [:flag_1, :flag_3]}

  """
  @doc since: "0.1.0"
  @doc group: "Generated Functions"
  @callback atom_flags_to_bitmask(list(atom())) :: %{bitmask: integer(), flags: list(atom())}

  @doc """
    Converts an integer into a bitmask
      iex> MyBitmask.int_to_bitmask(3)
      %MyBitmask{bitmask: 3, flags: [:flag_1, :flag_2]}

  """
  @doc since: "0.1.0"
  @doc group: "Generated Functions"
  @callback int_to_bitmask(integer()) :: %{bitmask: integer(), flags: list(atom())}

  @doc """
    Checks if a bitmask has a flag set
      iex> bitmask = MyBitmask.int_to_bitmask(3)
      %MyBitmask{bitmask: 3, flags: [:flag_1, :flag_2]}
      iex> MyBitmask.has_flag(bitmask, :flag_2)
      true

  """
  @doc since: "0.1.0"
  @callback has_flag(%{bitmask: integer(), flags: list(atom())} | integer(), atom()) :: boolean()

  #Bitmask behaviour, for documentation

  @doc """
    Returns the a list of all the bitmasks flags
      iex> MyBitmask.get_all_values()
      [flag_1: 1, flag_2: 2, flag_3: 4, flag_4: 8]

  """
  @doc since: "0.1.0"
  @doc group: "Generated Functions"
  @callback get_all_values() :: list({atom(), integer()})
end