Skip to content

Instantly share code, notes, and snippets.

@nwalker
Forked from smpallen99/constants.ex
Last active May 18, 2019 01:37
Show Gist options
  • Save nwalker/b452c82a7b861f740b52b74346ccf211 to your computer and use it in GitHub Desktop.
Save nwalker/b452c82a7b861f740b52b74346ccf211 to your computer and use it in GitHub Desktop.
Approach for constants shared between modules in Elixir
defmodule Constants do
defmodule Extractor do
require Record
Record.defrecord :tree, [:type, :pos, data: :undefined]
Record.defrecord :attr, :attribute, [:name, :args]
def extract(opts) do
cond do
lib = opts[:from_lib] -> from_lib_file(lib)
file = opts[:from_file] -> from_file(file)
end |> read_forms() |> decode_forms()
end
defp from_file(file) do
file = String.to_char_list(file)
case :code.where_is_file(file) do
:non_existing -> file
realfile -> realfile
end
end
def from_lib_file(file) do
[app|path] = :filename.split(String.to_char_list(file))
case :code.lib_dir(List.to_atom(app)) do
{:error, _} ->
raise ArgumentError, "lib file #{file} could not be found"
libpath ->
:filename.join([libpath|path])
end
end
def read_forms(filename) do
case :epp_dodger.parse_file(filename, []) do
{:ok, forms} -> forms
other -> raise ArgumentError, "error parsing file #{filename}, got #{inspect other}"
end
end
def decode_forms(forms) do
for tree(type: :attribute, data: attr) <- forms do
decode_attribute(attr)
end
|> Enum.filter_map(&(!is_nil(&1)), &rename_constant/1)
end
def rename_constant({key, val}) do
name = key |> Atom.to_string |> String.downcase |> String.to_atom
{name, val}
end
def decode_attribute(attr(name: {:atom, _, :define}, args: args)) do
with [arg1, arg2] <- args,
{:var, _pos, name} <- arg1,
{:ok, val} <- decode_val(arg2)
do
{:ok, name, val}
end |> case do
{:ok, name, val} -> {name, val}
_other -> nil
end
end
def decode_attribute(_), do: nil
def decode_val(tree(type: :binary, data: data)) do
extract_constant(:binary, data)
end
def decode_val(_), do: :unsupported_type
def extract_constant(:binary, [tree(type: :binary_field, data: data)]) do
with {:binary_field, f, _} <- data,
{:string, _, s} <- f, do: {:ok, to_string(s)}
end
def extract_constant(_type, _tree), do: nil
end
defmacro __using__(_opts) do
quote do
import Constants
end
end
defdelegate(extract(opts), to: Constants.Extractor)
@doc "Define a constant. An alias for constant"
defmacro define(name, value) do
quote do
defmacro unquote(name), do: unquote(value)
end
end
@doc "Define multiple constants"
defmacro define_all(constants) do
quote bind_quoted: [constants: constants, line: __CALLER__.line], location: :keep do
for {name, value} <- constants do
defmacro unquote({name, [line: line], Elixir}), do: unquote(value)
end
end
end
end
defmodule NSs do
use Constants
define_all Constants.extract(from_lib: "ejabberd/include/ns.hrl")
end
defmodule ConstantsTest do
use ExUnit.Case
alias Constants.Extractor, as: CE
require NSs
test "check extractor" do
d = CE.extract(from_lib: "ejabberd/include/ns.hrl")
assert Enum.count(d) > 0
end
test "check defined constants" do
assert NSs.ns_auth == "jabber:iq:auth"
end
end
@DerKastellan
Copy link

I wish I had seen this fork years ago before I coded essentially the same...

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment