Build your own overridable value on top of Ambient.ProcessOverride.
An ambient value is one resolved implicitly from the surrounding context
rather than threaded through arguments. Ambient.Config, Ambient.Clock,
Ambient.Random and Ambient.Env are all built on this module – nothing
about them is privileged. If your app has an ambient value of its own (the
current tenant, the acting user, a request id), this is the supported way to
make it as testable as the built-in ones.
defmodule MyApp.Tenant do
use Ambient.Value, table: :my_app_tenant_overrides
@doc "The tenant for the current process, or the default."
def current, do: get_or(:tenant, MyApp.Tenant.Default)
@doc "Pin the tenant for this test and everything it spawns."
def put(tenant), do: put_override(:tenant, tenant)
endRegister it once in test/test_helper.exs, exactly like a built-in:
Ambient.start_servers([Ambient.Clock, MyApp.Tenant])What you get
use Ambient.Value, table: :some_table defines:
get_or/2(imported macro) – the read. Returns the override if one is in scope, otherwise evaluates the fallback expression.@ambient_enabled– whether this build compiled the machinery in, for values that need to drop a branch of their own.put_override/2,delete_override/1,delete_all/0– the writers.overridden?/1– whether an override is in scope for a key.allow/2– grant a process outside the$callerschain access.set_shared/1,set_private/0– shared mode forasync: falsetests.__ambient_table__/0– soAmbient.start_servers/1accepts the module.
All of them are defoverridable. allow/2 and set_shared/1 take a
defaulted second/first argument, so allow/1 and set_shared/0 are exported
too.
A module holding a single value conventionally uses one sentinel key
(:clock, :tenant); one holding many (like Ambient.Config) keys by name.
get_or/2 is a macro, on purpose
It expands at compile time, so in a build that didn't opt into overrides
(see Ambient.ProcessOverride) the whole lookup disappears and only the
fallback expression remains:
def current, do: get_or(:tenant, MyApp.Tenant.Default)
# in a production build, compiles to exactly:
def current, do: MyApp.Tenant.DefaultThat is what keeps a wrapper free to use everywhere in production code. The
fallback is only evaluated when there is no override, so
get_or(:key, expensive_call()) doesn't pay for the call it doesn't need.
Summary
Functions
Read the override for key, falling back to fallback when there is none.
Functions
Read the override for key, falling back to fallback when there is none.
Imported by use Ambient.Value, and resolved against that module's table.
A macro rather than a function: in a build without overrides compiled in it
expands to fallback alone, so the wrapper costs nothing in production.
def utc_now, do: get_or(:clock, DateTime.utc_now())