An app-config accessor with a per-process override layer for test isolation.
use it once to bind it to your OTP application:
defmodule MyApp.Config do
use Ambient.Config, otp_app: :my_app
endThis generates the domain API – get/2, fetch/1, fetch!/1, put/2,
revert/1, reset/0 – where the reads check a per-process override first
(via Ambient.ProcessOverride), then fall back to
Application.get_env/3 / Application.fetch_env/2.
Underneath, Ambient.Value supplies the generic layer those wrap
(put_override/2, delete_override/1, delete_all/0) plus overridden?/1,
allow/2, set_shared/1 and set_private/0. You can use Ambient.Value
directly to build overridable values of your own.
Nested config
Most real config isn't flat – config :my_app, :oauth, client_id: "…" reads
back as a keyword list, and the call site is Application.get_env(:my_app, :oauth)[:client_id]. Pass a path and both the read and the override target
the leaf:
MyApp.Config.get([:oauth, :client_id], "default")
MyApp.Config.put([:oauth, :client_id], "test-client")Paths step through keyword lists and maps. A missing key anywhere along the
way yields the default, exactly as Application.get_env/3 does for a missing
top-level key.
Overrides resolve longest prefix first: an override on [:oauth, :client_id] wins, then one on :oauth (dug into), then app env. So pinning
a whole group still works, and a group override is visible to leaf reads:
MyApp.Config.put(:oauth, client_id: "a", secret: "b")
MyApp.Config.get([:oauth, :client_id]) #=> "a"It does not work in reverse – overriding a leaf doesn't synthesize a parent,
so get(:oauth) after put([:oauth, :client_id], …) returns the unmodified
app-env group. Override at the level you read at.
Why not Application.put_env/3 in tests?
Application.put_env/3 is global – concurrent async: true tests clobber
each other. put/2 is process-local, inherited by spawned children
($callers + allow/2), and auto-cleaned on exit. Safe under async: true.
Usage
# production / app code
MyApp.Config.get(:feature_x_enabled, false)
# tests
MyApp.Config.put(:feature_x_enabled, true)
MyApp.Config.revert(:feature_x_enabled)
MyApp.Config.reset()
# for a GenServer that reads config in its own process:
MyApp.Config.allow(genserver_pid)Remember to start the override server for the generated table in
test/test_helper.exs:
Ambient.start_servers([MyApp.Config])