A first program
This page builds a small configuration for a web service, one step at a time. Every step is a complete program that you can save as app.stav and evaluate.
The root struct
A Stav program describes one value, and the type of that value is the root struct. By default the stav command uses the struct named Root in the entry file.
struct Root {
name: Str
port: Int = 8080
}
name = "inventory"name has no default, so the program must set it. port has a default of 8080, which applies unless something sets another value. The statement name = "inventory" sets the attribute name of the root.
Evaluate the program:
stav eval app.stav{
"name": "inventory",
"port": 8080
}The output is JSON by default. -o toml and -o yaml select the other formats.
Types catch mistakes early
Change the port to a string and evaluate again:
struct Root {
name: Str
port: Int = 8080
}
name = "inventory"
port = "8080"error[E0401]: expected `Int`, found `Str`
┌─ app.stav:7:8
│
7 │ port = "8080"
│ ^^^^^^Stav reports the error before it evaluates anything. Nothing is printed on standard output, and the command exits with status 1. Every diagnostic has a code, here E0401, and the error code index explains each one.
Refinements
A type can be narrowed with where. A port must lie between 1 and 65535, and a name must not be empty:
struct Root {
name: Str where nonEmpty
port: Int where 1..=65535 = 8080
}
name = "inventory"
port = 80_000error[E0803]: value violates a refinement
┌─ app.stav:7:8
│
3 │ port: Int where 1..=65535 = 8080
│ ------------------- constraint declared here
·
7 │ port = 80_000
│ ^^^^^^
│ │
│ 80000 is not in 1..=65535Refinements are checked on the final value, after everything has been merged. The diagnostic shows both the value and the constraint it breaks.
Blocks
Configuration is easier to read in sections. A field whose type is a struct can be filled with a block: the name of the field, then a body in braces.
struct Tls {
cert: Str
key: Str
}
struct Server {
host: Str = "0.0.0.0"
port: Int where 1..=65535 = 8080
tls: Option<Tls>
}
struct Root {
name: Str where nonEmpty
server: Server = {}
}
name = "inventory"
server {
port = 8443
tls {
cert = "/etc/inventory/cert.pem"
key = "/etc/inventory/key.pem"
}
}name = "inventory"
[server]
host = "0.0.0.0"
port = 8443
[server.tls]
cert = "/etc/inventory/cert.pem"
key = "/etc/inventory/key.pem"Inside server { } the statements are typed against Server, so port means server.port. The same could be written as server.port = 8443 at the top level. Blocks and dotted paths are two ways to say the same thing.
tls is an Option<Tls>. An option without a default is none, and a block or assignment under it makes it present. The default server: Server = {} means "a Server with every field at its own default".
Named blocks
Many configurations hold a collection of similar things: upstreams, users, jobs. A map field with a block clause lets each entry be written as a labeled block.
type Level = debug | info | warn | error
struct Upstream {
url: Str where matches(r"https?://.*")
weight: Int where 1..=100 = 10
}
struct Root {
name: Str where nonEmpty
log_level: Level = info
upstreams: Map<Str, Upstream> = {} block upstream
}
name = "inventory"
log_level = warn
upstream primary { url = "https://a.example.org", weight = 90 }
upstream backup { url = "https://b.example.org" }name: inventory
log_level: warn
upstreams:
backup:
url: "https://b.example.org"
weight: 10
primary:
url: "https://a.example.org"
weight: 90block upstream says that a block upstream <label> { ... } adds the entry <label> to upstreams. Map keys are always printed in sorted order, so the output does not depend on the order of the blocks.
Level is a sum type with four variants. Where a Level is expected, the bare name warn is the variant. Any other name is an error:
type Level = debug | info | warn | error
struct Root {
log_level: Level = info
}
log_level = wranerror[E0302]: unknown name `wran`, which is not a variant of `Level`
┌─ typo.stav:7:13
│
7 │ log_level = wran
│ ^^^^
│
= help: did you mean `warn`?Picking out one value
--path prints a single attribute instead of the whole value:
"https://a.example.org"Formatting
stav fmt rewrites files in their canonical layout: two spaces of indentation, one statement per line, and aligned = and : in runs of related lines. Run it with --check to list the files that would change without changing them:
stav fmt --check .Next
The tour of the language covers functions, match, collections, strings and the rest of the expression language. Structuring configuration shows how to split a program into files and reusable parts.