Current section
Files
Jump to
Current section
Files
lib/arke_new/generator.ex
defmodule Arke.New.Generator do
@moduledoc false
import Mix.Generator
alias Arke.New.{Project}
@arke Path.expand("../..", __DIR__)
@arke_version Version.parse!(Mix.Project.config()[:version])
@callback prepare_project(Project.t()) :: Project.t()
@callback generate(Project.t()) :: Project.t()
defmacro __using__(_env) do
quote do
@behaviour unquote(__MODULE__)
import Mix.Generator
import unquote(__MODULE__)
Module.register_attribute(__MODULE__, :templates, accumulate: true)
@before_compile unquote(__MODULE__)
end
end
defmacro __before_compile__(env) do
root = Path.expand("../../templates", __DIR__)
templates_ast =
for {name, mappings} <- Module.get_attribute(env.module, :templates) do
for {format, _proj_location, files} <- mappings,
format != :keep,
{source, _target} <- files,
source = to_string(source) do
path = Path.join(root, source)
if format in [:config, :prod_config, :eex] do
compiled = EEx.compile_file(path)
quote do
@external_resource unquote(path)
@file unquote(path)
def render(unquote(name), unquote(source), var!(assigns))
when is_list(var!(assigns)),
do: unquote(compiled)
end
else
quote do
@external_resource unquote(path)
def render(unquote(name), unquote(source), _assigns), do: unquote(File.read!(path))
end
end
end
end
quote do
unquote(templates_ast)
def template_files(name), do: Keyword.fetch!(@templates, name)
end
end
defmacro template(name, mappings) do
quote do
@templates {unquote(name), unquote(mappings)}
end
end
def copy_from(%Project{} = project, mod, name) when is_atom(name) do
mapping = mod.template_files(name)
for {format, project_location, files} <- mapping,
{source, target_path} <- files,
source = to_string(source) do
target = Project.join_path(project, project_location, target_path)
case format do
:keep ->
File.mkdir_p!(target)
:zip ->
parent_dir = Path.dirname(target)
Mix.shell().info([:green, "* extracting ", :reset, Path.relative_to_cwd(target)])
File.mkdir_p!(parent_dir)
zip_contents = mod.render(name, source, project.binding)
{:ok, zip} = :zip.zip_open(zip_contents, [:memory])
{:ok, files} = :zip.zip_get(zip)
Enum.map(files, fn {path, contents} ->
full_path = Path.join(parent_dir, path)
File.mkdir_p!(Path.dirname(full_path))
File.write!(full_path, contents)
end)
:text ->
create_file(target, mod.render(name, source, project.binding))
:config ->
contents = mod.render(name, source, project.binding)
config_inject(Path.dirname(target), Path.basename(target), contents)
:prod_config ->
contents = mod.render(name, source, project.binding)
prod_only_config_inject(Path.dirname(target), Path.basename(target), contents)
:eex ->
contents = mod.render(name, source, project.binding)
create_file(target, contents)
end
end
end
def config_inject(path, file, to_inject) do
file = Path.join(path, file)
contents =
case File.read(file) do
{:ok, bin} -> bin
{:error, _} -> "import Config\n"
end
with :error <- split_with_self(contents, "use Mix.Config"),
:error <- split_with_self(contents, "import Config") do
Mix.raise(~s[Could not find "use Mix.Config" or "import Config" in #{inspect(file)}])
else
[left, middle, right] ->
write_formatted!(file, [left, middle, ?\n, ?\n, to_inject, right])
end
end
def prod_only_config_inject(path, file, to_inject) do
file = Path.join(path, file)
contents =
case File.read(file) do
{:ok, bin} ->
bin
{:error, _} ->
"""
import Config
if config_env() == :prod do
end
"""
end
case split_with_self(contents, "if config_env() == :prod do") do
[left, middle, right] ->
write_formatted!(file, [left, middle, ?\n, to_inject, right])
:error ->
Mix.raise(~s[Could not find "if config_env() == :prod do" in #{inspect(file)}])
end
end
defp write_formatted!(file, contents) do
formatted = contents |> IO.iodata_to_binary() |> Code.format_string!()
File.write!(file, [formatted, ?\n])
end
defp split_with_self(contents, text) do
case :binary.split(contents, text) do
[left, right] -> [left, text, right]
[_] -> :error
end
end
def in_umbrella?(app_path) do
umbrella = Path.expand(Path.join([app_path, "..", ".."]))
mix_path = Path.join(umbrella, "mix.exs")
apps_path = Path.join(umbrella, "apps")
File.exists?(mix_path) && File.exists?(apps_path)
end
def put_binding(%Project{opts: opts} = project) do
db = Keyword.get(opts, :db, "postgres")
dev = Keyword.get(opts, :dev, false)
dashboard = Keyword.get(opts, :dashboard, false)
arke_path = arke_path(project, dev, false)
mailer = Keyword.get(opts, :mailer, false)
# TODO: create database using arke_postgres
pubsub_server = get_pubsub_server(project.app_mod)
version = @arke_version
binding = [
app_name: project.app,
app_module: inspect(project.app_mod),
root_app_name: project.root_app,
root_app_module: inspect(project.root_mod),
arke_app_name: project.arke_app,
lib_arke_app: project.lib_arke_app,
endpoint_module: inspect(Module.concat(project.arke_ns, Endpoint)),
arke_ns: inspect(project.arke_ns),
arke_dep: arke_dep(arke_path, db),
db: db,
dashboard: dashboard,
mailer: mailer,
arke_version: version,
pubsub_server: pubsub_server,
secret_key_base_dev: random_string(64),
secret_key_base_test: random_string(64),
signing_salt: random_string(8),
lv_signing_salt: random_string(8),
generators: nil_if_empty(project.generators),
namespaced?: namespaced?(project),
dev: dev
]
%Project{project | binding: binding}
end
defp namespaced?(project) do
Macro.camelize(project.app) != inspect(project.app_mod)
end
defp get_pubsub_server(module) do
module
|> Module.split()
|> hd()
|> Module.concat(PubSub)
end
defp kw_to_config(kw) do
Enum.map(kw, fn
{k, {:literal, v}} -> ",\n #{k}: #{v}"
{k, v} -> ",\n #{k}: #{inspect(v)}"
end)
end
defp nil_if_empty([]), do: nil
defp nil_if_empty(other), do: other
defp arke_path(%Project{} = project, true = _dev, umbrella_root?) do
absolute = Path.expand(project.project_path)
relative = Path.relative_to(absolute, @arke)
if absolute == relative do
Mix.raise("--dev projects must be generated inside Arke directory")
end
project
|> arke_path_prefix(umbrella_root?)
|> Path.join(relative)
|> Path.split()
|> Enum.map(fn _ -> ".." end)
|> Path.join()
end
defp arke_path(%Project{}, false = _dev, _umbrella_root?) do
"deps/arke"
end
defp arke_path_prefix(_, _), do: ".."
defp arke_dep("deps/arke", db) do
~s[{:arke, github: "arkemishub/arke", override: true},
{:arke_server, github: "arkemishub/arke_server", override: true},
{:arke_auth, github: "arkemishub/arke_auth", override: true},
#{get_arke_db(db)}]
end
defp arke_dep(path, db) do
~s[{:arke, path: #{inspect(path)}, override: true},
{:arke_auth, path: #{inspect(path)}, override: true},
{:arke_server, path: #{inspect(path)}, override: true},
#{get_arke_db(db, path)}]
end
# GITHUB
defp get_arke_db("postgres"),
do: ~s({:arke_postgres, github: "arkemishub/arke_postgres", override: true})
defp get_arke_db(_),
do: ~s({:arke_postgres, github: "arkemishub/arke_postgres", override: true})
# LOCAL
defp get_arke_db("postgres", path),
do: ~s({:arke_postgres, path: #{inspect(path)}, override: true})
defp get_arke_db(_ = _db, path),
do: ~s({:arke_postgres, path: #{inspect(path)}, override: true})
defp random_string(length),
do: :crypto.strong_rand_bytes(length) |> Base.encode64() |> binary_part(0, length)
end