Current section
Files
Jump to
Current section
Files
lib/mix/tasks/sims.gen.config_module.ex
defmodule Mix.Tasks.Sims.Gen.ConfigModule do
use Igniter.Mix.Task
alias Mix.Sims.CodeGeneration
alias Mix.Sims.SwappableConfig
@example "mix sims.gen.config_module"
@shortdoc "Generate a swappable configuation module"
@moduledoc """
#{@shortdoc}
A centeralized configuration module is helpful so configuration can be swapped
during async tests via mox.
## Example
```sh
#{@example}
```
## Generated Modules
This task generates three modules and updates your test helper:
### Configuration Module
The main configuration module (e.g., `YourApp.Config`) acts as the central
configuration interface for your application. It delegates to the configured
adapter, which can be swapped at runtime (particularly useful for testing).
The adapter is retrieved from application config with a key `:config_adapter`,
falling back to the default adapter in production.
### Adapter Behaviour
The behaviour module (e.g., `YourApp.Config.Adapter`) defines the interface that
all adapter implementations must follow. You can add callbacks to this module to
define the functions your configuration needs to support.
### Default Adapter
The default adapter module (e.g., `YourApp.Config.DefaultAdapter`) implements the
adapter behaviour and is used in production. This is where you'll add your actual
configuration logic.
### Test Mock
A Mox mock (e.g., `YourApp.Config.MockAdapter`) is defined in `test/test_helper.exs`
and automatically configured as the adapter for tests. This allows you to mock
configuration behavior in your tests using Mox's `expect/3` and `stub/3` functions.
## Options
* `--config-namespace` - The module name for the configuration namespace.
If not provided, defaults to `YourApp.Config`.
* `--config-behaviour` - The module name for the adapter behaviour that defines
the interface that adapters must implement. If not provided, a default name
will be generated based on the config namespace.
* `--config-default-adapter` - The module name for the default adapter implementation.
If not provided, a default name will be generated based on the config namespace.
* `--config-test-adapter` - The module name for the test adapter mock.
If not provided, a default name will be generated based on the config namespace.
* `--update-test-helper` - Whether to automatically update `test/test_helper.exs`
to add the Mox mock definition. Defaults to `true`. Set to `false` with
`--no-update-test-helper` to skip this step.
## Setting Default Values in config/config.exs
You can configure default values for the options in your `config/config.exs` file
to avoid having to pass them on the command line every time. These defaults will
be used when running any `sims.gen.*` tasks.
Add a configuration block for your application under the `:sims` key:
```elixir
# config/config.exs
import Config
config :your_app, :sims,
config_namespace: "YourApp.Config",
config_behaviour: "YourApp.Config.Adapter",
config_default_adapter: "YourApp.Config.DefaultAdapter",
config_test_adapter: "YourApp.Config.MockAdapter",
update_test_helper: true
```
Command-line options will override these configured defaults if provided.
"""
@default_options [update_test_helper: true]
@template_namespace "sims.gen.config_module"
@impl Igniter.Mix.Task
def info(_argv, _composing_task) do
%Igniter.Mix.Task.Info{
# Groups allow for overlapping arguments for tasks by the same author
# See the generators guide for more.
group: :sims,
# *other* dependencies to add
# i.e `{:foo, "~> 2.0"}`
adds_deps: [],
# *other* dependencies to add and call their associated installers, if they exist
# i.e `{:foo, "~> 2.0"}`
installs: [],
# An example invocation
example: @example,
# a list of positional arguments, i.e `[:file]`
positional: [],
# Other tasks your task composes using `Igniter.compose_task`, passing in the CLI argv
# This ensures your option schema includes options from nested tasks
composes: [],
# `OptionParser` schema
schema: [
config_namespace: :string,
config_test_adapter: :string,
config_behaviour: :string,
config_default_adapter: :string,
update_test_helper: :boolean
],
# Don't set these defaults here so they can be overridden by project config. Use @default_options instead.
defaults: [],
# CLI aliases
aliases: [],
# A list of options in the schema that are required
required: []
}
end
@impl Igniter.Mix.Task
def igniter(igniter) do
default_options_from_project_config =
igniter
|> Igniter.Project.Application.app_name()
|> Application.get_env(:sims, [])
# Define default options here so they can be overridden by project config
options_with_project_defaults =
@default_options
|> Keyword.merge(default_options_from_project_config)
|> Keyword.merge(igniter.args.options)
config_module =
if custom_config = options_with_project_defaults[:config_namespace] do
CodeGeneration.parse_module_name(custom_config)
else
Igniter.Project.Module.module_name(igniter, "Config")
end
swappable_config =
SwappableConfig.new(
Igniter.Project.Application.app_name(igniter),
config_module,
test_config_adapter: options_with_project_defaults[:config_test_adapter],
behaviour: options_with_project_defaults[:config_behaviour],
default_adapter: options_with_project_defaults[:config_default_adapter]
)
igniter
|> Igniter.assign(:swappable_config, swappable_config)
|> Igniter.Project.Deps.add_dep({:mox, "~> 1.0", only: :test})
|> copy_template("config.ex.eex", swappable_config.namespace)
|> copy_template("config/adapter.ex.eex", swappable_config.behaviour)
|> copy_template("config/default_adapter.ex.eex", swappable_config.default_adapter)
|> then(fn igniter ->
if options_with_project_defaults[:update_test_helper] do
update_test_helper(igniter, swappable_config)
else
igniter
end
end)
end
defp update_test_helper(igniter, swappable_config) do
Igniter.update_elixir_file(igniter, "test/test_helper.exs", fn zipper ->
case Igniter.Code.Function.move_to_function_call_in_current_scope(
zipper,
{Mox, :defmock},
2,
&Igniter.Code.Function.argument_equals?(&1, 0, swappable_config.test_adapter)
) do
{:ok, _} ->
{:ok, zipper}
_ ->
{:ok,
Igniter.Code.Common.add_code(
zipper,
EEx.eval_file(
CodeGeneration.find_template_path(@template_namespace, "test_helper.exs.eex"),
assigns: [swappable_config: swappable_config]
)
)}
end
end)
end
defp copy_template(
igniter,
template_path,
module
)
when is_atom(module) do
case Igniter.Project.Module.module_exists(igniter, module) do
{true, igniter} ->
igniter
{false, igniter} ->
Igniter.copy_template(
igniter,
CodeGeneration.find_template_path(@template_namespace, template_path),
Igniter.Project.Module.proper_location(igniter, module),
module: module,
swappable_config: igniter.assigns.swappable_config
)
end
end
end