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

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.