The CASC configuration format
CASC is a configuration language with a real specification: nested blocks, typed values from durations to IP ranges, variables, environment reads, references to other keys, imports and loops — in a syntax that stays readable as plain text.
A first look
A CASC file is a list of statements that together build one tree of named values. This one reads an environment file, a few environment variables and a shared variable, and refers from one key to two others:
#@version = 1.0
import "env/${COOPER_ENV}.casc"
@region = ${REGION:"eu-west"}
server {
host = "0.0.0.0"
port = !int(${PORT:8080})
}
database {
host = "db.@{region}.internal"
*password = ${DB_PASSWORD:?"DB_PASSWORD is required"}
timeout = 500ms
cache = 512MiB
}
health.url = "http://%{server.host}:%{server.port}/health"#@version = 1.0— every file starts with its format version.importpulls in another file; its path may read the environment.${COOPER_ENV}names the current environment, such asdevorprod, in every implementation.@regionis a variable,${…}reads the environment,%{…}refers to another key of the finished tree.500msand512MiBare values of their own types, not strings to parse later.*passwordis marked secret: the value stays redacted wherever it ends up.:?"…"makes a missing environment variable a load error with your own message.
Design
- One kind of map. A block and a dotted path are two spellings of the same tree, and blocks written twice merge.
- Typed literals. Durations, byte sizes, dates and times, IP addresses and tuples are values in the language.
- Three references, one grammar.
@{}variables,${}environment and%{}config paths share the same defaults, guards, indexes and filters. - Explicit merging. Later writes win and blocks deep-merge; the sigils
~,+and-replace, append and remove when that is what you mean. - Nothing unknown passes silently. A tag, resolver or import scheme nobody registered is a load error that names it.
- Secrets in the syntax. A
*on a key marks its value secret, and interpolating it keeps the result redacted. - One meaning everywhere. A document comes to the same configuration in every implementation, and a refused one fails with the same error category.
The name is a pun: a cooper makes barrels, and a casc is a cask. The format is what the barrel holds.
File facts
| Property | Value |
|---|---|
| Extension | .casc |
| Encoding | UTF-8 |
| Header | #@version = 1.0 — mandatory, in every file; only comments and blank lines may come before it |
| Comments | # followed by a space, to the end of the line |
| Result | One tree: blocks are maps, leaves are typed values |
| Grammar | PEG, written in Aether — see Grammar |
| Implementations | Elixir (the reference), Node.js, PHP, Praxis |
Cooper’s README, like those of the other implementations, opens with a warning: it is under heavy development and not for production use before 1.0.0. Until then the libraries and the specification can change between any two releases, which is why CASC is marked as in development here. The format version stays 1.0.
Implementations and tools
These pages describe the format. Four implementations read it — in Elixir, Node.js, PHP and Praxis — and each comes with libraries that hand the result to an application or framework. Each has its own documentation for how it loads or exposes the format.
| Name | Language | Role |
|---|---|---|
| cooper | Elixir | The reference implementation (Hex: cooper 0.7.0) |
| cooper_config | Elixir | Uses a CASC file as the configuration of a Mix project or release (Hex: cooper_config 0.5.1) |
| node-cooper | Node.js | The Node.js port (npm: @joetjen/cooper 0.2.2) |
| node-cooper-config | Node.js | Loads an application’s configuration once, at startup, and gives the program functions to read it (npm: @joetjen/cooper-config 0.2.1) |
| node-cooper-fastify | Node.js | Fastify plugin: the configuration on fastify.config, the server’s own options from the same document (npm: @joetjen/cooper-fastify 0.2.1) |
| node-cooper-nestjs | Node.js | NestJS module that feeds the configuration to @nestjs/config (npm: @joetjen/cooper-nestjs 0.2.1) |
| php-cooper | PHP | The PHP port (Packagist: joetjen/cooper 0.2.1) |
| php-cooper-config | PHP | Loads an application’s configuration once, at startup, and gives it functions to read it (Packagist: joetjen/cooper-config 0.3.1) |
| php-cooper-laravel | PHP | Laravel integration: config/config.casc replaces config/*.php (Packagist: joetjen/cooper-laravel 0.2.1) |
| php-cooper-symfony | PHP | Symfony integration: a CASC document replaces config/packages/*.yaml for bundle configuration and container parameters (Packagist: joetjen/cooper-symfony 0.2.1) |
| cooper | Praxis | The Praxis port, generated from the same grammar; part of the Praxis repository, not released on its own yet |
| cooper_prx | Praxis | Reads a Praxis project’s config.casc into the application environment |
| cooper_conformance | Praxis | The conformance suite: the same documents through every implementation, every disagreement named |
| CASC for VS Code | TypeScript | Highlighting, snippets and a language server — definitions, hover, completion, outline and diagnostics across imports. Version 0.2.0, not yet published on the VS Code Marketplace or Open VSX |
| ichor | Elixir | The grammar compiler the Elixir and Praxis parsers are generated with |
The Node.js and PHP parsers are written by hand, porting the grammar rule for rule. The conformance suite runs one corpus of documents through all four implementations and compares every result exactly, and every refusal by its error category.
On these pages
- File structure — header, comments, keys, blocks, merging.
- Values — every literal type.
- References — variables, environment, config paths, filters, tags.
- Imports and loops — splitting and generating configuration, with a complete example.
- Grammar — the formal grammar, the error categories, and where the implementations still differ from the specification.