skip to content

Kinds, Registries and Identity

A deployment has hosts, and an admin kind that inherits from user. Each user names the host that holds their home directory, and reading that field returns the host.

hosts-and-admins.nix
let
gen = (builtins.getFlake "github:sini/gen").lib.mkGenLibs { };
inherit (gen.merge) evalModuleTree mkOption types;
kinds = gen.schema.evalSchema { } [{
config.schema.host = {
options.system = mkOption { type = types.str; default = "x86_64-linux"; };
options.address = mkOption { type = types.str; };
};
config.schema.user = {
options.shell = mkOption { type = types.str; default = "bash"; };
options.home = mkOption { type = gen.schema.declarationOf "host"; };
};
config.schema.admin = {
inherits = [ "user" ];
options.sudo = mkOption { type = types.bool; default = true; };
};
}];
result = evalModuleTree { } [ ({ config, ... }: {
options.hosts = gen.schema.mkInstanceRegistry { } kinds.host;
options.admins = gen.schema.mkInstanceRegistry { refs.home = config.hosts; } kinds.admin;
config.hosts.web-1 = { address = "10.0.0.2"; };
config.hosts.db-1 = { address = "10.0.0.1"; system = "aarch64-linux"; };
config.admins.deploy = { home = "web-1"; shell = "zsh"; };
}) ];
inherit (result.config) hosts admins;
in
{
db-1 = { inherit (hosts.db-1) address system id_hash; };
deploy = { inherit (admins.deploy) shell sudo id_hash; homeAddress = admins.deploy.home.address; };
}
Terminal window
$ nix eval --impure --json --file hosts-and-admins.nix
{
"db-1": {
"address": "10.0.0.1",
"id_hash": "host:cf1366d710f86decbddc7786f5a75f4fa7e5202941d13f32f168e4265681a629",
"system": "aarch64-linux"
},
"deploy": {
"homeAddress": "10.0.0.2",
"id_hash": "admin:8fdc148c005c84512c6ec67033ac62840f4f5156c06e2c6fa546437307b6bc79",
"shell": "zsh",
"sudo": true
}
}

A kind is a named set of typed options, like a NixOS submodule type with a name. A registry is an option holding the instances of one kind, keyed by name. Each instance gets its kind’s defaults (web-1.system, deploy.sudo) and an id_hash.

FeatureFunctionsPurpose
Declare kindsschema.evalSchema { specialArgs?; } modulesBuild a set of kinds from modules under config.schema.<kind>
Inheritinherits = [ "<kind>" ];Give a kind every option of another kind
Mix inmkMixin, applyMixin, composeMixinsAdd a reusable group of options to several kinds
Hold instancesmkInstanceRegistry { refs?; extraModules?; } kindAn option holding a kind’s instances, with defaults and id_hash
ValidatevalidateInstancesCheck every instance against its kind’s validators
Refine an optionschema.refined type [ schema.refinements.tcpPort ], checkRefinementsNarrow an option’s type with named predicates such as positive, nonEmpty and tcpPort
ReferencedeclarationOf "<kind>", mkFieldDeclaration, isFieldDeclarationAn option whose value is another instance, given by name or by value
DocumentrenderDocsRender a kind’s options as documentation
Mint identitiesidentity.hashIdentity kind labels valueOfA stable kind:digest string for any labelled value
Compare kindskindEqWhether two kind values are the same declaration
Kind lineageancestorsForKindA kind’s transitive ancestors, each once, every kind before its own ancestors
Comparable functionsalgebra.mkIntensional, conservativeEq, identityOfGive a function an identity so it can be compared

id_hash is the kind’s name, a colon, and a SHA-256 digest of the instance’s primitive option values together with the kind’s own identity. It follows from content:

  • Evaluating the same instance twice gives the same id_hash.
  • A host and a user with identical values have different hashes, because the kind is part of the digest.
  • Key order does not matter.

Compare instances by id_hash. Nix’s == on module values can recurse forever or fail, and a string comparison always terminates. An option marked identity = false is left out of the digest.

hashIdentity mints the same kind of identity for data outside a registry:

gen.identity.hashIdentity "host" [ "name" ] (label: { name = "db-1"; }.${label})
# "host:f7667c0783634b1119fef6262e89bfb5cd0b3628948abf41c2f1291ba75e6735"

Only inert data gets an identity: strings, booleans, integers, floats below 2^53 in magnitude, null, and lists and attribute sets of them. A function, a path or a derivation has no total encoding, so it gets no identity and the call fails, naming the kind and the label:

identity: a lambda in an identity position; kind "host", label "check"

The message never prints the rejected value. The encoder is private, so every library that mints an identity follows the same rules.

See Stable Identity for Everything for why gen gives every value an identity.

declarationOf "host" types an option whose value is a host. Bind it to a registry with refs.<option> on mkInstanceRegistry. The value can be written as the host’s name ("web-1") or as the host itself (config.hosts.web-1). Either way, reading it returns the registry entry.

A name with no entry fails with an error listing the names that exist:

gen-schema: ref field 'home' on kind 'admin': reference 'web-3' not found in instance registry (available: db-1, web-1)

Each reference is also an edge. The schema publishes _refEdges (for example { from = "user"; field = "home"; to = "host"; }) and _edges, which adds the inherits edges, so the kind graph can be queried with the functions on Graphs and Queries.

inherits copies options by kind name. A kind can list several parents, and a chain of parents resolves one level per pass. Each child has its own registry and its own identity tag, so an admin named root and a user named root are different instances.

gen-schema ships its own flake-parts module. It adds a schema option for kinds and passes the library to every module as genSchema. Kinds declared this way can use nixpkgs’ lib.types:

flake.nix
{
inputs = {
gen-schema.url = "github:sini/gen-schema";
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
outputs = inputs@{ flake-parts, ... }:
flake-parts.lib.mkFlake { inherit inputs; } ({ config, lib, genSchema, ... }: {
imports = [ inputs.gen-schema.flakeModules.default ];
options.hosts = genSchema.mkInstanceRegistry { } config.schema.host;
config = {
systems = [ ];
schema.host.options.address = lib.mkOption { type = lib.types.str; };
schema.host.options.role = lib.mkOption { type = lib.types.str; default = "worker"; };
hosts.web-1 = { address = "10.0.0.2"; role = "webserver"; };
hosts.db-1.address = "10.0.0.1";
flake.roles = lib.mapAttrs (_: host: host.role) config.hosts;
};
});
}
Terminal window
$ nix eval --json .#roles
{ "db-1": "worker", "web-1": "webserver" }

The module uses default settings. For custom strict, baseModule, collections or computed settings, call schema.mkSchemaOption { … } directly. To compose aspects and deliver hosts as well, use the gen hub’s flake module instead (see Delivery).


Provided by gen-schema (kinds, registries, references), gen-identity (hashIdentity) and gen-algebra (comparable functions).

palette
dark
light
↑↓ select↵ apply esc close

Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).