Current section
Files
Jump to
Current section
Files
lib/dynamic_module.ex
defmodule DynamicModule do
@moduledoc """
Generate a new module based on AST.
"""
@doc """
Generate `.beam` and `.ex` files based on:
* `mod_name` - the module name
* `preamble` - the module attibutes and directives in AST, e.g. `use GenServer`, `require Logger`, etc
* `contents` - the module body in AST
## Options
* `:doc` - the documents to the module. Defaults to `false`
* `:path` - the path for generated `.ex` files. Defaults to `""`
* `:type` - the file type of generated file. Could be `:code` or `:test`, Defaults to `:code`
* `:create` - a boolean value indicates whether to create `.beam` file. Defaults to `true`
"""
defmacro gen(mod_name, preamble, contents, opts \\ []) do
quote bind_quoted: [
mod_name: mod_name,
preamble: preamble,
contents: contents,
opts: opts
] do
alias Mix.Tasks.Format
mod_doc = Keyword.get(opts, :doc, false)
path = Keyword.get(opts, :path, "")
type = Keyword.get(opts, :type, :code)
create? = Keyword.get(opts, :create, true)
format? = Keyword.get(opts, :format, true)
output? = Keyword.get(opts, :output, true)
moduledoc =
quote do
@moduledoc unquote(mod_doc)
end
name = String.to_atom("Elixir.#{mod_name}")
if create? do
Module.create(
name,
[moduledoc] ++ [preamble] ++ [contents],
Macro.Env.location(__ENV__)
)
end
if path != "" do
case File.mkdir_p(path) do
{:error, :enoent} ->
[:red, "Failed to create #{path}."]
|> IO.ANSI.format()
|> IO.puts()
:ok ->
filename =
case type do
:code -> Path.join(path, "#{mod_name}.ex")
:test -> Path.join(path, "#{mod_name}_test.exs")
end
term =
if is_list(contents) do
quote do
defmodule unquote(name) do
unquote(moduledoc)
unquote(preamble)
unquote_splicing(contents)
end
end
else
quote do
defmodule unquote(name) do
unquote(moduledoc)
unquote(preamble)
unquote(contents)
end
end
end
term =
term
|> Macro.to_string()
|> String.replace(~r/(\(\s|\s\))/, "")
|> String.replace(
~r/(def|defp|defmodule|create|create_if_not_exists|get|post|patch|delete|object|enum|schema)\((.*?)\) do/,
"\\1 \\2 do"
)
|> String.replace(
~r/(alias|require|import|pipe|use|plug|forward|field|has_one|has_many|add|timestamps|drop|drop_if_exists)\((.*?)\)\n/,
"\\1 \\2\n"
)
File.write!(filename, term)
if format? do
Format.run([filename])
end
if output? do
[
"Module",
:cyan,
" #{mod_name} ",
:reset,
"is generated. File created at ",
:cyan,
"#{filename}."
]
|> IO.ANSI.format()
|> IO.puts()
end
end
end
end
end
@doc """
Generate module name based on `app`, `prefix` and `name`. The `postfix` is optional.
## Examples
iex> DynamicModule.gen_module_name(:app, "Prefix", "name")
"App.Prefix.Name"
iex> DynamicModule.gen_module_name(:app, "Prefix", "name", "Postfix")
"App.Prefix.Name.Postfix"
"""
@spec gen_module_name(atom(), String.t(), String.t(), String.t()) :: String.t()
def gen_module_name(app, prefix, name, postfix \\ "") do
app = app |> Atom.to_string() |> Recase.to_pascal()
name = Recase.to_pascal(name)
case postfix do
"" -> "#{app}.#{prefix}.#{name}"
_ -> "#{app}.#{prefix}.#{name}.#{postfix}"
end
end
@doc """
Normalize name to atom.
## Examples
iex> DynamicModule.normalize_name("a")
:a
iex> DynamicModule.normalize_name(:abc)
:abc
"""
@spec normalize_name(String.t() | atom()) :: atom()
def normalize_name(nil), do: nil
def normalize_name(name) when is_atom(name), do: name
def normalize_name(name), do: String.to_atom(name)
end