§ A9 · Track A · CUE from zero
One rule for every entry
Write a rule once for every entry of a collection with a pattern. Each entry states only what differs.
On this page
In HCL, the shape of an entry sits in a variable type. The loop sits in the resource. The key reaches the resource as each.key.
variable "buckets" {
type = map(object({ class = optional(string, "standard") }))
}
resource "aws_s3_bucket" "this" {
for_each = var.buckets
bucket = each.key
}
Three places must agree: the type, the loop and the use of the key. In CUE you already have the shape from A8. The question is how to apply it to every entry.
The text before the colon is the key of a field. A pattern is a rule for fields. It names a kind of key and a value. Every field whose key matches must satisfy that value. The pattern [Name=string]: #Bucket says that every field of a struct is a #Bucket. The Name= gives the key a name, so the rule can use it.
Warm-up
From earlier lessons
A bucket is #Bucket & {name: "logs", extra: 1}, and #Bucket declares no field extra. Is it accepted?
No. #Bucket is closed, and the error is field not allowed (A8).
A field is env: *"dev" | "prod" @tag(env). What does pravo eval -t env=qa do?
It refuses qa. The tag value must match one of the choices (A6).
Long-hand first
Work in an empty directory. Write estate.cue. Three buckets are written one by one.
package estate
#Bucket: {
name!: string
class: *"standard" | "ia" | "glacier"
versioning: *false | bool
}
buckets: {
logs: #Bucket & {name: "logs"}
media: #Bucket & {name: "media", versioning: true}
backups: #Bucket & {name: "backups", class: "glacier"}
}
Save the result. tee prints it and keeps a copy in long.txt.
buckets: {
logs: {
name: "logs"
class: "standard"
versioning: false
}
media: {
name: "media"
class: "standard"
versioning: true
}
backups: {
name: "backups"
class: "glacier"
versioning: false
}
}
Every entry repeats #Bucket &. Every entry writes its own key a second time, as name. Three entries show the repetition. Thirty entries repeat it thirty times.
One rule
State the rule once on buckets. Each entry then lists only what differs from the defaults.
package estate
#Bucket: {
name!: string
class: *"standard" | "ia" | "glacier"
versioning: *false | bool
}
buckets: [Name=string]: #Bucket & {name: Name}
buckets: {
logs: {}
media: {versioning: true}
backups: {class: "glacier"}
}
Compare the new result with the saved one.
diff prints nothing, so the two files give the same result. Read the rule in three parts.
buckets:is the struct that holds the entries.[Name=string]:matches the key of every entry.Namestands for that key.#Bucket & {name: Name}is what every entry must be. Itsnameis its key.
An empty struct such as logs: {} is a complete entry. The rule supplies the shape, the defaults and the name.
Break it
Misspell a field in one entry.
package estate
#Bucket: {
name!: string
class: *"standard" | "ia" | "glacier"
versioning: *false | bool
}
buckets: [Name=string]: #Bucket & {name: Name}
buckets: {
logs: {}
media: {vrsioning: true}
backups: {class: "glacier"}
}
Error: evaluating .:
buckets.media.vrsioning: field not allowed:
./estate.cue:13:12
The path is buckets.media.vrsioning. The rule reached the entry media, built it from #Bucket, and the closed shape refused the field. The single location is the line that wrote it. The other two entries are fine, and no result is printed.
A rule does not create entries
Remove the entries and keep the shape and the rule.
package estate
#Bucket: {
name!: string
class: *"standard" | "ia" | "glacier"
versioning: *false | bool
}
buckets: [Name=string]: #Bucket & {name: Name}
Decide before you open the answer.
Predict What does pravo eval print?
An empty struct. The rule checks the entries that exist. There are none.
buckets: {}
Pitfall
A pattern constrains entries that exist. It never adds one. To create entries from data, use a comprehension (A10).
Exercise
Collapse the rules
Replace estate.cue with this file. Three security rules are written long-hand.
package estate
#Rule: {
name!: string
port!: int & >=1 & <=65535
protocol: *"tcp" | "udp"
}
rules: {
http: #Rule & {name: "http", port: 80}
https: #Rule & {name: "https", port: 443}
dns: #Rule & {name: "dns", port: 53, protocol: "udp"}
}
Save the result, then rewrite the file.
rules: {
http: {
name: "http"
port: 80
protocol: "tcp"
}
https: {
name: "https"
port: 443
protocol: "tcp"
}
dns: {
name: "dns"
port: 53
protocol: "udp"
}
}
Add a pattern to rules. Remove #Rule & and every name from the entries. The check fails in three cases. The output differs from before.txt. An entry still writes its name. #Rule & appears more than once.
pravo eval | diff before.txt - > /dev/null || { echo "the output changed"; exit 1; }
if grep -q 'name: "' estate.cue; then echo "an entry still writes its name"; exit 1; fi
[ "$(grep -c '#Rule &' estate.cue)" -le 1 ] || { echo "#Rule & appears in more than one place"; exit 1; }
echo ok
Run it on the start state. It fails.
an entry still writes its name
Hint 1
Follow the rule in buckets. The key is bound with Name=string, and the shape is #Rule & {name: Name}.
Hint 2
See pattern. Each entry keeps only port, and protocol where it is not "tcp".
Solution
package estate
#Rule: {
name!: string
port!: int & >=1 & <=65535
protocol: *"tcp" | "udp"
}
rules: [Name=string]: #Rule & {name: Name}
rules: {
http: {port: 80}
https: {port: 443}
dns: {port: 53, protocol: "udp"}
}
Patterns that match some keys
The key part of a pattern can be any constraint on a string. A regular expression limits the rule to some entries.
package estate
#Bucket: {
name!: string
versioning: *false | bool
}
buckets: [Name=string]: #Bucket & {name: Name}
buckets: [=~"^logs-"]: versioning: true
buckets: {
"logs-web": {}
media: {}
}
buckets: {
"logs-web": {
name: "logs-web"
versioning: true
}
media: {
name: "media"
versioning: false
}
}
The entry logs-web matches both rules and gets versioning: true. The entry media matches only the first.
Recap
- A pattern
[Name=string]: shapeapplies the shape to every entry of a struct. - Entries state only what differs. The key supplies
name. - A pattern constrains entries that exist. It never creates one.
Check yourself
What does Name= do in [Name=string]: #Bucket & {name: Name}?
It gives the key of the entry being checked a name. The rule uses it to set name.
Does a pattern add entries to a struct that has none?
No. It constrains entries that exist. A comprehension generates entries.
The next lesson generates the entries themselves from data.