Packages

This library provides an easy way to make barrel files in Elixir. A barrel is a way to rollup exports from several modules into a single convenient module.

Current section

Files

Jump to
ex_export lib ex_export.ex
Raw

lib/ex_export.ex

defmodule ExExport do
@show_definitions Application.compile_env(:ex_export, :show_definitions, false)
@moduledoc """
This module inspects another module for public functions and generates the defdelegate needed to add them to the local modules name space
"""
@doc """
require in the module and them call export for each module you want to import.
The function list automatically filters out functions that start with an underscore
## Examples
defmodule Sample do
require ExExport
alias Sample.Actions.Greet
ExExport.export(Greet)
ExExport.export(Sample.Actions.Farewell)
end
## Options
* `:only` - (since v0.2.0) a list of [function: arity] only matching functions will be delegated
* `:exclude` - (since v0.2.0) a list of [function: arity] matching functions will *NOT* be delegated
* `:expansion` - (since v.0.8.1) :manual or :macro -
:macro - use Macro.expand. This seems to generate a compile time dependency
:manual - uses custom code to figure out the alias resolution -which prevents the compile
time dependency but can generate a **warning - alias is unused** because we are resolving it. You
can get rid of the warning by changing to :macro, undoing the alias or adding warn: false to the alias
## See the Output
In the configuration file for the environment you wish to render the
data attributes, you can set the `show_definitions` to true. This
will output the code that is being injected in a readable form. This can be useful
if you get warnings like (Cannot match because already defined)
```elixir
config :ex_export, :show_definitions, true
```
"""
defmacro export(module, opts \\ []) do
# DJE - changed to a manual method to remove a compile time dependency
resolved_module = expand_module(module, __CALLER__, get_keyword(opts, :expansion, :manual))
only = resolve_restriction(get_keyword(opts, :only), __CALLER__)
exclude = resolve_restriction(get_keyword(opts, :exclude), __CALLER__)
if exclude && only do
raise ArgumentError,
message: ":only and :exclude are mutually exclusive"
end
ExExport.output_definition("<<<<< Exporting To #{inspect(__CALLER__.context_modules)}>>>>")
try do
resolved_module.__info__(:functions)
|> Enum.map(
fn {func, arity} ->
if not private_func(func) and included(func, arity, only) and
not_excluded(func, arity, exclude) do
use_delegate(func, build_args(arity), resolved_module)
else
ExExport.output_definition("Skipping #{func}/#{arity}")
end
end
)
rescue
e in UndefinedFunctionError ->
IO.puts(
"ExExport doesn't support aliasing - you must use the canonical name of the module.
This prevents the need for a compile time dependency."
)
raise e
end
end
def show_definitions?, do: @show_definitions
if @show_definitions do
def output_definition(msg), do: IO.puts( msg)
else
def output_definition(_msg), do: nil
end
def expand_module(module, caller, method \\ :manual)
def expand_module(module, caller, :macro) do
Macro.expand(module, caller)
end
def expand_module(module, caller, :manual) do
# IO.inspect(module, label: "MOdule") R
# IO.inspect(caller.aliases, label: "Caller Aliaases")
case module do
{:__aliases__, _, parts} ->
resolve_module_name(parts, caller.aliases)
_ ->
raise "Don't know how to handle #{inspect(module)}"
end
end
defp find_alias(key, aliases) do
case Enum.find(
aliases,
fn {alias_key, _module} ->
# IO.puts("Comparing #{key} to #{alias_key}")
"Elixir.#{key}" == "#{alias_key}"
end
) do
nil -> nil
{_, module} -> module
end
end
defp resolve_module_name(parts, aliases) do
# IO.inspect(parts, label: "Parts")
# IO.inspect(aliases, label: "Aliases")
cond do
Enum.count(aliases) > 0 ->
[head | rest] = parts
case find_alias(head, aliases) do
nil ->
parts
result ->
[
result
|> Atom.to_string()
|> String.split(".")
|> List.wrap(),
rest
]
|> List.flatten()
|> Enum.reject(&is_nil/1)
end
true ->
parts
end
|> then(
fn list ->
case Enum.at(list, 0) do
"Elixir" -> list
:"Elixir" -> list
_ -> ["Elixir" | parts]
end
end
)
|> Enum.join(".")
|> String.to_atom()
end
defp get_keyword(list, label, default \\ nil) do
case Keyword.fetch(list, label) do
:error -> default
{:ok, val} -> val
end
end
defp private_func(func) do
String.at("#{func}", 0) == "_"
end
defp included(_func, _arity, nil), do: true
defp included(func, arity, only) do
found?(func, arity, only)
end
defp not_excluded(_func, _arity, nil), do: true
defp not_excluded(func, arity, exclude) do
if found?(func, arity, exclude), do: false, else: true
end
defp found?(func, arity, list) do
result =
list
|> Enum.find_index(
fn {f, a} ->
f == func and a == arity
end
)
|> is_nil
!result
end
defp build_args(0), do: []
defp build_args(arity) do
Enum.to_list(1..arity)
|> Enum.map(fn idx -> "arg#{idx}" end)
end
defp use_delegate(func, args, resolved_module) do
str_args = Enum.join(args, ",")
{:ok, func_args} = Code.string_to_quoted("#{func}(#{str_args})")
output_definition("defdelegate #{func}(#{str_args}), to: #{resolved_module}")
quote do
defdelegate unquote(func_args), to: unquote(resolved_module)
end
end
defp resolve_restriction(value, env) do
cond do
is_nil(value) -> nil
is_list(value) -> value
true ->
new_ast = Macro.prewalk(
value,
fn
{:__aliases__, meta, [alias]} ->
case Macro.Env.fetch_alias(env, alias) do
{:ok, module} -> result = module
|> Atom.to_string()
|> String.split(".")
|> List.wrap()
|> Enum.map(fn i -> String.to_existing_atom(i) end)
{:__aliases__, meta, result}
_ -> {:__aliases__, meta, [alias]}
end
other -> other
end
)
new_ast
|> Macro.expand(env)
|> Code.eval_quoted()
|> then(
fn {result, _} ->
result
end
)
end
end
end