skip to content

Aspects

A web server needs nginx, monitoring and SSH, and monitoring itself needs SSH. Each feature is an aspect, and each aspect carries content for one or more classes, here nixos and terranix.

webserver-aspects.nix
let
gen = (builtins.getFlake "github:sini/gen").lib.mkGenLibs { };
inherit (gen.merge) evalModuleTree mkOption types;
cnf = {
keySemantics.nixos.category = "class";
keySemantics.terranix.category = "class";
};
result = evalModuleTree { } [
((gen.aspects.mkAspectSchema cnf).mkAspectModule { })
{ options.hosts = mkOption { type = types.attrsOf types.raw; default = { }; }; }
{
aspects.ssh.nixos.services.openssh.enable = true;
aspects.monitoring.includes = [ "ssh" ];
aspects.monitoring.nixos.services.prometheus.enable = true;
aspects.webserver.includes = [ "monitoring" ];
aspects.webserver.nixos.services.nginx.enable = true;
aspects.webserver.terranix.resource.aws_security_group_rule.https.to_port = 443;
hosts.web-1.aspects = [ "webserver" ];
}
];
facts = gen.aspects.graphFacts cnf result.config.aspects;
in
{
includesOf = facts.includesOf;
webserverNeeds = gen.graph.reachableFrom { edges = id: facts.includesOf.${id} or [ ]; } "webserver";
key = result.config.aspects.webserver.key;
}
Terminal window
$ nix eval --impure --json --file webserver-aspects.nix
{
"includesOf": { "monitoring": ["ssh"], "ssh": [], "webserver": ["monitoring"] },
"key": "webserver",
"webserverNeeds": ["monitoring", "ssh"]
}

An aspect is a named bundle of configuration for one feature. It holds content per class, a list of other aspects it includes, and an identity. graphFacts turns a set of aspects into plain graph data, so every query on Graphs and Queries runs over it.

FeatureFunctionsPurpose
Declare aspectsmkAspectSchema cnf, then .mkAspectModule { } or .mkAspectOption { }Add an aspects option whose entries are typed aspects
Classify keyscnf.keySemantics.<key>.category, keyCategorySay whether a key is a class, a channel or a facet
Includeincludes = [ "ssh" ], keyRefReference other aspects by name, by value, or in another flake
Identifykey, aspectPath, pathKeyThe path-based identity of an aspect, such as hardware/cpu/intel
Guardguard, pred, mkGuardVocab, applyGuardInclude content only when a condition holds, with the condition and the body written as first-order terms
Read the graphgraphFacts cnf aspects, flattenNodes, includesOf, parentOf and node data as plain attribute sets
Federatelink.link { wire?; } sources, normalize, disjointUnionCombine aspect graphs from several flakes, keeping each node’s origin
PortsprovidesOf, requiresOf, checkCapabilityWhat an aspect graph offers and what it needs filled in

Every key on an aspect other than name, includes and the other structural keys is classified by cnf.keySemantics:

CategoryWhat the value isExample
classa module for one target, null when unsetnixos, darwin, homeManager, terranix, or a name of your own
channela raw value passed through unchangedsettings handed to another framework
faceta typed field declared by its own optionan aspect’s port number

A key with no entry is a nested aspect with its own identity. aspects.hardware.cpu.intel is the aspect with key hardware/cpu/intel. Two aspects share a key only when they sit at the same path.

FieldContents
nodesevery aspect’s id, and every anonymous declaration’s: inline content written at an include position, keyed by where it was declared; named inline content is one node per class of its sightings, so one value included at two places is one node
parentOfeach aspect’s enclosing aspect, null for a top-level one
includesOfthe includes that resolve to aspects in this set, and an edge to each anonymous declaration’s node
foreignIncludesOfkeyRef references to aspects in another flake, left for federation to resolve
unresolvedIncludesOfpositions in includes holding inline content that is no node: content past the depth budget, a guard record, a function, a deferred member
nodeDataeach aspect’s value

A name in includes that matches no aspect fails with an error naming the aspect and the position:

gen-aspects.includes (aspect 'webserver', include position 0): reference 'sshd' names no entry of the registry

includes states edges between aspects. A consumer that wants a host to receive the content of every included aspect computes the include closure, as webserverNeeds does above. Delivery shows the full route from aspects to a built host.

A guard includes content only in contexts that satisfy a predicate. gen-aspects has two forms. A function aspect such as { host, ... }: { nixos = ...; } stays unevaluated until a host is known. A guard record built from the mkGuardVocab vocabulary is plain data: its condition and its body are terms of gen-algebra’s first-order algebra, checked against the declared names, applyGuard evaluates it, and guardKey gives it an identity minted over the two, so two identical guards count as one.

link takes aspect graphs from several flakes and joins them into one graph. Each node keeps an origin label, so two flakes can both define ssh without a collision. wire fills a flake’s declared holes with nodes from another flake. The result includes a manifest of every resolution, which can be diffed like a lock file.

For the idea behind aspects, see Reusable Features. Guard records follow Definitional Interpreters: each predicate is first-order data read by one interpreter, applyGuard.


Provided by gen-aspects (aspects, key categories, guards, graph facts) and gen-link (federation).

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).