Skip to content

Data is queried, not merged

The NixOS module system merges every definition into one value, then reads it. yen does not merge. A read of a path is one query on the files that answer that path.

Here two feature files declare settings at two paths of one field. Neither file knows the other:

modules/features/zfs.nix
# Each feature declares its own setting, at its own path, in its own file.
{ yen, lib, ... }:
{
yen.kinds.host.fields.settings.zfs.arcMax = yen.lib.maybe yen.lib.int; # (1)
yen.kinds.environment.fields.settings.zfs.arcMax = yen.lib.maybe yen.lib.int; # (2)
}

In zfs.nix, (1) declares the field settings.zfs.arcMax of every host, and (2) declares the same path on the kind environment. In k3s.nix, (1) declares settings.k3s.role with the default "agent".

A read of host.settings.k3s.role looks at the instance, then at the type default. It never reads zfs.nix. result.declaredIn answers which files declare a path:

result
[
"k3s.nix"
"zfs.nix"
]

These hosts take a value from their own settings, or from their environment:

modules/hosts.nix
{ yen, ... }:
{
yen.kinds.host.fields.environment = yen.kinds.environment; # (1)
yen.environment.prod.settings.zfs.arcMax = 8; # (2)
yen.host.nas = {
environment = yen.environment.prod;
settings.zfs.arcMax = 32; # (3)
};
yen.host.node = {
environment = yen.environment.prod;
settings.k3s.role = "server";
};
}

At (1), a host has a ref to an environment. At (2), the environment prod sets arcMax. At (3), nas sets its own. node sets none. This aspect reads the value for each host:

modules/report.nix
# `cascade path xs`: the first non-null value along a list, in your order.
{ yen, ... }:
{
yen.aspects.report = {
at = yen.kinds.host;
report =
{ host, ... }:
{
${host.name} = {
arcMax = yen.lib.cascade [ "settings" "zfs" "arcMax" ] [ host host.environment ]; # (1)
k3s = yen.helpers.k3sRole host; # (2)
};
};
};
}

A cascade is a list in your order. At (1), yen.lib.cascade path xs gives the first non-null value along xs. nas has its own arcMax; node falls back to its environment. At (2), the aspect reads a helper (see Helpers below). The report of the fleet:

result
{
nas = {
arcMax = 32;
k3s = "agent";
};
node = {
arcMax = 8;
k3s = "server";
};
}

yen.helpers holds shared values and functions. It is an open namespace: any file declares a key, and any module reads it from its yen arg, as report.nix does. The key k3sRole is declared here:

modules/helpers.nix
# `yen.helpers`: shared values and functions. Any file declares a key; any module reads it from `yen`.
{ yen, ... }:
{
yen.helpers.k3sRole = host: host.settings.k3s.role; # (1)
}

A fleet chooses its own helpers, so yen does not document them and the schema (see Agent surface) does not list them. A helper is a declaration like any other: one answer per key. A second file that gives the same key is two answers, because two functions are never equal:

variants/helper-twice.nix
# A second file gives the same helper. Functions are never equal, so this is two answers, not an override.
{ yen, ... }:
{
yen.helpers.k3sRole = host: "server";
}
result
[
{
code = "two-answers";
path = [
"helpers"
"k3sRole"
];
}
]

One leaf with two different values is not an override. It is an error record, and you keep one answer:

variants/override.nix
# A second, different answer for one leaf. Data never overrides data.
{ yen, ... }:
{
yen.host.nas.settings.zfs.arcMax = 16;
}
result
[
{
class = "fixable";
code = "two-answers";
error = "two answers for host.nas.settings.zfs.arcMax";
fix = "keep one answer for host.nas.settings.zfs.arcMax: data never overrides data";
from = "declarations";
got = 2;
path = [
"host"
"nas"
"settings"
"zfs"
"arcMax"
];
}
]
ContributeCommunitySponsor