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