fake.modules transposition for aspect-oriented Dendritic Nix. with cross-aspect dependencies. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic nix aspect oriented

expose function for creating aspects option. (#10)

authored by oeiuwq.com and committed by

GitHub a0940b81 0fb045fa

+86 -57
+41 -32
README.md
··· 75 75 76 76 ## Usage 77 77 78 + The library can be used in two ways: as a dependency-free utility or as a `flake-parts` module. 79 + 78 80 ### As a Dependency-Free Library (`./nix/default.nix`) 79 81 80 - The [`transpose`](nix/default.nix) library accepts an optional `emit` function that can be used to ignore items, modify them, or generate multiple items from a single input. 82 + The core of this project is the [`transpose`](nix/default.nix) function, which is powerful enough to implement cross-aspect dependencies for any Nix configuration class. It accepts an optional `emit` function that can be used to ignore items, modify them, or generate multiple items from a single input. 81 83 82 84 ```nix 83 85 let transpose = import ./nix/default.nix { lib = pkgs.lib; }; in 84 86 transpose { a.b.c = 1; } # => { b.a.c = 1; } 85 87 ``` 86 88 87 - This `emit` function is utilized by the [`aspects`](nix/aspects.nix) library (both libraries are independent of flakes) to manage cross-aspect, same-class module dependencies. 89 + This `emit` function is utilized by the [`aspects`](nix/aspects.nix) library to manage module dependencies between different aspects of the same class. Both `transpose` and `aspects` are independent of flakes. 88 90 89 91 ### As a Dendritic Flake-Parts Module (`flake.aspects` option) 90 92 91 - The `flake.aspects` option is transposed into `flake.modules`. 93 + When used as a `flake-parts` module, the `flake.aspects` option is automatically transposed into `flake.modules`, making the modules available to other parts of your flake. 92 94 93 95 ```nix 94 96 # The code in this example can (and should) be split into different Dendritic modules. ··· 124 126 125 127 #### Declaring Cross-Aspect Dependencies 126 128 127 - `flake.aspects` also allows aspects to declare dependencies among themselves. 129 + Aspects can declare dependencies on other aspects using the `includes` attribute. This allows you to compose configurations in a modular way. 128 130 129 - Each module can have its own `imports`, but aspect dependencies are defined at the aspect level, not the module level. Dependencies are eventually resolved to modules and are imported only if they exist. 131 + Dependencies are defined at the aspect level, not within individual modules. When a module from an aspect is evaluated (e.g., `flake.modules.nixos.development-server`), the library resolves all dependencies for the `nixos` class and imports the corresponding modules if they exist. 130 132 131 - In the example below, the `development-server` aspect can be applied to both Linux and macOS hosts. Note that `alice` uses `nixos` + `homeManager`, while `bob` uses `darwin` + `hjem`. 132 - 133 - The `development-server` aspect addresses a usability concern by configuring the same development environment on different operating systems. When applied to a NixOS machine, the `alice.nixos` module will likely configure the `alice` user; there is no corresponding NixOS user for `bob`. 133 + In the example below, the `development-server` aspect includes the `alice` and `bob` aspects. This demonstrates how to create a consistent development environment across different operating systems and user configurations. 134 134 135 135 ```nix 136 136 { 137 137 flake.aspects = { aspects, ... }: { 138 138 development-server = { 139 + # This aspect now includes modules from 'alice' and 'bob'. 139 140 includes = with aspects; [ alice bob ]; 140 141 141 - # Without flake-aspects, you would normally do: 142 + # Without flake-aspects, you would have to do this manually for each class. 142 143 # nixos.imports = [ inputs.self.modules.nixos.alice ]; 143 144 # darwin.imports = [ inputs.self.modules.darwin.bob ]; 144 145 }; 145 146 146 147 alice = { 147 148 nixos = {}; 149 + homeManager = {}; 148 150 }; 149 151 150 152 bob = { 151 153 darwin = {}; 154 + hjem = {}; 152 155 }; 153 156 }; 154 157 } 155 158 ``` 156 159 157 - Creating OS configurations is outside the scope of this library - for that, see [`vic/den`](https://github.com/vic/den) -. Exposing os configurations might look like this: 160 + Creating the final OS configurations is outside the scope of this library—for that, see [`vic/den`](https://github.com/vic/den). However, exposing them would look like this: 158 161 159 162 ```nix 160 163 { inputs, ... }: ··· 171 174 } 172 175 ``` 173 176 174 - #### Advanced Aspect Dependencies 177 + ### Advanced Aspect Dependencies: Providers 175 178 176 - An aspect can declare a `includes` list: 179 + Dependencies are managed through a powerful abstraction called **providers**. A provider is a value that returns an aspect object, which can then supply modules to the aspect that includes it. 180 + 181 + A provider can be either a static aspect object or a function that dynamically returns one. This mechanism enables sophisticated dependency chains, conditional logic, and parameterization. 182 + 183 + #### The Default Provider: `provides.itself` 184 + 185 + Every aspect automatically has a default provider called `itself`, located at `<aspect>.provides.itself`. This provider simply returns the aspect itself. 186 + 187 + The `with aspects; [ bar baz ]` syntax is a convenient shorthand that relies on this default: 177 188 178 189 ```nix 179 190 # A 'foo' aspect that depends on 'bar' and 'baz' aspects. 180 191 flake.aspects = { aspects, ... }: { 181 - foo.includes = [ aspects.bar aspects.baz ]; 192 + foo.includes = with aspects; [ bar baz ]; 193 + # This is equivalent to: 194 + # foo.includes = [ aspects.bar.provides.itself aspects.baz.provides.itself ]; 182 195 } 183 196 ``` 184 197 185 - Cross-aspect dependencies work as follows: 186 - 187 - When a module like `flake.modules.nixos.foo` is requested (for example, included in a `nixosConfiguration`), a corresponding module is computed from `flake.aspects.foo.nixos`. 188 - 189 - `flake.aspects.foo.includes` is a list of functions (providers). A **provider** is either a constant `<aspect-object>` or a function `{ class, aspect-chain } => <aspect-object>`. They are called with `{ aspect-chain = [ aspects.foo ]; class = "nixos" }`, functions can inspect the chain of aspects that have led to the call. These providers return an aspect object that contains a module of the same `class` (in this case, `nixos`). Providers allow us to have a tree of aspects where each provided aspect can be either static or parametric. 190 - 191 - Providers answer the question: given we have `nixos` modules from `[foo]` aspects, what other aspects can provide `nixos` modules that need to be imported? 198 + #### Custom Providers 192 199 193 - This means that the included aspect determines which configuration its caller should use. 200 + You can define custom providers to implement more complex logic. A provider function receives the current `class` (e.g., `"nixos"`) and the `aspect-chain` (the list of aspects that led to the call). This allows a provider to act as a conditional proxy or router for dependencies. 194 201 195 - By default, all aspects have an `<aspect>.provides.itself` provider function that always returns the `<aspect>` itself. This is why `with aspects; [ bar baz ]` works: it is shorthand for `[ aspects.bar.provides.itself aspects.baz.provides.itself ]`. It is possible to override the default provider, by setting `__functor`, see how [test](https://github.com/vic/flake-aspects/blob/4f88b4ecefbe46ccfa5d9cfa11451a88be169a70/checkmate.nix#L270) or [vic/den](https://github.com/vic/den/blob/def1c396e7ce884578d6589391cca8a4c6a650d3/nix/aspects-config.nix#L55) do it. 196 - 197 - ##### Dynamic modules using provider function's `{ aspect-chain, class }` argument. 198 - 199 - You can also define custom providers that inspect the `aspect-chain` and `class` values and return a set of modules accordingly. This allows providers to act as conditional proxies or routers for dependencies. 202 + In this example, the `kde-desktop` aspect defines a custom `karousel` provider that only returns a module if certain conditions are met: 200 203 201 204 ```nix 202 205 flake.aspects.kde-desktop.provides.karousel = { aspect-chain, class }: ··· 211 214 } 212 215 ``` 213 216 214 - ##### Parametrized modules from providers. 217 + This pattern allows an included aspect to determine which configuration its caller should use, enabling a tree of dependencies where each node can be either static or parametric. 218 + 219 + #### Parameterized Providers 215 220 216 - Providers can be curried (but *must* use explicit argument names). This lets you have 217 - modules parametrized by some values, outside the aspect scope. For a real-world 218 - usage of this feature, see how `vic/den` [defines](https://github.com/vic/den/blob/def1c396e7ce884578d6589391cca8a4c6a650d3/nix/aspects-config.nix#L40) `flake.aspects.default.host` and their [use](https://github.com/vic/den/blob/def1c396e7ce884578d6589391cca8a4c6a650d3/templates/default/modules/_example/aspects.nix#L32). 221 + Providers can be implemented as curried functions, allowing you to create parameterized modules. All arguments must be explicitly named. This is useful for creating reusable configurations that can be customized at the inclusion site. 222 + 223 + For a real-world example, see how `vic/den` [defines](https://github.com/vic/den/blob/def1c396e7ce884578d6589391cca8a4c6a650d3/nix/aspects-config.nix#L40) `flake.aspects.default.host` and its [usage](https://github.com/vic/den/blob/def1c396e7ce884578d6589391cca8a4c6a650d3/templates/default/modules/_example/aspects.nix#L32). 219 224 220 225 ```nix 221 226 flake.aspects = { aspects, ... }: { ··· 224 229 provides.user = { userName }: { aspect-chain, class }: { 225 230 darwin.system.primaryUser = userName; 226 231 nixos.users.${userName}.isNormalUser = true; 227 - } 232 + }; 228 233 }; 229 234 230 235 home-server.includes = [ ··· 234 239 } 235 240 ``` 236 241 237 - See `aspects."test provides"` [checkmate tests](checkmate.nix) for more examples on chained providers. 242 + See the `aspects."test provides"` and `aspects."test provides using fixpoints"` sections in the [checkmate tests](checkmate.nix) for more examples of chained providers. 243 + 244 + #### The `_` Alias for `provides` 245 + 246 + For convenience, `_` is an alias for `provides`. This allows for more concise chaining of providers. For example, `foo.provides.bar.provides.baz` can be written as `foo._.bar._.baz`. 238 247 239 248 ## Testing 240 249
+9 -1
checkmate.nix
··· 163 163 { 164 164 name = "aspectTwo.foo"; 165 165 description = "aspectTwo foo provided"; 166 - includes = [ aspects.aspectThree.provides.moo ]; 166 + includes = [ 167 + aspects.aspectThree.provides.moo 168 + aspects.aspectTwo.provides.baz 169 + ]; 167 170 classOne.bar = [ "two:${class}:${lib.concatStringsSep "/" (lib.map (x: x.name) aspect-chain)}" ]; 168 171 classTwo.bar = [ "foo class two not included" ]; 169 172 }; ··· 171 174 provides.bar = { 172 175 # classOne is missing on bar 173 176 classTwo.bar = [ "bar class two not included" ]; 177 + }; 178 + # _ is an shortcut alias of provides. 179 + _.baz = { 180 + # classOne is missing on bar 181 + classTwo.bar = [ "baz" ]; 174 182 }; 175 183 }; 176 184 aspectThree.provides.moo =
+1
flake.nix
··· 3 3 __functor = _: import ./nix; 4 4 flakeModule = ./nix/flakeModule.nix; 5 5 lib.types = import ./nix/types.nix; 6 + lib.newAspects = import ./nix/new.nix; 6 7 }; 7 8 }
+6 -17
nix/flakeModule.nix
··· 4 4 ... 5 5 }: 6 6 let 7 - aspects = import ./aspects.nix lib config.flake.aspects; 8 - types = import ./types.nix lib; 7 + newAspects = import ./new.nix lib; 8 + mod = newAspects (option: transposed: { 9 + options.flake.aspects = option; 10 + config.flake.modules = transposed; 11 + }) config.flake.aspects; 9 12 in 10 13 { 11 - options.flake.aspects = lib.mkOption { 12 - default = { }; 13 - description = '' 14 - Attribute set of `<aspect>.<class>` modules. 15 - 16 - Convenience transposition of `flake.modules.<class>.<aspect>`. 17 - ''; 18 - type = types.aspectsType; 19 - }; 20 - config.flake.aspects = 21 - { config, ... }: 22 - { 23 - _module.args.aspects = config; 24 - }; 25 - config.flake.modules = aspects.transposed; 14 + imports = [ mod ]; 26 15 }
+17
nix/new.nix
··· 1 + # creates a new aspects option. 2 + # See flakeModule for usage. 3 + lib: cb: cfg: 4 + let 5 + aspects = import ./aspects.nix lib cfg; 6 + types = import ./types.nix lib; 7 + option = lib.mkOption { 8 + default = { }; 9 + description = '' 10 + Attribute set of `<aspect>.<class>` modules. 11 + 12 + Convenience transposition of `flake.modules.<class>.<aspect>`. 13 + ''; 14 + type = types.aspectsType; 15 + }; 16 + in 17 + cb option aspects.transposed
+12 -7
nix/types.nix
··· 1 1 lib: 2 2 let 3 3 4 - aspectsType = lib.types.submodule { 5 - freeformType = lib.types.attrsOf aspectSubmodule; 6 - }; 4 + aspectsType = lib.types.submodule ( 5 + { config, ... }: 6 + { 7 + freeformType = lib.types.attrsOf aspectSubmodule; 8 + config._module.args.aspects = config; 9 + } 10 + ); 7 11 8 12 # checks the argument names to be those of a provider function: 9 13 # 10 14 # { class, aspect-chain } => aspect-object 11 - # { _class, ... } => aspect-object 12 - # { _aspect-chain, ... } => aspect-object 15 + # { class, ... } => aspect-object 16 + # { aspect-chain, ... } => aspect-object 13 17 # name => aspect-object 14 18 functionToAspect = lib.types.addCheck (lib.types.functionTo aspectSubmodule) ( 15 19 f: ··· 17 21 args = lib.functionArgs f; 18 22 arity = lib.length (lib.attrNames args); 19 23 isEmpty = arity == 0; 20 - hasClass = args ? class || args ? _class; 21 - hasChain = args ? aspect-chain || args ? _aspect-chain; 24 + hasClass = args ? class; 25 + hasChain = args ? aspect-chain; 22 26 classOnly = hasClass && arity == 1; 23 27 chainOnly = hasChain && arity == 1; 24 28 both = hasClass && hasChain && arity == 2; ··· 39 43 { 40 44 freeformType = lib.types.attrsOf lib.types.deferredModule; 41 45 config._module.args.aspect = config; 46 + imports = [ (lib.mkAliasOptionModule [ "_" ] [ "provides" ]) ]; 42 47 options.name = lib.mkOption { 43 48 description = "Aspect name"; 44 49 default = name;