Current section
Files
Jump to
Current section
Files
lib/common_x/application_x.ex
defmodule ApplicationX do
@moduledoc ~S"""
Application module extended functions.
"""
alias Mix.Project
import Mix.Task, only: [run: 2]
@ignore [:kernel, :stdlib, :elixir, :logger]
@load_error 'no such file or directory'
task_module =
if p = Process.whereis(Mix.TasksServer) do
p
|> Agent.get(&Map.keys/1)
|> Enum.find_value(false, fn
{:task, task, m} when task in ~W(app.start deps.loadpaths) -> m
_ -> false
end)
end
main_project =
case task_module do
falsy when falsy in [nil, false] -> falsy
Mix.InstallProject -> []
module -> module.project()
end
main_app =
if main_project do
main_project[:app]
else
[
Path.join(File.cwd!(), "../../mix.exs"),
Path.join(File.cwd!(), "mix.exs"),
Path.join(__DIR__, "../../../../mix.exs"),
Path.join(__DIR__, "../../mix.exs")
]
|> Enum.find_value("", fn file ->
case File.read(file) do
{:ok, d} -> d
_ -> false
end
end)
|> (fn v ->
~r/app:\W*:(?<app>[a-z\_]+)/
|> Regex.named_captures(v)
|> Kernel.||(%{})
|> Map.get("app", to_string(Mix.Project.config()[:app]))
end).()
|> String.to_atom()
end
@doc ~S"""
The atom of the current main application.
For example take an application called `:my_app`,
which includes the `:my_dep` dependencies,
which has `:common_x` as dependency.
So:
```
:my_app
├── ...
└── :my_dep
├── ...
└── :common_x
```
In that scenario calling `ApplicationX.main` will return `:my_app`
both for code in `:my_app` and `:my_dep`.
## Examples
```elixir
iex> ApplicationX.main
:common_x
```
"""
@spec main :: atom
def main, do: unquote(main_app)
@doc ~S"""
The mix configuration of the current main application.
For example take an application called `:my_app`,
which includes the `:my_dep` dependencies,
which has `:common_x` as dependency.
So:
```
:my_app
├── ...
└── :my_dep
├── ...
└── :common_x
```
In that scenario calling `ApplicationX.main` will return
the mix config of `:my_app` both for code in `:my_app` and `:my_dep`.
## Examples
```elixir
iex> config = ApplicationX.main_project
iex> config[:app]
:common_x
iex> config[:description]
"Extension of common Elixir modules."
```
"""
@spec main_project :: Keyword.t()
def main_project, do: unquote(Macro.escape(main_project || []))
env =
cond do
e = System.get_env("MIX_ENV") ->
String.to_existing_atom(e)
p = Process.whereis(Mix.TasksServer) ->
tasks =
p
|> Agent.get(&Map.keys(&1))
|> Enum.map(&elem(&1, 1))
|> Enum.uniq()
preferred =
if main_project, do: Keyword.get(main_project, :preferred_cli_env, []), else: []
path =
case :lists.reverse(String.split(to_string(:code.priv_dir(:common_x)), "/")) do
["priv", "common_x", "lib", env, "_build" | _] -> env
_ -> nil
end
cond do
env = %{"test" => :test, "dev" => :dev, "prod" => :prod}[path] -> env
env = Enum.find_value(tasks, &Mix.Task.preferred_cli_env/1) -> env
env = Enum.find_value(tasks, &Keyword.get(preferred, String.to_atom(&1))) -> env
:default -> :dev
end
:default ->
:prod
end
@doc ~S"""
Get the current Mix environment.
## Example
```elixir
iex> ApplicationX.mix_env
:test
```
"""
@spec mix_env :: atom
def mix_env, do: unquote(env)
@doc ~S"""
List all available applications excluding system ones.
This function is save to run in `Mix.Task`s.
## Example
Since `:common_x` does not have any dependencies:
```elixir
iex> ApplicationX.applications
[:common_x]
```
"""
@spec applications :: [atom]
def applications do
app = if(Process.whereis(Mix.ProjectStack), do: Project.config()[:app])
if app do
with {:error, {@load_error, _}} <- :application.load(app), do: run("loadpaths", [])
gather_applications([app])
else
:application.loaded_applications()
|> Enum.map(&elem(&1, 0))
|> Enum.reject(&(&1 in @ignore))
|> gather_applications()
end
end
@doc ~S"""
List all dependant applications excluding system ones.
Does includes the given application.
This function is save to run in `Mix.Task`s.
## Example
Since `:common_x` does not have any dependencies:
```elixir
iex> ApplicationX.applications(:common_x)
[:common_x]
```
Duplicates are ignored and only returned once:
```elixir
iex> ApplicationX.applications([:common_x, :common_x])
[:common_x]
```
Unknown applications are safe, but returned:
```elixir
iex> ApplicationX.applications(:fake)
[:fake]
```
"""
@spec applications(atom | [atom]) :: [atom]
def applications(app) when is_atom(app), do: gather_applications([app])
def applications(apps), do: gather_applications(apps)
@doc ~S"""
List all available modules excluding system ones.
This function is save to run in `Mix.Task`s.
## Example
Since `:common_x` does not have any dependencies:
```elixir
iex> ApplicationX.modules
[ApplicationX, CodeX, CommonX, EnumX, MacroX, MapX]
```
"""
@spec modules :: [module]
def modules do
app = if(Process.whereis(Mix.ProjectStack), do: Project.config()[:app])
if app do
with {:error, {@load_error, _}} <- :application.load(app), do: run("loadpaths", [])
modules(app)
else
:application.loaded_applications()
|> Enum.map(&elem(&1, 0))
|> Enum.reject(&(&1 in @ignore))
|> modules()
end
end
@doc ~S"""
List all available modules for the given app[s] and dependencies of those apps.
This excludes system modules.
This function is save to run in `Mix.Task`s.
## Example
Normally system modules are excluded,
but can be added by manually passing the respective system application:
```elixir
iex> ApplicationX.modules(:logger)
[Logger, Logger.App, Logger.BackendSupervisor, Logger.Backends.Console,
Logger.Config, Logger.Counter, Logger.Filter, Logger.Formatter, Logger.Handler,
Logger.Translator, Logger.Utils, Logger.Watcher]
```
Duplicate applications are ignored:
```elixir
iex> ApplicationX.modules([:logger, :logger])
[Logger, Logger.App, Logger.BackendSupervisor, Logger.Backends.Console,
Logger.Config, Logger.Counter, Logger.Filter, Logger.Formatter, Logger.Handler,
Logger.Translator, Logger.Utils, Logger.Watcher]
```
Unknown applications are safe to pass:
```elixir
iex> ApplicationX.modules(:fake)
[]
```
"""
@spec modules(atom | [atom]) :: [module]
def modules(app) when is_atom(app), do: gather_modules([app])
def modules(apps), do: gather_modules(apps)
### Helpers ###
@spec gather_applications([atom], [atom]) :: [atom]
defp gather_applications(apps, acc \\ [])
defp gather_applications([], acc), do: acc
defp gather_applications([app | t], acc) do
if app in acc do
gather_applications(t, acc)
else
:application.load(app)
gather =
case :application.get_all_key(app) do
{:ok, data} -> t ++ ((data[:applications] || []) -- @ignore)
_ -> t
end
gather_applications(gather, [app | acc])
end
end
@spec gather_modules([atom], %{required(atom) => [module]}) :: [module]
defp gather_modules(apps, acc \\ %{})
defp gather_modules([], acc), do: acc |> Map.values() |> List.flatten()
defp gather_modules([app | t], acc) do
if Map.has_key?(acc, app) do
gather_modules(t, acc)
else
:application.load(app)
case :application.get_all_key(app) do
{:ok, data} ->
gather_modules(
t ++ ((data[:applications] || []) -- @ignore),
Map.put(acc, app, data[:modules] || [])
)
_ ->
gather_modules(t, acc)
end
end
end
end