References
Three kinds of reference bring values in from elsewhere — variables, the environment and other keys — and they share one syntax for defaults, guards, indexes and filters. Tags and resolvers convert and fetch.
Three kinds
| Form | Reads | Resolved |
|---|---|---|
@{name} | a variable declared with @name = … | against all variables of the load |
${NAME} | an environment variable | while loading |
%{a.b.c} | another key of the configuration | against the final, merged tree |
A reference that is the whole value keeps the type of what it reads — %{server.port} is an integer. Inside a double-quoted string it is interpolated as text.
Defaults, guards and indexes
All three kinds take the same suffixes. Without one, a reference to something undefined is a load error.
| Suffix | Meaning |
|---|---|
@{x:default} | use default if undefined |
@{x:+alt} | alt if defined, otherwise "" |
@{x:?"message"} | fail the load with this message if undefined |
@{x[1]} | index into a list (from 0), combinable with a default |
@{x | trim} | apply filters, left to right, after the suffix |
There is deliberately no shell-style :-default. A default is parsed as a CASC value, so ${PORT:8080} falls back to the integer 8080.
Variables
@name = value declares a public variable, visible to every file of the load — the files that import this one and the files it imports. @*name declares a private one, visible only in its own file; it shadows a public variable of the same name there.
@hosts = ["x", "y"]
@*suffix = ".internal"
primary = "@{hosts[0]}@{suffix}"
second = @{hosts[1]}
all = @{hosts}
mode = @{mode:"standalone"}
note = @{suffix:+"private suffix set"}%{"primary" => "x.internal", "second" => "y", "all" => ["x", "y"],
"mode" => "standalone", "note" => "private suffix set"}Variables are not in the result. They resolve against the whole load, not in reading order: a variable may be used above its declaration, and if it is declared twice the last declaration wins.
Environment
An unset or empty environment variable counts as undefined. What the environment supplies is always a string; a tag converts it. A reference ending in [] splits the value on , and ; into a list.
region = ${REGION}
retries = !int(${MAX_RETRIES:3})
hosts = ${HOSTS[]}
second = ${HOSTS[1]}
allowed = ${ALLOWED_HOSTS[]:["localhost"]}%{"region" => "eu-west", "retries" => 3, "hosts" => ["a", "b", "c"],
"second" => "b", "allowed" => ["localhost"]}Conditional statements
${?NAME} on a line of its own keeps the next statement only if NAME is set and not empty — exactly one statement, which may be a whole block.
${?ENABLE_METRICS}
metrics {
port = 9568
path = "/metrics"
}Config references
%{path} reads another key. It resolves last, against the finished tree — after every import, override and sigil — so it sees the value the key finally has, wherever in the files that was set. A cycle of references is a load error.
health.url = "http://%{server.host}:%{server.port}/health"
server.host = "api.internal"
server.port = 8080
admin = %{contact.admin:"ops@example.com"}%{"health" => %{"url" => "http://api.internal:8080/health"},
"server" => %{"host" => "api.internal", "port" => 8080},
"admin" => "ops@example.com"}Filters
Filters clean up a string on its way in. The set is closed: trim, downcase, upcase, trim_prefix: "…" and trim_suffix: "…". Anything else is a load error, as is a filter applied to a non-string. Single-quoted arguments make filters usable inside a double-quoted string.
scheme = ${SCHEME | trim_suffix: "://" | downcase}
region = ${REGION:" us-east-1 " | trim}
endpoint = "${SCHEME | trim_suffix: '://' | downcase}://api.internal"%{"scheme" => "https", "region" => "us-east-1",
"endpoint" => "https://api.internal"}Built names
The name inside a reference may itself be a double-quoted string with interpolation. That is how a loop reads one environment variable per element. There is deliberately no concatenation operator besides this.
@which = "HOST"
host = ${"APP_@{which}"} # reads APP_HOST
@ids = ["1", "2"]
for @id in @{ids} as tokens {
"@{id}" = ${"TOKEN_@{id}"} # reads TOKEN_1, TOKEN_2
}Tags and resolvers
A tag !name(value) converts its one argument; tags nest. Built in are !int, !float, !bool, !duration, !bytes, !trim, !downcase, !upcase and !module. A failed conversion is a load error.
port = !int(${PORT:"8080"})
debug = !bool(${DEBUG:"false"})
ttl = !duration("5m")
cache = !bytes("512MiB")%{"port" => 8080, "debug" => false,
"ttl" => {:duration, 300000000000}, "cache" => {:bytes, 536870912}}A resolver !{name:payload} fetches a value from somewhere else — a secret store, a parameter service. CASC ships none: the application registers each one, and so decides exactly what a configuration file is allowed to reach. The same holds for further tags and for import schemes.
*password = !{vault:secret/db/password}