Verweise
Drei Arten von Verweisen holen Werte von anderswo — aus Variablen, der Umgebung und anderen Schlüsseln — und teilen sich eine Syntax für Defaults, Wächter, Indizes und Filter. Tags und Resolver wandeln um und holen ab.
Drei Arten
| Form | Liest | Aufgelöst |
|---|---|---|
@{name} | eine mit @name = … deklarierte Variable | gegen alle Variablen des Ladevorgangs |
${NAME} | eine Umgebungsvariable | beim Laden |
%{a.b.c} | einen anderen Schlüssel der Konfiguration | gegen den fertigen, zusammengeführten Baum |
Ein Verweis, der den ganzen Wert bildet, behält den Typ dessen, was er liest — %{server.port} ist eine Ganzzahl. In einer Zeichenkette mit doppelten Anführungszeichen wird er als Text eingesetzt.
Werte in einer Zeichenkette
Ein in eine Zeichenkette interpolierter Wert liest sich so, wie CASC ihn schreibt, in jeder Implementierung: nil, true, inf; ein Atom als sein Name; eine Zeitdauer in Nanosekunden, eine Bytegröße in Bytes; eine Adresse in kanonischer Form; eine Gleitkommazahl mit den kürzesten Ziffern, die wieder dieselbe Zahl ergeben, immer mit Nachkommastelle. Listen, Tupel und Maps haben keine Textform; sie zu interpolieren ist ein Ladefehler.
@d = 500ms
@b = 2KiB
@f = 1000.0
@i = inf
@a = info
@ip = 10.0.0.0/8
s = "@{d} @{b} @{f} @{i} @{a} @{ip}"%{"s" => "500000000ns 2048B 1.0e3 inf info 10.0.0.0/8"}Defaults, Wächter und Indizes
Alle drei Arten nehmen dieselben Suffixe. Ohne Suffix ist ein Verweis auf etwas Undefiniertes ein Ladefehler.
| Suffix | Bedeutung |
|---|---|
@{x:default} | default verwenden, wenn undefiniert |
@{x:+alt} | alt, wenn definiert, sonst "" |
@{x:?"message"} | den Ladevorgang mit dieser Meldung abbrechen, wenn undefiniert |
@{x[1]} | Index in eine Liste (ab 0), kombinierbar mit einem Default |
@{x | trim} | Filter anwenden, von links nach rechts, nach dem Suffix |
Ein shell-artiges :-default gibt es bewusst nicht. Ein Default wird als CASC-Wert gelesen, ${PORT:8080} fällt also auf die Ganzzahl 8080 zurück. :+ leitet immer einen Ersatzwert ein: ${PORT:+1} setzt 1 ein, ein Default von minus eins ist ${PORT:-1}. Die Meldung nach :? ist eine gewöhnliche Zeichenkette in doppelten Anführungszeichen und darf interpolieren.
Variablen
@name = value deklariert eine öffentliche Variable, sichtbar in jeder Datei des Ladevorgangs — in den Dateien, die diese importieren, und in denen, die sie importiert. @*name deklariert eine private, nur in der eigenen Datei sichtbare Variable; sie verdeckt dort eine öffentliche gleichen Namens.
@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"}Variablen erscheinen nicht im Ergebnis. Sie werden gegen den gesamten Ladevorgang aufgelöst, nicht in Lesereihenfolge: Eine Variable darf oberhalb ihrer Deklaration verwendet werden, und bei doppelter Deklaration gewinnt die letzte.
Umgebung
Eine nicht gesetzte oder leere Umgebungsvariable gilt als undefiniert. Was aus der Umgebung kommt, ist immer eine Zeichenkette; ein Tag wandelt sie um. Ein Verweis mit [] am Ende teilt den Wert an , und ; in eine Liste.
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"]}Bedingte Anweisungen
${?NAME} in einer eigenen Zeile behält die nächste Anweisung nur, wenn NAME gesetzt und nicht leer ist — genau eine Anweisung, die auch ein ganzer Block sein darf.
${?ENABLE_METRICS}
metrics {
port = 9568
path = "/metrics"
}Die aktuelle Umgebung
${COOPER_ENV} ist in jeder Implementierung immer gesetzt, sodass ein Dokument Dateien pro Umgebung überall gleich auswählt: import "env/${COOPER_ENV}.casc". Die Namen sind dev, staging, test und prod. Ein echtes COOPER_ENV gewinnt und gilt genau so, wie es geschrieben ist. Ist es nicht gesetzt oder leer, greift jede Implementierung auf die Variable ihres Hosts zurück — MIX_ENV, PRX_ENV, APP_ENV, NODE_ENV — oder sonst auf dev und bildet diesen Wert auf die gemeinsamen Namen ab:
| Wert des Hosts | COOPER_ENV |
|---|---|
development, local | dev |
testing | test |
production | prod |
alles andere (dev, staging, test, prod, …) | unverändert |
Konfigurationsverweise
%{path} liest einen anderen Schlüssel. Er wird zuletzt aufgelöst, gegen den fertigen Baum — nach allen Imports, Überschreibungen und Sigils — und sieht daher den endgültigen Wert, egal wo er gesetzt wurde. Ein Zyklus von Verweisen ist ein Ladefehler.
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"}Ein Pfad wird durch einen Verweis hindurch verfolgt: Ein Schlüssel, der einen Verweis enthält, enthält das, worauf dieser zeigt, und ein Pfad darf darin weiterlaufen. Ein Pfad durch den eigenen Schlüssel (a = %{a.b}) ist ein Zyklus.
origins.eu { code = "eu", host = "eu.example.com" }
origin = %{origins.eu}
code = %{origin.code}%{"origins" => %{"eu" => %{"code" => "eu", "host" => "eu.example.com"}},
"origin" => %{"code" => "eu", "host" => "eu.example.com"},
"code" => "eu"}Filter
Filter bereinigen eine Zeichenkette beim Einlesen. Die Menge ist abgeschlossen: trim, downcase, upcase, trim_prefix: "…" und trim_suffix: "…". Alles andere ist ein Ladefehler, ebenso ein Filter auf etwas anderes als eine Zeichenkette. Ein Geheimnis lässt sich filtern und bleibt geheim. Argumente in einfachen Anführungszeichen machen Filter auch innerhalb doppelt gequoteter Zeichenketten nutzbar.
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"}Zusammengesetzte Namen
Der Name in einem Verweis darf selbst eine Zeichenkette mit Interpolation sein. So liest eine Schleife pro Element eine eigene Umgebungsvariable. Einen Verkettungsoperator gibt es darüber hinaus bewusst nicht.
@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 und Resolver
Ein Tag !name(value) wandelt sein eines Argument um; Tags lassen sich verschachteln. Eingebaut sind !int, !float, !bool, !duration, !bytes, !trim, !downcase, !upcase und !module. Eine fehlgeschlagene Umwandlung ist ein Ladefehler.
port = !int(${PORT:"8080"})
debug = !bool(${DEBUG:"false"})
ttl = !duration("5m")
cache = !bytes("512MiB")%{"port" => 8080, "debug" => false,
"ttl" => %Cooper.Duration{nanoseconds: 300000000000},
"cache" => %Cooper.ByteSize{bytes: 536870912}}!bool liest true, 1, yes und on als wahr und false, 0, no und off als falsch — nur kleingeschrieben — und lehnt alles andere ab. DEBUG=1 funktioniert also wie in den meisten .env-Dateien.
Ein Resolver !{name:payload} holt einen Wert von anderswo — aus einem Secret-Store, einem Parameterdienst. CASC bringt keinen mit: Die Anwendung registriert jeden einzelnen und entscheidet damit genau, was eine Konfigurationsdatei erreichen darf. Dasselbe gilt für weitere Tags und für Import-Schemata.
*password = !{vault:secret/db/password}Module
!module("Name") benennt ein Modul der Host-Sprache, oft eines, das pro Deployment über ${…} gewählt wird. Der Name wird für jede Implementierung gleich geschrieben: durch Punkte getrennte Segmente, jedes in PascalCase. Alles andere — crypto, Foo.bar, stripe_client — ist ein Ladefehler. Jede Implementierung schlägt den Namen zuerst in einer modules-Zuordnung nach, die die Anwendung übergibt, und wendet dann ihre eigene Konvention an. Das Modul wird nur benannt, nie geladen oder geprüft.
client = !module("Acme.Payments")
label = "x-!module(\"Foo.Bar\")"%{"client" => Acme.Payments, "label" => "x-Foo.Bar"}| Geschrieben | Elixir | Praxis | PHP | Node.js |
|---|---|---|---|---|
Acme.Payments.StripeClient | Acme.Payments.StripeClient | acme.payments.stripe-client | Acme\Payments\StripeClient | Zuordnung nötig |
ASCO.HTTPClient | ASCO.HTTPClient | asco.http-client | ASCO\HTTPClient | Zuordnung nötig |
Node.js hat keine Konvention, in die sich übersetzen ließe — ein Node-Modul ist ein Ort, kein Name —, daher braucht dort jedes Modul einen Eintrag in der Zuordnung. So erreicht auch Elixir ein Erlang-Modul: %{"Crypto" => :crypto}. Ein zugeordnetes Modul, das in eine Zeichenkette interpoliert wird, liest sich als das, worauf es abgebildet ist — hier crypto, nicht Crypto —, denn die Zuordnung gehört der Anwendung.