Packages
igniter
0.6.11
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
lib/igniter/project/config.ex
defmodule Igniter.Project.Config do
@moduledoc "Codemods and utilities for modifying Elixir config files."
require Igniter.Code.Function
alias Igniter.Code.Common
alias Sourceror.Zipper
@type updater :: (Sourceror.Zipper.t() -> {:ok, Sourceror.Zipper.t()}) | :error | nil
@type after_predicate :: (Sourceror.Zipper.t() -> boolean())
@type config_group_item ::
{list(term) | term(), term()} | {list(term) | term(), term(), Keyword.t()}
@doc """
Sets a config value in the given configuration file, if it is not already set.
See `configure/6` for more.
## Options
* `failure_message` - A message to display to the user if the configuration change is unsuccessful.
* `after` - `t:after_predicate/0`. Moves to the last node that matches the predicate.
"""
@spec configure_new(Igniter.t(), Path.t(), atom(), list(atom), term(), opts :: Keyword.t()) ::
Igniter.t()
def configure_new(igniter, file_path, app_name, config_path, value, opts \\ []) do
configure(
igniter,
file_path,
app_name,
config_path,
value,
Keyword.put(opts, :updater, &{:ok, &1})
)
end
@doc """
Configures a "group" of configurations, which is multiple configurations set at one time.
If the app + the shared prefix is already configured, then each configuration is added individually,
and the `comment` for the group is ignored. The sub configurations use `configure`, so if you want to
not change the value if its already set, use `updater: &{:ok, &1}` in the item opts.
## Options
- `comment` - A comment string to add above the group when its added.
"""
@spec configure_group(
Igniter.t(),
Path.t(),
atom(),
shared_prefix :: list(atom),
list(config_group_item()),
opts :: Keyword.t()
) :: Igniter.t()
def configure_group(igniter, file_path, app_name, shared_prefix, items, opts \\ []) do
if Enum.empty?(items) do
raise ArgumentError, "Must provide at least one item in configure_group/6"
end
items =
Enum.map(items, fn
{sub_path, value} ->
{List.wrap(sub_path), value, opts}
{sub_path, value, opts} ->
{List.wrap(sub_path), value, opts}
end)
if configures_key?(igniter, file_path, app_name, shared_prefix) do
Enum.reduce(items, igniter, fn {path, value, opts}, igniter ->
configure(igniter, file_path, app_name, shared_prefix ++ path, value, opts)
end)
else
zipper =
{:__block__, [], []}
|> Zipper.zip()
code_with_configuration_added =
Enum.reduce(items, zipper, fn {path, value, opts}, zipper ->
modify_configuration_code(zipper, shared_prefix ++ path, app_name, value, opts)
end)
|> Zipper.topmost()
|> then(fn zipper ->
if opts[:comment] do
Igniter.Code.Common.add_comment(zipper, opts[:comment])
else
zipper
end
end)
|> Zipper.topmost_root()
config_file_path = config_file_path(igniter, file_path)
file_contents = "import Config\n"
igniter
|> Igniter.include_or_create_file(config_file_path, file_contents)
|> ensure_default_configs_exist(file_path)
|> Igniter.update_elixir_file(config_file_path, fn zipper ->
case find_config(zipper) do
nil ->
{:warning,
bad_config_message(
app_name,
file_path,
shared_prefix,
Sourceror.to_string(zipper),
opts
)}
zipper ->
Igniter.Code.Common.add_code(zipper, code_with_configuration_added)
end
end)
end
end
@spec configure_runtime_env(
Igniter.t(),
atom(),
atom(),
list(atom),
term(),
opts :: Keyword.t()
) ::
Igniter.t()
def configure_runtime_env(igniter, env, app_name, config_path, value, opts \\ []) do
default_runtime =
"""
import Config
if config_env() == #{inspect(env)} do
end
"""
config_file_path = config_file_path(igniter, "runtime.exs")
igniter
|> Igniter.create_or_update_elixir_file(config_file_path, default_runtime, &{:ok, &1})
|> Igniter.update_elixir_file(config_file_path, fn zipper ->
patterns = [
"""
if config_env() == #{inspect(env)} do
__cursor__()
end
""",
"""
if #{inspect(env)} == config_env() do
__cursor__()
end
"""
]
modify_to =
case value do
{:code, value} -> value
value -> Sourceror.parse_string!(Sourceror.to_string(Macro.escape(value)))
end
zipper
|> Igniter.Code.Common.move_to_cursor_match_in_scope(patterns)
|> case do
{:ok, zipper} ->
modify_configuration_code(
zipper,
config_path,
app_name,
modify_to,
updater:
opts[:updater] || fn zipper -> {:ok, Common.replace_code(zipper, modify_to)} end
)
:error ->
zipper
|> Igniter.Code.Common.add_code("""
if config_env() == #{inspect(env)} do
end
""")
|> Igniter.Code.Common.move_to_cursor_match_in_scope(patterns)
|> case do
{:ok, zipper} ->
modify_configuration_code(
zipper,
config_path,
app_name,
modify_to,
updater:
opts[:updater] || fn zipper -> {:ok, Common.replace_code(zipper, modify_to)} end
)
_ ->
value =
case value do
{:code, value} -> Sourceror.to_string(value)
value -> inspect(value)
end
{:warning,
"""
Could not set #{inspect([app_name | config_path])} in `#{inspect(env)}` of `#{config_file_path}`.
```elixir
# in `runtime.exs`
if config_env() == #{inspect(env)} do
# Please configure #{inspect([app_name | config_path])} it to the following value
#{value}
end
```
"""}
end
end
end)
end
@doc """
Sets a config value in the given configuration file, updating it with `updater` if it is already set.
If the value is source code, pass `{:code, value}`, otherwise pass just the value.
To produce this source code, we suggest using `Sourceror.parse_string!`. For example:
```elixir
|> Igniter.Project.Config.configure(
"fake.exs",
:tailwind,
[:default, :args],
{:code,
Sourceror.parse_string!(\"\"\"
~w(--config=tailwind.config.js --input=css/app.css --output=../output/assets/app.css)
\"\"\")}
)
```
## Options
* `failure_message` - A message to display to the user if the configuration change is unsuccessful.
* `updater` - `t:updater/0`. A function that takes a zipper at a currently configured value and returns a new zipper with the value updated.
* `after` - `t:after_predicate/0`. Moves to the last node that matches the predicate. Useful to guarantee a `config` is placed after a specific node.
"""
@spec configure(
Igniter.t(),
Path.t(),
atom(),
list(atom),
term(),
opts :: Keyword.t()
) :: Igniter.t()
def configure(igniter, file_name, app_name, config_path, value, opts \\ []) do
file_contents = "import Config\n"
file_path = config_file_path(igniter, file_name)
config_path = List.wrap(config_path)
value =
case value do
{:code, value} -> value
value -> Sourceror.parse_string!(Sourceror.to_string(Macro.escape(value)))
end
updater = opts[:updater] || fn zipper -> {:ok, Common.replace_code(zipper, value)} end
igniter
|> ensure_default_configs_exist(file_name)
|> Igniter.include_or_create_file(file_path, file_contents)
|> Igniter.update_elixir_file(file_path, fn zipper ->
case find_config(zipper) do
nil ->
{:warning, bad_config_message(app_name, file_path, config_path, value, opts)}
_ ->
modify_configuration_code(zipper, config_path, app_name, value,
updater: updater,
after: opts[:after]
)
end
end)
end
@doc """
Removes an applications config completely.
"""
@spec remove_application_configuration(Igniter.t(), Path.t(), atom()) :: Igniter.t()
def remove_application_configuration(igniter, file_name, app_name) do
file_path = config_file_path(igniter, file_name)
Igniter.update_elixir_file(
igniter,
file_path,
fn zipper ->
case find_config(zipper) do
nil -> igniter
_ -> recursively_remove_configurations(zipper, app_name)
end
end,
required?: false
)
end
defp recursively_remove_configurations(zipper, app_name) do
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
[2, 3],
&Igniter.Code.Function.argument_equals?(&1, 0, app_name)
) do
:error ->
zipper
{:ok, zipper} ->
zipper
|> Zipper.remove()
|> Zipper.top()
|> recursively_remove_configurations(app_name)
end
end
defp config_file_path(igniter, file_name) do
case igniter |> Igniter.Project.Application.config_path() |> Path.split() do
[path] -> [path]
path -> Enum.drop(path, -1)
end
|> Path.join()
|> Path.join(file_name)
end
defp ensure_default_configs_exist(igniter, file_name) do
if file_name in ["dev.exs", "test.exs", "prod.exs"] do
igniter
|> Igniter.include_or_create_file(config_file_path(igniter, "config.exs"), """
import Config
""")
|> ensure_config_evaluates_env()
|> Igniter.include_or_create_file(config_file_path(igniter, "dev.exs"), """
import Config
""")
|> Igniter.include_or_create_file(config_file_path(igniter, "test.exs"), """
import Config
""")
|> Igniter.include_or_create_file(config_file_path(igniter, "prod.exs"), """
import Config
""")
else
igniter
end
end
defp ensure_config_evaluates_env(igniter) do
Igniter.update_elixir_file(igniter, config_file_path(igniter, "config.exs"), fn zipper ->
case Igniter.Code.Function.move_to_function_call_in_current_scope(zipper, :import_config, 1) do
{:ok, _} ->
{:ok, zipper}
_ ->
{:ok,
Igniter.Code.Common.add_code(zipper, """
import_config "\#{config_env()}.exs"
""")}
end
end)
end
defp bad_config_message(app_name, file_path, config_path, value, opts) do
path =
config_path
|> keywordify(value)
code =
quote do
config unquote(app_name), unquote(path)
end
message =
if opts[:failure_message] do
"""
#{opts[:failure_message]}
"""
else
""
end
or_update =
if opts[:updater] do
" or update"
else
""
end
"""
Please set#{or_update} the following config in #{file_path}:
#{Macro.to_string(code)}#{String.trim_trailing(message)}
"""
end
defp keywordify([], value), do: value
defp keywordify([key | rest], value), do: [{key, keywordify(rest, value)}]
@doc """
Modifies elixir configuration code starting at the configured zipper.
If you want to set configuration, use `configure/6` or `configure_new/5` instead. This is a lower-level
tool for modifying configuration files when you need to adjust some specific part of them.
## Options
* `updater` - `t:updater/0`. A function that takes a zipper at a currently configured value and returns a new zipper with the value updated.
* `after` - `t:after_predicate/0`. Moves to the last node that matches the predicate.
"""
@spec modify_configuration_code(
Zipper.t(),
list(atom),
atom(),
term(),
opts :: Keyword.t()
) :: Zipper.t()
def modify_configuration_code(zipper, config_path, app_name, value, opts \\ [])
def modify_configuration_code(zipper, config_path, app_name, value, updater)
when is_function(updater) do
IO.warn("updater argument is deprecated, please use opts updater: fun instead")
modify_configuration_code(zipper, config_path, app_name, value, updater: updater)
end
def modify_configuration_code(zipper, config_path, app_name, value, opts) do
updater = opts[:updater] || fn zipper -> {:ok, Common.replace_code(zipper, value)} end
Igniter.Code.Common.within(zipper, fn zipper ->
zipper = move_to_after(zipper, opts[:after])
case try_update_three_arg(zipper, config_path, app_name, value, updater) do
{:ok, zipper} ->
{:ok, zipper}
:error ->
case try_update_two_arg(zipper, config_path, app_name, value, updater) do
{:ok, zipper} ->
{:ok, zipper}
:error ->
[first | rest] = config_path
config =
if simple_atom(first) do
{:config, [],
[app_name, [{first, Igniter.Code.Keyword.keywordify(rest, value)}]]}
else
{:config, [], [app_name, first, Igniter.Code.Keyword.keywordify(rest, value)]}
end
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:import,
1,
fn function_call ->
Igniter.Code.Function.argument_matches_predicate?(
function_call,
0,
&Common.nodes_equal?(&1, Config)
)
end
) do
{:ok, zipper} ->
zipper
|> Zipper.right()
|> case do
nil ->
{:ok, Common.add_code(zipper, config)}
zipper ->
{:ok, Common.add_code(zipper, config, placement: :before)}
end
:error ->
zipper
|> Zipper.right()
|> case do
nil ->
{:ok, Common.add_code(zipper, config)}
zipper ->
{:ok, Common.add_code(zipper, config, placement: :before)}
end
end
end
end
end)
|> case do
{:ok, zipper} -> zipper
:error -> zipper
end
end
defp move_to_after(zipper, pred) when is_function(pred, 1) do
case Common.move_to_last(zipper, pred) do
{:ok, zipper} -> zipper
:error -> zipper
end
end
defp move_to_after(zipper, _pred), do: zipper
@doc """
Checks if either `config :root_key, _` or `config :root_key, _, _` is present
in the provided config file.
Note: The config file name should _not_ include the `config/` prefix.
"""
@spec configures_root_key?(Igniter.t(), String.t(), atom()) :: boolean()
def configures_root_key?(igniter, config_file_name, root_key) do
case load_config_zipper(igniter, config_file_name) do
nil -> false
zipper -> configures_root_key?(zipper, root_key)
end
end
@doc """
Same as `configures_root_key?/3` but accepts a Zipper instead.
"""
@spec configures_root_key?(Zipper.t(), atom()) :: boolean()
def configures_root_key?(%Zipper{} = zipper, root_key) do
with :error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
2,
&Igniter.Code.Function.argument_equals?(&1, 0, root_key)
),
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
3,
&Igniter.Code.Function.argument_equals?(&1, 0, root_key)
) do
false
else
{:ok, _zipper} -> true
end
end
@doc """
If the last argument is key, checks if either `config :root_key, :key, ...`
or `config :root_key, key: ...` is set.
If the last argument is a keyword path, checks if
`config :root_key, path_head, [...]` defines `path_rest` or if
`config :root_key, [...]` defines `path`, where `path_head` is the first
element of `path` and `path_rest` are the remaining elements.
Note: `config_file_name` should _not_ include the `config/` prefix.
"""
@spec configures_key?(
Igniter.t(),
String.t(),
atom(),
atom() | list(atom())
) :: boolean()
def configures_key?(igniter, config_file_name, root_key, key_or_path) do
case load_config_zipper(igniter, config_file_name) do
nil -> false
zipper -> configures_key?(zipper, root_key, key_or_path)
end
end
@doc """
Same as `configures_key?/4` but accepts a Zipper.
"""
@spec configures_key?(Zipper.t(), atom(), atom() | list(atom())) :: boolean()
def configures_key?(zipper, root_key, key_or_path)
def configures_key?(zipper = %Zipper{}, root_key, key) when is_atom(key) do
configures_key?(zipper, root_key, List.wrap(key))
end
def configures_key?(zipper = %Zipper{}, root_key, path) when is_list(path) do
with :error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
2,
fn function_call ->
Igniter.Code.Function.argument_equals?(function_call, 0, root_key) and
Igniter.Code.Function.argument_matches_predicate?(
function_call,
1,
fn argument_zipper ->
Igniter.Code.Keyword.keyword_has_path?(argument_zipper, path)
end
)
end
),
:error <-
Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
3,
fn function_call ->
case path do
[key] ->
Igniter.Code.Function.argument_equals?(function_call, 0, root_key) and
Igniter.Code.Function.argument_equals?(function_call, 1, key)
[path_head | path_rest] ->
Igniter.Code.Function.argument_equals?(function_call, 0, root_key) and
Igniter.Code.Function.argument_equals?(function_call, 1, path_head) and
Igniter.Code.Function.argument_matches_predicate?(
function_call,
2,
fn argument_zipper ->
Igniter.Code.Keyword.keyword_has_path?(argument_zipper, path_rest)
end
)
end
end
) do
false
else
{:ok, _zipper} -> true
end
end
@doc "Returns `true` if the given configuration path is set somewhere after the provided zipper, or in the given configuration file."
@deprecated "Use configures_root_key?/3 or configures_key?/4 instead."
@spec configures?(Igniter.t(), String.t(), list(atom), atom()) :: boolean()
def configures?(igniter, config_file_name, path, app_name) do
case load_config_zipper(igniter, config_file_name) do
nil -> false
zipper -> configures?(zipper, path, app_name)
end
end
@spec configures?(Zipper.t(), list(atom), atom()) :: boolean()
@deprecated "Use configures_root_key?/2 or configures_key?/3 instead."
def configures?(zipper, path, app_name) do
case path do
[] ->
configures_root_key?(zipper, app_name)
path ->
configures_key?(zipper, app_name, path)
end
end
defp load_config_zipper(igniter, config_file_name) do
config_file_path = config_file_path(igniter, config_file_name)
igniter =
Igniter.include_existing_file(igniter, config_file_path, required?: false)
case Rewrite.source(igniter.rewrite, config_file_path) do
{:ok, source} ->
source
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
_ ->
nil
end
end
defp try_update_three_arg(zipper, config_path, app_name, value, updater) do
if Enum.count(config_path) == 1 and simple_atom(Enum.at(config_path, 0)) do
config_item = Enum.at(config_path, 0)
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
3,
fn function_call ->
Igniter.Code.Function.argument_equals?(function_call, 0, app_name) &&
Igniter.Code.Function.argument_equals?(function_call, 1, config_item)
end
) do
:error ->
:error
{:ok, zipper} ->
Igniter.Code.Function.update_nth_argument(zipper, 2, updater)
end
else
[config_item | path] = config_path
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
3,
fn function_call ->
Igniter.Code.Function.argument_equals?(function_call, 0, app_name) &&
(Igniter.Code.Function.argument_equals?(function_call, 1, config_item) ||
Igniter.Code.Function.argument_matches_predicate?(
function_call,
1,
&Common.nodes_equal?(&1, config_item)
))
end
) do
:error ->
:error
{:ok, zipper} ->
if path == [] do
case Igniter.Code.Function.move_to_nth_argument(zipper, 2) do
{:ok, zipper} ->
if updater do
updater.(zipper)
else
{:ok, Igniter.Code.Common.replace_code(zipper, value)}
end
_ ->
:error
end
else
with {:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 2),
{:ok, zipper} <-
Igniter.Code.Keyword.put_in_keyword(zipper, path, value, updater) do
{:ok, zipper}
else
_ ->
:error
end
end
end
end
end
defp try_update_two_arg(zipper, config_path, app_name, value, updater) do
if simple_atom(Enum.at(config_path, 0)) do
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
:config,
2,
fn function_call ->
Igniter.Code.Function.argument_equals?(function_call, 0, app_name)
end
) do
:error ->
:error
{:ok, zipper} ->
Igniter.Code.Function.update_nth_argument(
zipper,
1,
&Igniter.Code.Keyword.put_in_keyword(&1, config_path, value, updater)
)
end
else
:error
end
end
defp simple_atom(value) do
is_atom(value) and Regex.match?(~r/^[a-z_][a-zA-Z0-9_?!]*$/, to_string(value))
end
defp find_config(zipper) do
Zipper.find(zipper, fn
{:import, _, [Config]} ->
true
{:import, _, [{:__aliases__, _, [:Config]}]} ->
true
_ ->
false
end)
end
end