Everything a configuration declares is an entity: a database, an app, a variable, a module. Each is written as a block whose labels give its type and name, and each has an address that other blocks, and the application, refer to it by.
Blocks and labels
database "postgres" "main" { location = "db.local"}
app "web" { image = "nginx:1.27"}The keyword before the labels is the entity’s type: database and
app. The last label is its name: main and web. A type can take a
subtype between the two: postgres is the subtype of the first block.
The subtype is optional, but whether a block has one is not its own choice. It is fixed by how the
type was registered: a type registered with a subtype is always written with
one, and a type registered without one never takes one.
local := registry.NewLocal()registry.Register[PostgreSQL](local, "database", "postgres") // database "postgres" "main" {}registry.Register[App](local, "app") // app "web" {}@dataclassclass PostgreSQL: location: str meta: xcl.Meta | None = None
@dataclassclass App: image: str meta: xcl.Meta | None = None
local = xcl.LocalRegistry()local.register_type(PostgreSQL, "database", "postgres") # database "postgres" "main" {}local.register_type(App, "app") # app "web" {}A block written with too few or too many labels is reported at the block,
saying which labels its type takes: app takes a label: write it as app "<label>" { … }.
Each entity is declared once. A second block with the same labels, in the same
file or another file of the configuration, is an error naming where the first
one is: app.web is already declared at <file>:<line>,<col>-<col>, each entity is declared once, or for a singleton runner is declared once, it is already declared at <file>:<line>,<col>-<col>.
Singletons
runner { server = "https://maalstrom.app" listen = "127.0.0.1:7660"}A type of which there is one is written without labels. It is a
singleton: a module holds at most one of it, and it is addressed by its
type alone, runner, so runner.server reads its server.
Whether a type is a singleton follows from whether it can read a name. A type
reads its name through a label field, or through the entity’s metadata: in Go
a field called Meta, in Python a field called meta. A type with neither is
a singleton. A singleton takes no subtype.
type Runner struct { Server string `xcl:"server"` Listen string `xcl:"listen,optional"`}
registry.Register[Runner](local, "runner") // runner {}@dataclassclass Runner: server: str listen: str = ""
local.register_type(Runner, "runner") # runner {}A singleton written with a label is reported at the label: runner takes no labels, there is one of it: write it as runner { … }, without "nas". The same
holds for a plugin’s types.
Addresses
An entity’s address is its labels joined with dots: database.postgres.main
and app.web, or its type alone for a singleton, runner. The address is
what a reference starts with, so
database.postgres.main.location reads the first block’s location, and it
is what the application passes to xcl.Find[T] or cfg.find.
An entity declared inside a module has the module’s name in front:
module.<name>.<address>, such as module.db.database.postgres.main, and
one in a module nested in another module has both, module.a.module.b.….
See References and processing order for what a
reference may reach.
Built-in fields
Every entity has two attributes that can be written in configuration, and a
meta object that xcl fills in and configuration can only read:
app "web" { image = "nginx:1.27" depends_on = ["database.postgres.main"] disabled = false}depends_onlists the addresses of entities that must be processed before this one, for an order that no reference expresses.disabled, whentrue, leaves the entity out of processing.
The meta fields are read through another entity’s address, so a block can
pass on an entity’s identity rather than repeat it:
deployment "api" { image = "api:1.4" config_map = config_map.api.meta.id}docker "container" "web" { image = "nginx:1.27-alpine"
network { name = docker.network.main.meta.name }}| Field | Value |
|---|---|
meta.id | the entity’s address, such as config_map.api, or runner for a singleton |
meta.name | the name label, such as api, empty for a singleton |
meta.type | the type keyword, such as config_map |
meta.subtype | the subtype label, empty for a type without one |
meta.module | the name of the module the entity was declared in, empty at the root |
meta.file | the absolute path of the file the block is in |
meta.line | the line the block starts on |
meta.column | the column the block starts at |
In Go a type reads these fields through a field called Meta, a
types.Meta with no tag, which receives a copy of the entity’s metadata. It
also carries the disabled flag and the depends_on list configuration set
on the block, as Meta.Disabled and Meta.DependsOn. Every block takes
disabled, depends_on and meta, so a type declares no fields by those
names; xcl holds them for every entity, beside the type’s own fields. In
Python the field is called meta, an xcl.Meta | None, and carries the same,
meta.disabled and meta.depends_on among it; it is not part of the class’s
schema:
type Meta struct { ID string // the entity's address Name string // its name label Type string Subtype string Module string File string Line int Column int
// set in configuration on any block Disabled bool DependsOn []string
// …}@dataclassclass App: image: str
# id, the entity's address, name, type, subtype, module, file, line, # column, and disabled and depends_on, set in configuration on any block meta: xcl.Meta | None = NoneReading them back from an entity the application has found:
app, err := xcl.Find[App](c, "app.web")if err != nil { return err}
fmt.Println(app.Meta.ID, app.Meta.File, app.Meta.Line)fmt.Println(app.Meta.DependsOn, app.Meta.Disabled)app = cfg.find(App, "app.web")
print(app.meta.id, app.meta.file, app.meta.line)print(app.meta.depends_on, app.meta.disabled)Entity types
The language itself declares three types: variable, output and
module. Every other type comes from the application that reads the
configuration, either a plain Go or Python type it registers, or a type a
plugin provides together with the provider that runs it. Both are given to a
Config through registries, and a configuration can only use
types the Config was given.
Next
Attributes and blocks covers every shape a type’s fields can take. Entities that use plugin-provided types are run by their providers; see Registries and the plugins example.