···11+# Transpose aspects.<aspect>.<class> to modules.<class>.<aspect>
22+# Resolves aspect dependencies and applies transformations during transposition
33+14lib: aspects:
25let
66+ # Import transpose utility with custom emit function for aspect resolution
37 transpose = import ./. { inherit lib emit; };
44- emit =
55- transposed:
66- let
77- aspect = aspects.${transposed.child};
88- in
99- [
1010- {
1111- inherit (transposed) parent child;
1212- value = aspect.resolve { class = transposed.parent; };
1313- }
1414- ];
88+99+ # Emit function: resolves each aspect for its target class
1010+ # Returns: [{ parent = class, child = aspect, value = resolved-module }]
1111+ emit = transposed: [
1212+ {
1313+ inherit (transposed) parent child;
1414+ value = aspects.${transposed.child}.resolve { class = transposed.parent; };
1515+ }
1616+ ];
1517in
1618{
1919+ # Exports: transposed.<class>.<aspect> = resolved-module
1720 transposed = transpose aspects;
1821}
+18
nix/default.nix
···11+# Generic 2-level attribute set transposition
22+# Swaps parent/child levels: { a.b = 1; } โ { b.a = 1; }
33+# Parameterized via emit function for custom value handling
44+15{
26 lib,
77+ # emit: Customization function for each item during transpose
88+ # Signature: { child, parent, value } โ [{ parent, child, value }]
99+ # Default: lib.singleton (identity transformation)
310 emit ? lib.singleton,
411}:
512let
1313+ # Create transposition metadata by calling emit
614 transposeItem =
715 child: parent: value:
816 emit { inherit child parent value; };
1717+1818+ # Fold accumulator: rebuilds transposed structure
919 accTransposed =
1020 acc: item:
1121 acc
···1424 ${item.child} = item.value;
1525 };
1626 };
2727+2828+ # Process all children of a parent
1729 transposeItems = parent: lib.mapAttrsToList (transposeItem parent);
3030+3131+ # Flatten input into transposition items
1832 deconstruct = lib.mapAttrsToList transposeItems;
3333+3434+ # Fold items back into swapped structure
1935 reconstruct = lib.foldl accTransposed { };
3636+3737+ # Main transpose: deconstruct โ flatten โ reconstruct
2038 transpose =
2139 attrs:
2240 lib.pipe attrs [
+7
nix/flakeModule.nix
···11+# Flake-parts integration for aspect-oriented configuration
22+# Provides flake.aspects (input) and flake.modules (output)
33+14{
25 lib,
36 config,
47 ...
58}:
99+# Invoke new() factory to create flake.aspects and flake.modules
610import ./new.nix lib (option: transposed: {
1111+ # User-facing aspects input
712 options.flake.aspects = option;
1313+1414+ # Computed modules output organized by class
815 config.flake.modules = transposed;
916}) config.flake.aspects
+11
nix/lib.nix
···11+# Public API entry point for flake-aspects library
22+# Exports: types, transpose, aspects, new, new-scope
13lib:
24let
55+ # Type system: aspectsType, aspectSubmodule, providerType
36 types = import ./types.nix lib;
77+88+ # Generic transposition utility: parameterized by emit function
49 transpose =
510 {
611 emit ? lib.singleton,
712 }:
813 import ./default.nix { inherit lib emit; };
1414+1515+ # Aspect transposition with resolution
916 aspects = import ./aspects.nix lib;
1717+1818+ # Low-level scope factory: parameterized by callback
1019 new = import ./new.nix lib;
2020+2121+ # High-level named scope factory
1122 new-scope = import ./new-scope.nix new;
1223in
1324{
+6-14
nix/new-scope.nix
···11-# usage:
22-#
33-# { inputs, ... }: {
44-# imports = [ (new-scope "foo") ];
55-# foo.aspects.<aspect> = ...;
66-# # and use foo.modules.<class>.<aspect>
77-# }
88-#
99-# returns a nix module that defines the ${name} option having:
1010-#
1111-# options.${name}.aspects # for user
1212-# options.${name}.modules # read-only resolved modules.
1313-#
1414-# for lower-level usage like using other option names, see new.nix.
11+# Creates named aspect scopes: ${name}.aspects and ${name}.modules
22+# Enables multiple independent aspect namespaces
153new: name:
164{ config, lib, ... }:
55+# Invoke new() to create ${name}.aspects and ${name}.modules
176new (option: transposed: {
187 options.${name} = {
88+ # User-facing aspects input
199 aspects = option;
1010+1111+ # Computed modules output (read-only)
2012 modules = lib.mkOption {
2113 readOnly = true;
2214 default = transposed;
+9-7
nix/new.nix
···11-# creates a new aspects option.
22-# See flakeModule for usage.
11+# Low-level aspect scope factory
22+# Creates aspect integration via callback pattern for maximum flexibility
33lib: cb: cfg:
44let
55+ # Import aspects transposer: validates and transposes aspect config
56 aspects = import ./aspects.nix lib cfg;
77+88+ # Import type system for aspect validation
69 types = import ./types.nix lib;
1010+1111+ # Create aspects input option
712 option = lib.mkOption {
813 default = { };
99- description = ''
1010- Attribute set of `<aspect>.<class>` modules.
1111-1212- Convenience transposition of `flake.modules.<class>.<aspect>`.
1313- '';
1414+ description = "Aspect definitions organized as <aspect>.<class>";
1415 type = types.aspectsType;
1516 };
1617in
1818+# Invoke callback with option and transposed results
1719cb option aspects.transposed
+5-1
nix/resolve.nix
···11+# Core aspect resolution algorithm
22+# Resolves aspect definitions into nixpkgs modules with dependency resolution
33+14lib:
25let
33-66+ # Process a single provider: invoke with context and resolve
47 include =
58 class: aspect-chain: provider:
69 let
···811 in
912 resolve class aspect-chain provided;
10131414+ # Main resolution: extract class config and recursively resolve includes
1115 resolve = class: aspect-chain: provided: {
1216 imports = lib.flatten [
1317 (provided.${class} or { })
+105-220
nix/types.nix
···11+# Core type system for aspect-oriented configuration
22+13lib:
24let
33- # Import the resolve function which handles aspect resolution and dependency injection
45 resolve = import ./resolve.nix lib;
5666- # Top-level aspects container type
77- # This is the entry point for defining all aspects in a flake
88- # Structure: aspects.<aspectName> = { ... }
99- # Makes the entire aspects config available as 'aspects' in module args
1010- # allowing cross-referencing between aspects
1111- aspectsType = lib.types.submodule (
1212- { config, ... }:
1313- {
1414- # Allow arbitrary aspect definitions as attributes
1515- # Each aspect can be either:
1616- # - An aspect submodule (aspectSubmoduleAttrs)
1717- # - A provider function (providerType)
1818- freeformType = lib.types.lazyAttrsOf (lib.types.either aspectSubmoduleAttrs providerType);
1919- # Inject the aspects config into _module.args for cross-referencing
2020- config._module.args.aspects = config;
2121- }
2222- );
2323-2424- # Type checker for provider functions with specific argument patterns
2525- # Valid provider function signatures:
2626- # 1. { class } => aspect-object
2727- # 2. { aspect-chain } => aspect-object
2828- # 3. { class, aspect-chain } => aspect-object
2929- # 4. { class, ... } => aspect-object (with other ignored args)
3030- # 5. { aspect-chain, ... } => aspect-object (with other ignored args)
3131- #
3232- # This ensures that provider functions receive the proper context when invoked
3333- functionToAspect = lib.types.addCheck (lib.types.functionTo aspectSubmodule) (
3434- f:
3535- let
3636- args = lib.functionArgs f;
3737- arity = lib.length (lib.attrNames args);
3838- hasClass = args ? class;
3939- hasChain = args ? aspect-chain;
4040- classOnly = hasClass && arity == 1;
4141- chainOnly = hasChain && arity == 1;
4242- both = hasClass && hasChain && arity == 2;
4343- in
4444- classOnly || chainOnly || both
4545- );
4646-4747- # Provider functions can be:
4848- # 1. A function taking { class, aspect-chain } and returning an aspect (functionToAspect)
4949- # 2. A function taking parameters and returning another provider (curried)
5050- # This allows for parametric aspects and lazy evaluation
5151- functionProviderType = lib.types.either functionToAspect (lib.types.functionTo providerType);
5252-5353- # Provider type allows three forms:
5454- # 1. A function provider (functionProviderType)
5555- # 2. An aspect configuration (aspectSubmodule)
5656- # This enables both immediate aspect definitions and deferred/parametric ones
5757- providerType = lib.types.either functionProviderType aspectSubmodule;
5858-5959- # Additional validation for aspect submodules to ensure they're not mistyped functions
6060- # An aspectSubmoduleAttrs is either:
6161- # - Not a function at all (plain attribute set)
6262- # - A function with submodule-style arguments (lib, config, options, aspect)
6363- # This prevents accidentally treating provider functions as aspect configs
6464- aspectSubmoduleAttrs = lib.types.addCheck aspectSubmodule (
6565- m: (!builtins.isFunction m) || (isAspectSubmoduleFn m)
6666- );
6767-6868- # Helper to identify if a function is a submodule-style function
6969- # Submodule functions take args like { lib, config, options, aspect, ... }
7070- # Returns true if the function accepts at least one of these special args
7171- isAspectSubmoduleFn =
7272- m:
7373- lib.pipe m [
7474- lib.functionArgs
7575- lib.attrNames
7676- (lib.intersectLists [
7777- "lib"
7878- "config"
7979- "options"
8080- "aspect"
8181- ])
8282- (x: lib.length x > 0)
8383- ];
8484-8585- # Special type that accepts any value but always merges to null
8686- # Used for internal computed values that shouldn't be serialized
8787- # This prevents type errors when values don't have proper types
77+ # Type for computed values that only exist during evaluation
888 ignoredType = lib.types.mkOptionType {
899 name = "ignored type";
9010 description = "ignored values";
···9212 check = _: true;
9313 };
94149595- # Core aspect definition type
9696- # Each aspect represents a reusable configuration module that can:
9797- # - Define configuration for multiple "classes" (e.g., nixos, home-manager, darwin)
9898- # - Include other aspects as dependencies
9999- # - Provide sub-aspects for selective composition
100100- # - Be parametrized via __functor
101101- aspectSubmodule = lib.types.submodule (
102102- {
103103- name,
104104- config,
105105- ...
106106- }:
107107- {
108108- # Allow arbitrary class configurations (e.g., nixos, home-manager, etc.)
109109- # Each class maps to a deferred module that will be resolved later
110110- freeformType = lib.types.attrsOf lib.types.deferredModule;
111111-112112- # Make the aspect config available as 'aspect' in module args
113113- # This allows modules within the aspect to reference their own aspect
114114- config._module.args.aspect = config;
1515+ # Create internal read-only option with custom apply function
1616+ mkInternal =
1717+ desc: type: fn:
1818+ lib.mkOption {
1919+ internal = true;
2020+ visible = false;
2121+ readOnly = true;
2222+ description = desc;
2323+ inherit type;
2424+ apply = fn;
2525+ };
2626+2727+ # Check if function has submodule-style arguments
2828+ isSubmoduleFn =
2929+ m:
3030+ lib.length (
3131+ lib.intersectLists [ "lib" "config" "options" "aspect" ] (lib.attrNames (lib.functionArgs m))
3232+ ) > 0;
11533116116- # Create "_" as a shorthand alias for "provides"
117117- # Allows writing: aspect._.foo instead of aspect.provides.foo
118118- # This improves ergonomics for the common case of defining sub-aspects
119119- imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ];
3434+ # Check if function accepts { class } and/or { aspect-chain }
3535+ isProviderFn =
3636+ f:
3737+ let
3838+ args = lib.functionArgs f;
3939+ n = lib.length (lib.attrNames args);
4040+ in
4141+ (args ? class && n == 1)
4242+ || (args ? aspect-chain && n == 1)
4343+ || (args ? class && args ? aspect-chain && n == 2);
12044121121- # Human-readable aspect name, defaults to the attribute name
122122- # Used in aspect-chain tracking and for display purposes
123123- options.name = lib.mkOption {
124124- description = "Aspect name";
125125- default = name;
126126- type = lib.types.str;
127127- };
4545+ # Direct provider function: ({ class, aspect-chain }) โ aspect
4646+ directProviderFn = lib.types.addCheck (lib.types.functionTo aspectSubmodule) isProviderFn;
12847129129- # Optional description for documentation purposes
130130- # Defaults to a generic description using the aspect name
131131- options.description = lib.mkOption {
132132- description = "Aspect description";
133133- default = "Aspect ${name}";
134134- type = lib.types.str;
135135- };
4848+ # Curried provider function: (params) โ provider (enables parametrization)
4949+ curriedProviderFn = lib.types.functionTo providerType;
13650137137- # Dependencies: list of other providers this aspect includes
138138- # During resolution, included aspects are merged with this aspect
139139- # Includes can be:
140140- # - Direct aspect references: aspects.otherAspect
141141- # - Parametrized providers: aspects.other.provides.foo "param"
142142- # - Functorized aspects: aspects.otherAspect { param = value; }
143143- # The resolution order matters for module merging semantics
144144- options.includes = lib.mkOption {
145145- description = "Providers to ask aspects from";
146146- type = lib.types.listOf providerType;
147147- default = [ ];
148148- };
5151+ # Any provider function: direct or curried
5252+ providerFn = lib.types.either directProviderFn curriedProviderFn;
14953150150- # Sub-aspects that can be selectively included by other aspects
151151- # This allows aspects to expose multiple named variants or components
152152- # Creates a fixpoint where provides can reference the aspects in their scope
153153- # The provides scope gets its own 'aspects' arg for internal cross-referencing
154154- options.provides = lib.mkOption {
155155- description = "Providers of aspect for other aspects";
156156- default = { };
157157- type = lib.types.submodule (
158158- { config, ... }:
159159- {
160160- # Allow arbitrary sub-aspect definitions
161161- freeformType = lib.types.attrsOf providerType;
162162- # Make the provides scope available as 'aspects' for fixpoint references
163163- # This enables provides.foo to reference provides.bar via aspects.bar
164164- config._module.args.aspects = config;
165165- }
166166- );
167167- };
5454+ # Provider type: function or aspect that can provide configurations
5555+ providerType = lib.types.either providerFn aspectSubmodule;
16856169169- # Functor enables aspects to be callable like functions
170170- # When defined, calling aspect { param = value; } invokes the functor
171171- # The functor receives:
172172- # 1. The aspect config itself
173173- # 2. The parameters passed by the caller (which must include class and aspect-chain)
174174- # This allows aspects to be parametrized and context-aware
175175- #
176176- # The default functor:
177177- # - Takes the aspect config
178178- # - Takes { class, aspect-chain } parameters
179179- # - Returns the aspect unchanged (identity function with parameter access)
180180- # - The weird `if true || (class aspect-chain) then` is to silence nixf-diagnose
181181- # about unused variables while ensuring they're in scope
182182- options.__functor = lib.mkOption {
183183- internal = true;
184184- visible = false;
185185- description = "Functor to default provider";
186186- type = lib.types.functionTo providerType;
187187- default =
188188- aspect:
189189- { class, aspect-chain }:
190190- # silence nixf-diagnose about unused variables
191191- if true || (class aspect-chain) then aspect else aspect;
192192- };
5757+ # Core aspect submodule with all aspect properties
5858+ aspectSubmodule = lib.types.submodule (
5959+ { name, config, ... }:
6060+ {
6161+ freeformType = lib.types.lazyAttrsOf lib.types.deferredModule;
6262+ config._module.args.aspect = config;
6363+ imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ];
19364194194- # Convenience accessor: aspect.modules.<class> automatically resolves
195195- # This is equivalent to calling aspect.resolve { class = "<class>"; }
196196- # Returns a map of all classes with their resolved modules
197197- #
198198- # For example: aspect.modules.nixos == aspect.resolve { class = "nixos"; }
199199- #
200200- # This is computed lazily and uses ignoredType to avoid serialization issues
201201- options.modules = lib.mkOption {
202202- internal = true;
203203- visible = false;
204204- readOnly = true;
205205- description = "resolved modules from this aspect";
206206- type = ignoredType;
207207- # For each class in the aspect, resolve it with empty aspect-chain
208208- apply = _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config;
209209- };
6565+ options = {
6666+ name = lib.mkOption {
6767+ description = "Aspect name";
6868+ default = name;
6969+ type = lib.types.str;
7070+ };
7171+7272+ description = lib.mkOption {
7373+ description = "Aspect description";
7474+ default = "Aspect ${name}";
7575+ type = lib.types.str;
7676+ };
7777+7878+ includes = lib.mkOption {
7979+ description = "Providers to ask aspects from";
8080+ type = lib.types.listOf providerType;
8181+ default = [ ];
8282+ };
8383+8484+ provides = lib.mkOption {
8585+ description = "Providers of aspect for other aspects";
8686+ default = { };
8787+ type = lib.types.submodule (
8888+ { config, ... }:
8989+ {
9090+ freeformType = lib.types.lazyAttrsOf providerType;
9191+ config._module.args.aspects = config;
9292+ }
9393+ );
9494+ };
9595+9696+ __functor = lib.mkOption {
9797+ internal = true;
9898+ visible = false;
9999+ description = "Functor to default provider";
100100+ type = lib.types.functionTo providerType;
101101+ default = aspect: { class, aspect-chain }: if true || (class aspect-chain) then aspect else aspect;
102102+ };
103103+104104+ modules = mkInternal "resolved modules from this aspect" ignoredType (
105105+ _: lib.mapAttrs (class: _: config.resolve { inherit class; }) config
106106+ );
210107211211- # Main resolution function that converts an aspect into a nixpkgs module
212212- # Takes { class, aspect-chain } and returns a resolved module
213213- # - class: The target configuration class (e.g., "nixos", "home-manager")
214214- # - aspect-chain: List of aspects traversed so far (for tracking dependencies)
215215- #
216216- # The resolution process:
217217- # 1. Invokes the aspect config with class and aspect-chain parameters
218218- # This triggers the __functor if defined, allowing parametrization
219219- # 2. Calls resolve.nix to recursively resolve includes
220220- # 3. Returns a module with imports from the aspect and its dependencies
221221- #
222222- # The aspect-chain parameter allows aspects to introspect their dependency tree
223223- # This is useful for debugging and for aspects that need to know their context
224224- options.resolve = lib.mkOption {
225225- internal = true;
226226- visible = false;
227227- readOnly = true;
228228- description = "function to resolve a module from this aspect";
229229- type = ignoredType;
230230- apply =
108108+ resolve = mkInternal "function to resolve a module from this aspect" ignoredType (
231109 _:
232110 {
233111 class,
234112 aspect-chain ? [ ],
235113 }:
236236- # Invoke config (the aspect) with class and aspect-chain parameters
237237- # This works because config is wrapped with __functor via the submodule system
238238- # Then pass the result to resolve for dependency resolution
239114 resolve class aspect-chain (config {
240115 inherit class aspect-chain;
241241- });
116116+ })
117117+ );
242118 };
243119 }
244120 );
245121246122in
247123{
248248- inherit
249249- aspectsType # Main entry point for flake.aspects
250250- aspectSubmodule # Individual aspect definition type
251251- providerType # Type for provider expressions
252252- ;
124124+ # Top-level aspects container with fixpoint semantics
125125+ aspectsType = lib.types.submodule (
126126+ { config, ... }:
127127+ {
128128+ freeformType = lib.types.lazyAttrsOf (
129129+ lib.types.either (lib.types.addCheck aspectSubmodule (
130130+ m: (!builtins.isFunction m) || isSubmoduleFn m
131131+ )) providerType
132132+ );
133133+ config._module.args.aspects = config;
134134+ }
135135+ );
136136+137137+ inherit aspectSubmodule providerType;
253138}