Cooper
Copy MarkdownCooper loads CASC config files — a hierarchical, extensible config language with imports, variables, string interpolation, environment/config references, loops, and consumer-registered resolvers and tags — into native Elixir terms.
# config.casc
#@version = 1.0
@region = "eu-west"
server {
host = "0.0.0.0"
port = 8080
}
database {
*password = ${DB_PASSWORD}
host = "db.@{region}.internal"
pool_size = 10
timeout = 500ms
}iex> Cooper.load_file("config.casc", env: %{"DB_PASSWORD" => "hunter2"})
{:ok,
%{
"server" => %{"host" => "0.0.0.0", "port" => 8080},
"database" => %{
"password" => [~~REDACTED~~],
"host" => "db.eu-west.internal",
"pool_size" => 10,
"timeout" => {:duration, 500_000_000}
}
}}Maps come back with string keys, durations/byte sizes as tagged
{:duration, nanoseconds}/{:bytes, count} tuples, tuples as real
Elixir tuples (never coerced to lists, CASC.md §6.11), and any
*key-prefixed value wrapped in Cooper.Secret — redacted by
inspect/1 and to_string/1 by default; call
Cooper.Secret.reveal/1 to get the real value back.
Why
Most config libraries either parse a fixed format (JSON/YAML/TOML) or
hand you a bespoke DSL with no formal grammar behind it. CASC has a
real specification (guides/casc/CASC.md), and
Cooper implements it via Ichor, a sibling grammar-compiler library,
rather than hand-rolled string parsing — so the interpreter's behavior
tracks the grammar, not the other way around.
How it fits together
Loading a file runs one pipeline, each stage handing off to the next:
- Lex + parse —
Cooper.GrammarandCooper.Actionsturn CASC source into a flat list of assignments and variable declarations, with every literal value (numbers, durations, dates, strings, ...) already converted to its native Elixir form. - Loop expansion —
Cooper.Loopexpands eachforstatement (CASC.md §5.5) into the ops it generates. - Import resolution —
Cooper.Loaderrecursively loads eachimportstatement (CASC.md §5.1), splicing the imported file's own ops into the importer's list and propagating its public variables. - Merge and secret-wrapping —
Cooper.Mergefolds the flattened ops into a tree, honoring the merge-control sigils (~/+/-, CASC.md §5.7/§8), and wraps every*key-prefixed value inCooper.Secretso it can't leak through an accidentalinspect/log call. - Reference resolution —
Cooper.Resolverresolves everything left unresolved:@{}variables,${}environment reads,%{}config references,!{resolver:...}dispatch, and!Name(...)tagged values (CASC.md §7) — including re-wrapping a secret that gets copied elsewhere by one of those references, so the redaction travels with the value, not just its original declared path.
See the tutorial for a full walkthrough of both the library and the CASC syntax it parses, or the cheatsheet for a terse API reference.
Installation
Add cooper to your list of dependencies in mix.exs. ichor comes
along as its own dependency automatically — no separate line needed:
def deps do
[
{:cooper, "~> 0.1.0"}
]
endWhere to go next
- Tutorial — loading config, secrets, error handling, test-time env/import injection, and enough CASC syntax to follow along.
- Examples — layered per-environment config, a real secrets manager, IP allowlisting, generating per-shard config from a template, testing config-loading code without touching disk.
- Cheatsheet — quick reference for
Cooper's public API. - CASC tutorial and CASC reference — everything about the language itself, independent of the surrounding library.
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. See
CONTRIBUTION.md for how to propose changes, and
CHANGELOG.md for release history.
License
MIT — see LICENSE.