Aufbau einer Datei
Kopfzeile, Kommentare, Schlüssel und Blöcke — und was passiert, wenn derselbe Schlüssel mehrfach geschrieben wird.
Die Versionszeile
Jede Datei — auch eine importierte — beginnt mit #@version und der Formatversion, gegen die sie geschrieben ist. Das = ist optional, die Version folgt SemVer. Die kleinste gültige Datei besteht nur aus dieser Zeile:
#@version = 1.0
%{}Die Versionszeile ist die erste Anweisung, nicht unbedingt die erste Zeile: Kommentare und Leerzeilen dürfen davor stehen.
# Configuration of the api service. #@version = 1.0 name = "api"
%{"name" => "api"}Leerraum und Kommentare
Anweisungen werden durch Leerraum getrennt; eine pro Zeile ist die Konvention. Innerhalb eines Blocks dürfen auch Kommas trennen. Ein # mit folgendem Leerzeichen beginnt einen Kommentar bis zum Zeilenende. Blockkommentare gibt es nicht.
#@version = 1.0
# A comment runs to the end of the line.
name = "api" # so does a trailing one
limits { soft = 10, hard = 20 }%{"name" => "api", "limits" => %{"soft" => 10, "hard" => 20}}Schlüssel
Ein nackter Schlüssel beginnt mit einem Buchstaben oder _ und setzt sich mit Buchstaben, Ziffern, _, + und - fort; am Ende darf ? oder ! stehen. logger, handle-otp-reports, enabled?, ready! und __sentry__ sind also Schlüssel. Dieselbe Regel gilt für Variablen- und Atom-Namen.
Überall reserviert: nil, true, false, inf, +inf, -inf. Die Wörter for, in, from, as und import haben nur dort Bedeutung, wo eine Schleife oder ein Import sie erwartet, und sind sonst gewöhnliche Schlüssel.
Schlüssel in Anführungszeichen
Alles andere — Leerzeichen, Punkte, ein reserviertes Wort — steht in Anführungszeichen. Doppelte Anführungszeichen erlauben Escapes, einfache sind wörtlich. Ein Schlüssel in Anführungszeichen kann ein Segment eines Punkt-Pfads sein.
headers { "Content-Type" = "application/json" }
foo."bar baz".dronf = "fnord"
"true" = 1%{"headers" => %{"Content-Type" => "application/json"},
"foo" => %{"bar baz" => %{"dronf" => "fnord"}},
"true" => 1}Ein Schlüssel in doppelten Anführungszeichen darf Variablen und die Umgebung interpolieren. Heraus muss eine nicht leere Zeichenkette ohne . kommen — ein Punkt, der erst durch Interpolation entsteht, würde den Pfad stillschweigend teilen —, und er darf nicht aus einem Geheimnis gebaut sein. %{…}, Resolver und Tags sind in einem Schlüssel nicht erlaubt: Schlüssel entstehen, bevor es den Baum gibt, den sie bräuchten.
@name = "primary"
db."@{name}".host = "x"
"${REGION}" = 1%{"db" => %{"primary" => %{"host" => "x"}}, "eu-west" => 1}Geheime Schlüssel
Ein * vor einem beliebigen Schlüsselsegment markiert den Wert als geheim. Der Wert selbst bleibt unverändert; eine Implementierung verpackt ihn so, dass er beim Ausgeben oder Loggen geschwärzt wird. Die Geheimhaltung wandert mit: Eine Zeichenkette, die ein Geheimnis interpoliert, ist selbst geheim und wird mit geschwärzter Stelle ausgegeben.
*password = "hunter2"
db.*token = "t0k3n"
url = "postgres://app:%{password}@db.internal"%{"password" => [~~REDACTED~~],
"db" => %{"token" => [~~REDACTED~~]},
"url" => "postgres://app:[~~REDACTED~~]@db.internal"}Zuweisung
Eine Anweisung weist einem Schlüssel einen Wert zu: key = value. Das = ist optional — port 8080 ist dieselbe Anweisung —, wird der Lesbarkeit halber aber empfohlen. Ein Doppelpunkt ist nie eine Zuweisung.
port 8080 name "api"
%{"port" => 8080, "name" => "api"}Pfade und Blöcke
Ein Block { … } fasst Anweisungen unter einem Schlüssel zusammen; ein Punkt-Pfad greift direkt in verschachtelte Blöcke. Beides erzeugt denselben Baum, und beides lässt sich frei mischen. Diese vier Zeilen ergeben dasselbe:
foo { bar { baz = "dronf" } }
foo { bar.baz = "dronf" }
foo = { bar = { baz = "dronf" } }
foo.bar.baz = "dronf"%{"foo" => %{"bar" => %{"baz" => "dronf"}}}Blöcke sind CASCs einziger Map-Typ. Ein Block steht auf der rechten Seite einer Anweisung oder als Element einer Liste oder eines Tupels (siehe Listen). Ein leerer Block ist eine leere Map; über eine schon vorhandene Map geschrieben, mischt er nichts hinein und lässt sie, wie sie ist.
logger {}
cache = {}
db { host = "x" }
db {}%{"logger" => %{}, "cache" => %{}, "db" => %{"host" => "x"}}Deaktivierte Anweisungen
Ein # direkt vor einer Anweisung schaltet sie ab, egal über wie viele Zeilen sie reicht. Der Schlüssel fehlt im Ergebnis — er ist nicht nil. So lässt sich eine Konfiguration ohne eine Einstellung ausprobieren, ohne sie zu verlieren.
section {
#ignored = 1
#sub { remove = yes }
kept = true
}%{"section" => %{"kept" => true}}Eine deaktivierte Anweisung wird nie ausgewertet: Ein deaktiviertes #import liest keine Datei, ein deaktiviertes #@name deklariert keine Variable, und ein fehlerhaftes Literal darin ist kein Fehler.
Zusammenführen und Überschreiben
Ein Schlüssel darf beliebig oft geschrieben werden, in einer Datei oder über Imports hinweg. Die Anweisungen wirken der Reihe nach:
- Blöcke werden tief zusammengeführt, auf jeder Ebene.
- Skalare, Listen und Tupel werden ersetzt: die letzte Zuweisung gewinnt.
- Ein Typwechsel ersetzt ebenfalls: ein Block nach einem Skalar ist ein Block.
Wo der Normalfall nicht passt, sagt ein Sigil vor dem Schlüssel, was gemeint ist:
| Sigil | Wirkung |
|---|---|
~key { … } | Den Block ersetzen statt hineinzumischen |
+key = [ … ] | An eine Liste anhängen (bei neuem Schlüssel eine normale Zuweisung) |
-key = [ … ] | Diese Elemente aus einer Liste entfernen, nach Gleichheit |
-key | Den Schlüssel löschen; ein fehlender Schlüssel bleibt ohne Wirkung |
Ein Listen-Operand steht für seine Elemente; alles andere, auch nil, ist ein einzelnes Element, +tags = "d" hängt also "d" an. Entfernt wird nach strikter Gleichheit: 1 und 1.0 sind verschiedene Elemente. Ein Sigil innerhalb eines Blocks gilt dort für seinen eigenen Schlüssel.
Tupel werden nie gemischt: + oder - auf ein Tupel ist ein Fehler. Kommen mehrere Präfixe zusammen, ist die Reihenfolge #, dann das Sigil, dann *, etwa #~*database { … }.
Anweisungen enden nicht am Zeilenumbruch. Ein nacktes -key, dem eine weitere vollständige Anweisung folgt, ist daher immer das Löschen, nie ein Entfernen, dessen Wert der Schlüssel der nächsten Zeile wäre. Ist der Wert ein nacktes Wort, das Entfernen mit = schreiben (-tags = b).
# base.casc
server { host = "0.0.0.0", port = 8080 }
tags = ["a", "b", "c"]
feature.legacy_mode = true
# override.casc
import "base.casc"
~server { port = 9090 }
+tags = ["d"]
-tags = ["b"]
-feature.legacy_mode%{"server" => %{"port" => 9090},
"tags" => ["a", "c", "d"],
"feature" => %{}}