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.
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; };}$ 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.
What you can do
Section titled “What you can do”| Feature | Functions | Purpose |
|---|---|---|
| Declare kinds | schema.evalSchema { specialArgs?; } modules | Build a set of kinds from modules under config.schema.<kind> |
| Inherit | inherits = [ "<kind>" ]; | Give a kind every option of another kind |
| Mix in | mkMixin, applyMixin, composeMixins | Add a reusable group of options to several kinds |
| Hold instances | mkInstanceRegistry { refs?; extraModules?; } kind | An option holding a kind’s instances, with defaults and id_hash |
| Validate | validateInstances | Check every instance against its kind’s validators |
| Refine an option | schema.refined type [ schema.refinements.tcpPort ], checkRefinements | Narrow an option’s type with named predicates such as positive, nonEmpty and tcpPort |
| Reference | declarationOf "<kind>", mkFieldDeclaration, isFieldDeclaration | An option whose value is another instance, given by name or by value |
| Document | renderDocs | Render a kind’s options as documentation |
| Mint identities | identity.hashIdentity kind labels valueOf | A stable kind:digest string for any labelled value |
| Compare kinds | kindEq | Whether two kind values are the same declaration |
| Kind lineage | ancestorsForKind | A kind’s transitive ancestors, each once, every kind before its own ancestors |
| Comparable functions | algebra.mkIntensional, conservativeEq, identityOf | Give a function an identity so it can be compared |
Details
Section titled “Details”Identity
Section titled “Identity”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
hostand auserwith 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.
References
Section titled “References”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.
Inheritance
Section titled “Inheritance”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.
Using gen-schema in a flake-parts flake
Section titled “Using gen-schema in a flake-parts flake”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:
{ 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; }; });}$ 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).
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).