CooperConfig
CooperConfig replaces Mix's own config sources (config/runtime.exs
via Config.Provider in a release; config/config.exs/Application. start/2 outside one) with a
CASC
file, loaded via cooper. It is a
thin bridge, not a parser: Cooper.load_file/2 still does all the
actual work (lexing, imports, merging, ${...}/@{...}/%{...}
resolution, secret-wrapping); CooperConfig only converts the result
into the shape Application/Config.Provider expect and plugs it into
either a release's boot sequence (CooperConfig.Provider) or your own
application's startup (CooperConfig.load!/2, see below).
# config.casc
#@version = 1.0
my_app {
port = 4000
*password = ${DB_PASSWORD}
}
# mix.exs
defp releases do
[
my_app: [
config_providers: [
{CooperConfig.Provider, path: "/etc/my_app/config.casc"}
]
]
]
end
At boot, the release reads config.casc, and Application.get_env(:my_app, :port)/Application.get_env(:my_app, :password) return 4000 and the
real (revealed) value of DB_PASSWORD respectively — merged on top of
whatever config/runtime.exs (if any) already set, the same way a
second config.exs import merges on top of the first.
Outside a release: CooperConfig.load!/2
Provider only runs as part of a mix release boot — mix run/iex -S mix/mix test never invoke Config.Provider at all. For those, call
CooperConfig.load!/2 as the first line of your own application's
Application.start/2:
def start(_type, _args) do
CooperConfig.load!()
Supervisor.start_link(children, strategy: :one_for_one, name: MyApp.Supervisor)
end
By that point :cooper has already started as one of cooper_config's
own dependencies, so — unlike Provider, which has to disable it —
this genuinely benefits from Cooper.load_file/2's cache. In a release,
use both together: Provider gets the value in before anything could
miss it, and load!/2 re-reads the same file through Cooper's cache
once it's safe to, overwriting Provider's result (a no-op for an
unchanged file). See CooperConfig.load!/2's own moduledoc for the full
tradeoff.
Installation
Add cooper_config to your list of dependencies in mix.exs. cooper
comes along as its own dependency automatically — no separate line
needed:
def deps do
[
{:cooper_config, "~> 0.1.0"}
]
end
Options
CooperConfig.Provider and CooperConfig.load!/2 both accept most of
what Cooper.load_file/2 does (:env, :resolvers, :tags,
:import_schemes, :dotenv, :dotenv_env, :dotenv_files), plus
:reveal_secrets (default true — see CooperConfig.Convert's
moduledoc for the tradeoff of turning it off). :env is an override
layer, not a replacement — System.get_env/0 (and any .env file) is
always consulted too, for any name not given an explicit entry.
CooperConfig.Provider additionally takes :path (required — a
literal string, {:system, "ENV_VAR"}, or {:system, "ENV_VAR", "/default"}, same as Config.Provider.resolve_config_path!/1), and
never exposes :cache/:watch_env — it always loads with
cache: false, since Config.Provider callbacks run before :cooper's
own OTP application (and Cooper.Cache's process) has started.
CooperConfig.load!/2 takes the CASC file's path as a plain argument
instead (defaulting to config/config.casc), and does expose
:cache/:watch_env, on top of Cooper.load_file/2's own default
(cache: true) — see its moduledoc, or "Outside a release" above, for
why that's safe here but not in Provider.
Where to go next
CooperConfig.Provider— theConfig.Provider, full option list, and error behavior.CooperConfig.load!/2— theApplication.start/2-time loader, cached, for use outside a release (or alongsideProviderinside one).CooperConfig.Convert— the underlying map → app-config conversion, usable on its own.cooperand its CASC reference — everything about the config language and the library that loads it.
Development
mix deps.get
mix precommit
mix precommit runs everything a change needs to pass before review —
formatting, --warnings-as-errors compilation, Credo, Sobelow, the
test suite, and Dialyzer, in that order.
License
MIT — see LICENSE.