Skip to content

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:

modules/host.nix
# 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 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.

modules/hosts.nix
{ 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:

result
{
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";
};
}

Add this file to the example. It sets a channel that is not in the list, and a label that is not a string:

variants/bad-values.nix
# 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:

result
[
{
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\"";
}
]
result
[
{
class = "fixable";
code = "deep";
error = "labels.owner: a part is not a string";
from = "host:cache";
path = [
"labels"
"owner"
];
want = "a string";
}
]
ContributeCommunitySponsor