Types and defaults
yen has no type system of its own. A type is any value with check = value: bool.
A lib.types value from nixpkgs works, a kind works (see Instances and refs),
and a check function can make a type. The fields of a kind use types, and some types add defaults.
This kind has one field of each form:
# A type is anything with `check = value: bool`. yen adds defaults on top of it.{ yen, lib, ... }:let inherit (yen.lib) opt maybe derive is ;in{ yen.kinds.host.fields = { addr = yen.lib.str; # (1) required port = opt yen.lib.port 22; # (2) a default jump = maybe yen.lib.str; # (3) null by default vpn = derive yen.lib.str (self: "vpn-${self.name}"); # (4) a default computed from the instance tags = opt (is (v: builtins.isList v && builtins.all builtins.isString v)) [ ]; # (5) your own check labels = opt (yen.lib.attrsOf yen.lib.str) { }; # (6) an attrset whose values are strings settings.kernel.channel = opt (yen.lib.enum [ "lts" "latest" ]) "lts"; # (7) one of a list, and a nested field };}Each field has a type. (1) is required: it has no default. (2) has the default 22.
(3) is null when an instance does not set it. (4) computes its default from the instance itself.
(5) makes a type from your own check function. (6) is an attrset whose values are strings.
(7) is one of a list of values, and the field sits in a nested path: settings.kernel.channel.
The constructors
Section titled “The constructors”The constructors of the types above, with the default of each:
| Constructor | Default |
|---|---|
yen.lib.str, any type |
none: the field is required |
yen.lib.opt type value |
value |
yen.lib.maybe type |
null |
yen.lib.derive type (self: …) |
computed from the instance itself |
yen.lib.is (v: …) |
none: a type from a check function |
yen.lib.listOf type |
none: a list whose elements pass the type |
yen.lib.attrsOf type |
none: an attrset whose values pass the type |
yen.lib.enum [ … ] |
none: one of the listed values |
A field at a path, such as settings.kernel.channel, has its own type and default.
An instance gives only what it needs. The aspect that prints the report is in
the example code.
{ yen, ... }:{ yen.host.web.addr = "10.0.0.1"; yen.host.db = { addr = "10.0.0.2"; port = 2222; vpn = "vpn-database"; # (1) an explicit value wins over `derive` labels.role = "database"; settings.kernel.channel = "latest"; };}(1) sets vpn by hand, so the derive default of vpn does not apply to db.
The read of a field looks at the instance first, then at the default, then checks the value:
{ db = { channel = "latest"; jump = null; labels = { role = "database"; }; port = 2222; tags = [ ]; vpn = "vpn-database"; }; web = { channel = "lts"; jump = null; labels = { }; port = 22; tags = [ ]; vpn = "vpn-web"; };}Values that fail a type
Section titled “Values that fail a type”Add this file to the example. It sets a channel that is not in the list, and a label that is not a string:
# A channel that is not in the list, and a label that is not a string.{ yen, ... }:{ yen.host.cache = { addr = "10.0.0.3"; settings.kernel.channel = "stable"; labels.owner = 1; };}A value that fails its type is an error record that names the field. A field reads only the shape of its
value: a list, an attrset, a function. The parts are checked later, by result.deep, after the fleet
evaluated: so building one host never forces the list elements of another, and a part that throws is an
error record, not a crash. nix flake check fails on both (checks.<system>.yen-lean).
These are the errors, then the deep records:
[ { class = "fixable"; code = "check"; error = "settings.kernel.channel fails its check"; fix = "give settings.kernel.channel a value of type one of \"lts\", \"latest\""; from = "host:cache"; got = "stable"; path = [ "settings" "kernel" "channel" ]; want = "one of \"lts\", \"latest\""; }][ { class = "fixable"; code = "deep"; error = "labels.owner: a part is not a string"; from = "host:cache"; path = [ "labels" "owner" ]; want = "a string"; }]