lib/radius/dict.ex

defmodule Radius.Dict do
  @moduledoc """
  Parses dictionaries and generates lookup functions and helper macro's.

  The helper macro's give the benefit of compile time checks and faster encoding without losing
  readability.
  """
  require Radius.Dict.Helpers
  alias Radius.Dict.EntryNotFoundError
  alias Radius.Dict.Helpers
  alias Radius.Dict.Parser

  defmacro __using__(_) do
    quote do
      require Radius.Dict
      import Radius.Dict
    end
  end

  extra_dictionaries = Application.compile_env(:elixir_radius, :extra_dictionaries, [])

  {includes, generic_attributes, generic_values} =
    Path.join(["dicts", "dictionary"])
    |> File.read!()
    |> Parser.parse_index()

  filtered_includes =
    if config_includes = Application.compile_env(:elixir_radius, :included_dictionaries) do
      Enum.filter(includes, &(&1 in config_includes))
    else
      includes
    end

  dict_files =
    Enum.map(filtered_includes, fn dict -> Path.join(["dicts", "dictionary.#{dict}"]) end)

  {vendors, attributes, values} =
    Enum.reduce(
      dict_files ++ extra_dictionaries,
      {[], generic_attributes, generic_values},
      fn dict_file, {acc_vendors, acc_attributes, acc_values} ->
        dict =
          dict_file
          |> File.read!()
          |> Parser.parse()

        {acc_attributes, rest} =
          case Map.pop(dict, :attributes) do
            {[], rest} -> {acc_attributes, rest}
            {attributes, rest} -> {attributes ++ acc_attributes, rest}
          end

        {acc_values, rest} =
          case Map.pop(rest, :values) do
            {[], rest} -> {acc_values, rest}
            {values, rest} -> {values ++ acc_values, rest}
          end

        acc_vendors =
          if vendor = Map.get(rest, :vendor) do
            [vendor | acc_vendors]
          else
            acc_vendors
          end

        {acc_vendors, acc_attributes, acc_values}
      end
    )

  Helpers.define_attribute_doc_helpers()

  for attribute <- attributes do
    Helpers.define_attribute_functions(attribute)
  end

  Helpers.define_value_doc_helpers()

  for val <- values do
    Helpers.define_value_functions(val)
  end

  @doc """
  Get vendor struct based on vendor id
  """
  @doc group: :lookup
  def vendor_by_id(attr_id)

  @doc """
  Get vendor struct based on vendor name
  """
  @doc group: :lookup
  def vendor_by_name(attr_name)

  for vendor <- vendors do
    mod = Module.concat(__MODULE__, Helpers.safe_name("Vendor#{vendor.name}"))
    [tl, ll] = Keyword.get(vendor.opts, :format) || [1, 1]
    vendor_data = %{id: vendor.id, name: vendor.name, format: {tl, ll}, module: mod}
    def vendor_by_id(unquote(vendor.id)), do: unquote(Macro.escape(vendor_data))
    def vendor_by_name(unquote(vendor.name)), do: unquote(Macro.escape(vendor_data))

    attribute_funs =
      for attribute <- vendor.attributes do
        quote do
          unquote(Helpers.generate_attribute_functions(Macro.escape(attribute)))
        end
      end

    elixir_compiler_limit = 1024

    value_funs =
      if Enum.count(vendor.attributes ++ vendor.values) < elixir_compiler_limit do
        for val <- vendor.values do
          quote do
            unquote(Helpers.generate_value_functions(Macro.escape(val)))
          end
        end
      else
        for {attr, vals} <- Enum.group_by(vendor.values, & &1.attr) do
          val_fun_name = Helpers.safe_name("val_#{attr}")

          quote location: :keep do
            defmacro unquote(val_fun_name)(val_name),
              do:
                Enum.find_value(
                  unquote(Macro.escape(vals)),
                  &(&1.name == val_name && &1.value)
                )

            def value_by_value(unquote(attr), val_value),
              do:
                Enum.find(
                  unquote(Macro.escape(vals)),
                  &(&1.value == val_value)
                )

            def value_by_name(unquote(attr), val_name),
              do:
                Enum.find(
                  unquote(Macro.escape(vals)),
                  &(&1.name == val_name)
                )
          end
        end
      end

    IO.puts("Compiling #{mod}")

    value_doc_funs = if value_funs == [], do: [], else: [Helpers.generate_value_doc_helpers()]

    module_body =
      [Helpers.generate_attribute_doc_helpers()] ++
        attribute_funs ++ value_funs ++ value_doc_funs

    Module.create(mod, module_body, Macro.Env.location(__ENV__))
  end

  def attribute_by_id(id), do: raise(EntryNotFoundError, type: :attribute, key: id)
  def attribute_by_name(name), do: raise(EntryNotFoundError, type: :attribute, key: name)
  def value_by_value(attr, _val), do: raise(EntryNotFoundError, type: :value, key: attr)
  def value_by_name(attr, _val_name), do: raise(EntryNotFoundError, type: :value, key: attr)
  def vendor_by_id(id), do: raise(EntryNotFoundError, type: :vendor, key: id)
  def vendor_by_name(name), do: raise(EntryNotFoundError, type: :vendor, key: name)
end