Apply a template to many keys
Use a `&:` spread entry to unify one template into every key of a map or every element of a list.
Rendered from
docs/how-to/apply-a-template-to-many-keys.md
in the engine repository, where a correction belongs, and where the
test suite executes every example on this page.
Ten endpoints, one policy, and the policy written ten times: that is
how drift starts. Write it once as a &: spread entry: it is
unified into every other key of the map:
endpoints: { &: { method: *GET|string auth: *true|boolean } }
endpoints: list: {}
endpoints: create: method: POST
{ "endpoints": {
"create": { "auth": true, "method": "POST" },
"list": { "auth": true, "method": "GET" }
} }
create keeps its own method and picks up the rest: the template
is a peer of each child, so children override its defaults the
ordinary way. (Keys come out sorted, whatever order they were
written in.) The same entry works in lists:
a: [&: { x:1 } y:1 y:2]
{ "a": [ { "x": 1, "y": 1 }, { "x": 1, "y": 2 } ] }
A top-level &: {...} applies to every key of the root map.
Stamp a schema and a shared field
A template does not have to be defaults. The service-catalog pattern
stamps a type()-marked schema together with the one field every
child in a block shares:
CatalogEntry: type({
owner: string
tier: integer & min(1) & max(3)
description: string
})
payments: services: { &: $.CatalogEntry & { owner:"team-payments" } }
payments: services: payments: { tier:1 description:"Card capture API." }
payments: services: ledger: { tier:1 description:"Ledger of record." }
{ "payments": { "services": {
"ledger": { "description": "Ledger of record.",
"owner": "team-payments", "tier": 1 },
"payments": { "description": "Card capture API.",
"owner": "team-payments", "tier": 1 }
} } }
CatalogEntry itself never appears in the output (the mark keeps
it for unification only, per keep schema out of the
output) and no service repeats its
owner. The stamped constraints are enforced per child. Change
ledger to tier: 4 and run it as catalog.aon:
CatalogEntry: type({
owner: string
tier: integer & min(1) & max(3)
description: string
})
payments: services: { &: $.CatalogEntry & { owner:"team-payments" } }
payments: services: payments: { tier:1 description:"Card capture API." }
payments: services: ledger: { tier:4 description:"Ledger of record." }
$ aontu catalog.aon
[aontu/constraint]: Cannot unify values at path $.payments.services.ledger.tier
...
Cannot unify value: 4 with value: integer&min(1)&max(3)
...
$ echo $?
1
The refusal lands on the child that broke the rule, not on the template: the reader of a CI log gets the offending service by name.
One thing to watch: a spread constrains the children it
meets and creates none of its own, so a map holding
only &: generates {}. Generating children from a table is pack’s
job, and sealing what it generates is the subject of seal generated
children deeply. For lists, the choice
between [&: T] and the positional [T] is decided in constrain every
element of a list.
Spreads are specified in Spreads
&:. The live versions: the
service catalog stamps a
schema and an owner across three domains exactly as above, and the
Kubernetes golden path
combines spreads with generators to fan one service model out into
whole manifests.