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.
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;}$ 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.
What you can do
Section titled “What you can do”| Feature | Functions | Purpose |
|---|---|---|
| Declare aspects | mkAspectSchema cnf, then .mkAspectModule { } or .mkAspectOption { } | Add an aspects option whose entries are typed aspects |
| Classify keys | cnf.keySemantics.<key>.category, keyCategory | Say whether a key is a class, a channel or a facet |
| Include | includes = [ "ssh" ], keyRef | Reference other aspects by name, by value, or in another flake |
| Identify | key, aspectPath, pathKey | The path-based identity of an aspect, such as hardware/cpu/intel |
| Guard | guard, pred, mkGuardVocab, applyGuard | Include content only when a condition holds, with the condition and the body written as first-order terms |
| Read the graph | graphFacts cnf aspects, flatten | Nodes, includesOf, parentOf and node data as plain attribute sets |
| Federate | link.link { wire?; } sources, normalize, disjointUnion | Combine aspect graphs from several flakes, keeping each node’s origin |
| Ports | providesOf, requiresOf, checkCapability | What an aspect graph offers and what it needs filled in |
Details
Section titled “Details”Key categories
Section titled “Key categories”Every key on an aspect other than name, includes and the other structural keys is classified by
cnf.keySemantics:
| Category | What the value is | Example |
|---|---|---|
class | a module for one target, null when unset | nixos, darwin, homeManager, terranix, or a name of your own |
channel | a raw value passed through unchanged | settings handed to another framework |
facet | a typed field declared by its own option | an 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.
What graphFacts returns
Section titled “What graphFacts returns”| Field | Contents |
|---|---|
nodes | every 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 |
parentOf | each aspect’s enclosing aspect, null for a top-level one |
includesOf | the includes that resolve to aspects in this set, and an edge to each anonymous declaration’s node |
foreignIncludesOf | keyRef references to aspects in another flake, left for federation to resolve |
unresolvedIncludesOf | positions in includes holding inline content that is no node: content past the depth budget, a guard record, a function, a deferred member |
nodeData | each 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 registryIncludes and delivery
Section titled “Includes and delivery”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.
Guards
Section titled “Guards”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.
Federation
Section titled “Federation”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).
Palettes adapted from Catppuccin (Macchiato) (MIT), Tokyo Night (Apache-2.0), gruvbox (MIT), Catppuccin (Latte) (MIT), Rosé Pine (Dawn) (MIT).