Packages

Generators for Elixir apps using Dotenvy for config including variants of the phx_new generators

Current section

Files

Jump to
dotenvy_generators lib dot_new generator.ex
Raw

lib/dot_new/generator.ex

defmodule DotenvyGenerators.Generator do
@moduledoc """
Modified from `Mix.Generator`
Conveniences for working with paths and generating content.
"""
@doc ~S"""
Creates a file with the given contents.
If the file already exists and the contents are not the same,
it asks for user confirmation.
## Options
* `:force` - forces creation without a shell prompt
* `:quiet` - does not log command output
## Examples
iex> Mix.Generator.create_file(".gitignore", "_build\ndeps\n")
* creating .gitignore
true
"""
@spec create_file(Path.t(), iodata, keyword) :: boolean()
def create_file(path, contents, opts \\ []) when is_binary(path) do
log(:green, :creating, Path.relative_to_cwd(path), opts)
if opts[:force] || overwrite?(path, contents) do
File.mkdir_p!(Path.dirname(path))
File.write!(path, contents)
true
else
false
end
end
@doc """
Creates a directory if one does not exist yet.
This function does nothing if the given directory already exists; in this
case, it still logs the directory creation.
## Options
* `:quiet` - does not log command output
## Examples
iex> Mix.Generator.create_directory("path/to/dir")
* creating path/to/dir
true
"""
@spec create_directory(Path.t(), keyword) :: true
def create_directory(path, options \\ []) when is_binary(path) do
log(:green, "creating", Path.relative_to_cwd(path), options)
File.mkdir_p!(path)
true
end
@doc ~S"""
Copies `source` to `target`.
If `target` already exists and the contents are not the same,
it asks for user confirmation.
## Options
* `:force` - forces copying without a shell prompt
* `:quiet` - does not log command output
## Examples
iex> Mix.Generator.copy_file("source/gitignore", ".gitignore")
* creating .gitignore
true
"""
@doc since: "1.9.0"
@spec copy_file(Path.t(), Path.t(), keyword) :: boolean()
def copy_file(source, target, options \\ []) do
create_file(target, File.read!(source), options)
end
@doc ~S"""
Evaluates and copy templates at `source` to `target`.
The template in `source` is evaluated with the given `assigns`.
If `target` already exists and the contents are not the same,
it asks for user confirmation.
## Options
* `:force` - forces copying without a shell prompt
* `:quiet` - does not log command output
## Examples
iex> assigns = [project_path: "/Users/joe/newproject"]
iex> Mix.Generator.copy_template("source/gitignore", ".gitignore", assigns)
* creating .gitignore
true
"""
@doc since: "1.9.0"
@spec copy_template(Path.t(), Path.t(), keyword, keyword) :: boolean()
def copy_template(source, target, assigns, options \\ []) do
create_file(target, EEx.eval_file(source, assigns: assigns), options)
end
@doc """
Prompts the user to overwrite the file if it exists.
Returns false if the file exists and the user forbade
to override it. Returns true otherwise.
"""
@doc since: "1.9.0"
@spec overwrite?(Path.t()) :: boolean
def overwrite?(path) do
if File.exists?(path) do
full = Path.expand(path)
Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
else
true
end
end
@doc """
Prompts the user to overwrite the file if it exists.
The contents are compared to avoid asking the user to
override if the contents did not change. Returns false
if the file exists and the content is the same or the
user forbade to override it. Returns true otherwise.
"""
@doc since: "1.9.0"
@spec overwrite?(Path.t(), iodata) :: boolean
def overwrite?(path, contents) do
case File.read(path) do
{:ok, binary} ->
if binary == IO.iodata_to_binary(contents) do
false
else
full = Path.expand(path)
Mix.shell().yes?(Path.relative_to_cwd(full) <> " already exists, overwrite?")
end
_ ->
true
end
end
defp log(color, command, message, opts) do
unless opts[:quiet] do
Mix.shell().info([color, "* #{command} ", :reset, message])
end
end
@doc """
Embeds a template given by `contents` into the current module.
It will define a private function with the `name` followed by
`_template` that expects assigns as arguments.
This function must be invoked passing a keyword list.
Each key in the keyword list can be accessed in the
template using the `@` macro.
For more information, check `EEx.SmartEngine`.
## Examples
defmodule Mix.Tasks.MyTask do
require Mix.Generator
Mix.Generator.embed_template(:log, "Log: <%= @log %>")
end
"""
defmacro embed_template(name, contents) do
quote bind_quoted: binding() do
contents =
case contents do
[from_file: file] ->
@file file
File.read!(file)
c when is_binary(c) ->
@file {__ENV__.file, __ENV__.line + 1}
c
_ ->
raise ArgumentError, "expected string or from_file: file"
end
require EEx
source = "<% _ = assigns %>" <> contents
EEx.function_from_string(:defp, :"#{name}_template", source, [:assigns])
end
end
@doc """
Embeds a text given by `contents` into the current module.
It will define a private function with the `name` followed by
`_text` that expects no arguments.
## Examples
defmodule Mix.Tasks.MyTask do
require Mix.Generator
Mix.Generator.embed_text(:error, "There was an error!")
end
"""
defmacro embed_text(name, contents) do
quote bind_quoted: binding() do
contents =
case contents do
[from_file: f] -> File.read!(f)
c when is_binary(c) -> c
_ -> raise ArgumentError, "expected string or from_file: file"
end
defp unquote(:"#{name}_text")(), do: unquote(contents)
end
end
end