joetjen.net
EN DE
Configuration language

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.

Format 1.0

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:

config.casc
#@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.
  • import pulls in another file; its path may read the environment. ${COOPER_ENV} names the current environment, such as dev or prod, in every implementation.
  • @region is a variable, ${…} reads the environment, %{…} refers to another key of the finished tree.
  • 500ms and 512MiB are values of their own types, not strings to parse later.
  • *password is 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

PropertyValue
Extension.casc
EncodingUTF-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
ResultOne tree: blocks are maps, leaves are typed values
GrammarPEG, written in Aether — see Grammar
ImplementationsElixir (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.

NameLanguageRole
cooperElixirThe reference implementation (Hex: cooper 0.7.0)
cooper_configElixirUses a CASC file as the configuration of a Mix project or release (Hex: cooper_config 0.5.1)
node-cooperNode.jsThe Node.js port (npm: @joetjen/cooper 0.2.2)
node-cooper-configNode.jsLoads an application’s configuration once, at startup, and gives the program functions to read it (npm: @joetjen/cooper-config 0.2.1)
node-cooper-fastifyNode.jsFastify plugin: the configuration on fastify.config, the server’s own options from the same document (npm: @joetjen/cooper-fastify 0.2.1)
node-cooper-nestjsNode.jsNestJS module that feeds the configuration to @nestjs/config (npm: @joetjen/cooper-nestjs 0.2.1)
php-cooperPHPThe PHP port (Packagist: joetjen/cooper 0.2.1)
php-cooper-configPHPLoads an application’s configuration once, at startup, and gives it functions to read it (Packagist: joetjen/cooper-config 0.3.1)
php-cooper-laravelPHPLaravel integration: config/config.casc replaces config/*.php (Packagist: joetjen/cooper-laravel 0.2.1)
php-cooper-symfonyPHPSymfony integration: a CASC document replaces config/packages/*.yaml for bundle configuration and container parameters (Packagist: joetjen/cooper-symfony 0.2.1)
cooperPraxisThe Praxis port, generated from the same grammar; part of the Praxis repository, not released on its own yet
cooper_prxPraxisReads a Praxis project’s config.casc into the application environment
cooper_conformancePraxisThe conformance suite: the same documents through every implementation, every disagreement named
CASC for VS CodeTypeScriptHighlighting, 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
ichorElixirThe 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.