Ambient.Config (Ambient v0.2.0)

Copy Markdown View Source

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
end

This 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])

Summary

Types

A config key: an atom for a top-level entry, or a path into a nested one.

Types

key()

@type key() :: atom() | [atom(), ...]

A config key: an atom for a top-level entry, or a path into a nested one.

A single-element path is the same key as the bare atom, so [:port] and :port are interchangeable everywhere.