§ A11 · Track A · CUE from zero

Naming a value once

Use the value of one field in another. Keep helper values out of the output with let. Share them across files with hidden fields.

Time
8 min
Level
beginner
Needs
A10
You will learn
How does one field use the value of another? Where does a helper value live so that it stays out of the output?
Done when
pravo eval prints the same data from a shorter file, and a misspelt name is refused.

View as Markdown

On this page

HCL keeps a named value in a locals block. You read it with the prefix local..

main.tf
locals {
  base = "10.0"
}

resource "aws_subnet" "a" {
  vpc_id     = "vpc-0abc1234"
  cidr_block = "${local.base}.1.0/24"
}

The prefix says where the name lives. It also means a value that is not in locals needs another prefix: var., data., module..

CUE has no prefix. A field is a name, and you write the name where you need the value. This is a reference.

Warm-up

From earlier lessons

A pattern gives every bucket a name built from its key: buckets: [Name=string]: {name: "estate-\(Name)"}. What does pravo eval print for buckets: logs: {}?

The name estate-logs. The pattern from A9 applies to every entry, and Name stands for the key.

A bucket is built from a definition that declares name. What happens when a second field is spelled nmae?

The file is refused with field not allowed. A definition is closed (A8).

Refer to a field

Work in an empty directory. Write estate.cue. The prefix 10.0 is written once, in base.

estate.cue
package estate

vpc: cidr: "\(base).0.0/16"

subnets: {
	a: cidr: "\(base).1.0/24"
	b: cidr: "\(base).2.0/24"
}

base: "10.0"
terminal
$pravo eval
vpc: {
	cidr: "10.0.0.0/16"
}
subnets: {
	a: {
		cidr: "10.0.1.0/24"
	}
	b: {
		cidr: "10.0.2.0/24"
	}
}
base: "10.0"

Inside a string, \(base) stands for the value of base. A12 covers strings.

base is written last and used first. Order does not matter (A4). There is no evaluation order to manage.

pravo eval also prints base. A regular field is data, and all data is output.

Keep a helper out of the output

A value that only helps you build other values should not be output. A let names a value for use in the file. It is not a field.

estate.cue
package estate

let base = "10.0"

vpc: cidr: "\(base).0.0/16"

subnets: {
	a: cidr: "\(base).1.0/24"
	b: cidr: "\(base).2.0/24"
}
terminal
$pravo eval
vpc: {
	cidr: "10.0.0.0/16"
}
subnets: {
	a: {
		cidr: "10.0.1.0/24"
	}
	b: {
		cidr: "10.0.2.0/24"
	}
}

The data is the same, and base is gone from the output.

Share a helper between files

A let at the top of a file belongs to that file. A hidden field belongs to the package. Its name starts with _. Every file of the package can read it, and pravo eval does not print it.

Write a second file.

zone.cue
package estate

_region: "eu-west-1"

Read _region from estate.cue.

estate.cue
package estate

let base = "10.0"

vpc: cidr: "\(base).0.0/16"

subnets: {
	a: {cidr: "\(base).1.0/24", zone: "\(_region)a"}
	b: {cidr: "\(base).2.0/24", zone: "\(_region)b"}
}
terminal
$pravo eval
vpc: {
	cidr: "10.0.0.0/16"
}
subnets: {
	a: {
		cidr: "10.0.1.0/24"
		zone: "eu-west-1a"
	}
	b: {
		cidr: "10.0.2.0/24"
		zone: "eu-west-1b"
	}
}

The three kinds of name differ in two ways.

Kind Written as In the output Readable from
Regular field base: "10.0" Yes Every file of the package, and importing packages
let let base = "10.0" No The file, or the struct where it is written
Hidden field _region: "eu-west-1" No Every file of the package

pravo eval omits hidden fields, and -e cannot name one.

terminalexit 1
$pravo eval -e _region
Error: -e "_region": reference "_region" not found:
    1:1

To look at a hidden value, copy it into a regular field.

Break it

Misspell a name.

estate.cuerejected
package estate

let base = "10.0"

vpc: cidr: "\(bse).0.0/16"

subnets: {
	a: {cidr: "\(base).1.0/24", zone: "\(_region)a"}
	b: {cidr: "\(base).2.0/24", zone: "\(_region)b"}
}
terminalexit 6
$pravo eval
Error: evaluating .:
vpc.cidr: reference "bse" not found:
    ./estate.cue:5:15

Read the error from the top.

  • The path is vpc.cidr: the field that holds the reference.
  • The message is reference "bse" not found. No struct around the reference has a field or let named bse.
  • The location is line 5, column 15: the first letter of bse.

The same error appears when the name exists but is not visible from the file. Correct the typo, then read base from a second file.

estate.cue
package estate

let base = "10.0"

vpc: cidr: "\(base).0.0/16"

subnets: {
	a: {cidr: "\(base).1.0/24", zone: "\(_region)a"}
	b: {cidr: "\(base).2.0/24", zone: "\(_region)b"}
}
other.cuerejected
package estate

other: cidr: "\(base).9.0/24"
terminalexit 6
$pravo eval
Error: evaluating .:
other.cidr: reference "base" not found:
    ./other.cue:3:17

The let is private to estate.cue, so other.cue cannot see it.

A let that nothing reads is refused too. Replace other.cue.

other.cuerejected
package estate

let spare = "unused"
terminalexit 6
$pravo eval
Error: evaluating .:
unreferenced alias or let clause spare:
    ./other.cue:3:1

Remove the file.

terminal
$rm other.cue

A field cannot read its own name

In HCL you write region = local.region. The same shape in CUE, region: region, does not work.

A name looks in the struct where it is written, then in each struct around it. It uses the first match. The field you are writing is a match.

estate.cueunintended
package estate

region: "eu-west-1"

bucket: {
	region: region
}

Decide before you open the answer.

Predict A top-level region exists. What does bucket.region hold?

It holds _, which means any value. The region on the right is the field being written, so the field refers to itself.

terminalexit 1
$pravo eval
$pravo eval -c
region: "eu-west-1"
bucket: {
	region: _
}

Error: the value is not concrete:
bucket.region: incomplete value _

pravo eval does not fail here. The flag -c does, because _ is not concrete.

Pitfall

A field must not share a name with the value it reads. Give the helper another name: let prefix, or _region for a field region.

Exercise

Exercise

Write each value once

Start clean.

terminal
$rm zone.cue

This file has 24 lines. It repeats 10.0 and eu-west-1.

estate.cue
package estate

vpc: cidr: "10.0.0.0/16"

subnets: {
	a: {cidr: "10.0.1.0/24", zone: "eu-west-1a"}
	b: {cidr: "10.0.2.0/24", zone: "eu-west-1b"}
	c: {cidr: "10.0.3.0/24", zone: "eu-west-1c"}
}

buckets: {
	logs: {
		name:   "estate-logs-eu-west-1"
		region: "eu-west-1"
	}
	assets: {
		name:   "estate-assets-eu-west-1"
		region: "eu-west-1"
	}
	backups: {
		name:   "estate-backups-eu-west-1"
		region: "eu-west-1"
	}
}

Save the output first.

terminal
$pravo eval > expected.txt
$wc -l < estate.cue | tr -d ' '
24

Rewrite estate.cue so that 10.0 and eu-west-1 each appear once. The output must match expected.txt, and the file must be shorter.

check.sh
pravo eval | diff expected.txt - > /dev/null || { echo "the output changed"; exit 1; }
[ "$(grep -o '10\.0' estate.cue | wc -l | tr -d ' ')" = 1 ] || { echo "10.0 should appear once"; exit 1; }
[ "$(grep -o 'eu-west-1' estate.cue | wc -l | tr -d ' ')" = 1 ] || { echo "eu-west-1 should appear once"; exit 1; }
[ "$(wc -l < estate.cue | tr -d ' ')" -lt 24 ] || { echo "the file should be shorter than 24 lines"; exit 1; }
echo ok

Run it on the start state. It fails.

terminalexit 1
$sh check.sh
10.0 should appear once
Hint 1

Use a let for each repeated value. The field region needs a helper with a different name.

Hint 2

Read let and the pitfall above. A helper named region clashes with the field region.

Solution
estate.cue
package estate

let prefix = "10.0"
let zoneName = "eu-west-1"

vpc: cidr: "\(prefix).0.0/16"

subnets: {
	a: {cidr: "\(prefix).1.0/24", zone: "\(zoneName)a"}
	b: {cidr: "\(prefix).2.0/24", zone: "\(zoneName)b"}
	c: {cidr: "\(prefix).3.0/24", zone: "\(zoneName)c"}
}

buckets: {
	logs: {name: "estate-logs-\(zoneName)", region: zoneName}
	assets: {name: "estate-assets-\(zoneName)", region: zoneName}
	backups: {name: "estate-backups-\(zoneName)", region: zoneName}
}
terminal
$wc -l < estate.cue | tr -d ' '
18

Recap

  • A field reads another field by its name. Order does not matter.
  • A let or a _hidden field names a helper and stays out of the output. A let belongs to its file. A hidden field belongs to the package.
  • A name uses the first match, searching outward from where it is written. The field you are writing is a match.

Check yourself

Which two kinds of name stay out of pravo eval output?

A let and a hidden field. A regular field is data and is always printed.

Why does region: region leave region as _?

The right-hand region finds the field being written, which is the first match. The field refers to itself. Name the helper differently.

The next lesson makes a field exist only when a condition holds.