Modular, context-aware and aspect-oriented dendritic Nix configurations. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ den.oeiuwq.com
configurations den dendritic nix aspect oriented
8
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat: tombstones, identity paths, and substitution in filterIncludes (#409)

filterIncludes now produces tombstones (~name, meta.excluded) for
excluded includes instead of silently dropping them. Tombstones are
empty aspects harmless to module but visible to trace/debug adapters.

Substituted includes produce a tombstone for the original plus the
replacement, enabling both to appear in traces.

New adapters:
- aspectPath: derive identity from name + provider (replaces == on
aspects)
- excludeAspect: exclude by aspect reference via path comparison
- substituteAspect: substitute by aspect reference via mapAspect + path
- tombstone: create a tombstone from a resolved aspect (exported
utility)

authored by

Jason Bowman and committed by
GitHub
737843b6 ca9f7e4f

+311 -40
+86 -32
nix/lib/aspects/adapters.nix
··· 1 - # Adapters for resolve.withAdapter. Default one is module. 1 + # Adapters for resolve.withAdapter. Default adapter is module. 2 2 # 3 - # These adapters determine the return value of resolve. The adapters 4 - # are called by resolve for each resolved aspect, and the adapter can choose 5 - # to recurse or to replace which aspects will be used. 3 + # Adapters determine the return value of resolve. They are called for each 4 + # resolved aspect and can recurse into includes, filter, or transform them. 6 5 # 7 - # Only basic adapters are provided here, see the arguments given by resolve.nix to them. 8 - # Some adapters compose by taking other adapters as parameters. 6 + # See resolve.nix for the arguments passed to adapters: 7 + # { aspect, class, classModule, recurse, aspect-chain, resolveChild } 9 8 { lib, ... }: 10 9 let 11 - 12 - # Default adapter used by `resolve`. 13 - default = filterIncludes module; 14 10 15 11 # Produces a single module importing all classModules from aspect and its includes. 16 12 module = ··· 21 17 ... 22 18 }: 23 19 { 24 - imports = classModule ++ (lib.concatMap (i: (recurse i).imports or [ ]) (aspect.includes or [ ])); 20 + imports = classModule ++ lib.concatMap (i: (recurse i).imports or [ ]) (aspect.includes or [ ]); 25 21 }; 26 22 23 + # Conditionally apply adapter. Returns { } when pred fails (signals exclusion). 27 24 filter = 28 25 pred: adapter: args: 29 26 if pred args.aspect then adapter args else { }; 30 27 31 - # transforms the result of other adapters using f. 28 + # Post-process adapter result. 32 29 map = 33 30 f: adapter: args: 34 31 f (adapter args); 35 32 36 - # transform each aspect into another by applying f to it. 33 + # Transform the aspect before passing to inner adapter. 37 34 mapAspect = 38 35 f: adapter: args: 39 36 adapter (args // { aspect = f args.aspect; }); 40 37 41 - # transforms aspect includes by applying f to it. 38 + # Transform includes before recursion. 42 39 mapIncludes = 43 40 f: adapter: args: 44 - adapter (args // { recurse = included: args.recurse (f included); }); 41 + adapter (args // { recurse = i: args.recurse (f i); }); 42 + 43 + # Derive an aspect's identity path from name and provider. 44 + # Use instead of reference equality — resolved aspects are fresh attrsets. 45 + aspectPath = a: (a.meta.provider or [ ]) ++ [ (a.name or "<anon>") ]; 46 + 47 + # Exclude by aspect reference. 48 + excludeAspect = ref: filter (a: aspectPath a != aspectPath ref); 49 + 50 + # Substitute an aspect reference with a replacement. 51 + substituteAspect = 52 + ref: replacement: mapAspect (a: if aspectPath a == aspectPath ref then replacement else a); 53 + 54 + # Empty aspect marking an excluded include. ~prefix prevents accidental 55 + # name collisions with live aspects. Harmless to module, visible to trace. 56 + # Consumers should check meta.excluded before accessing other aspect fields. 57 + tombstone = 58 + resolved: extra: 59 + let 60 + n = resolved.name or "<anon>"; 61 + in 62 + { 63 + name = "~${n}"; 64 + meta = 65 + (resolved.meta or { }) 66 + // { 67 + excluded = true; 68 + originalName = n; 69 + } 70 + // extra; 71 + }; 45 72 46 - # Handles per-aspect adapter accumulation via meta.adapter. 47 - # Composes meta.adapter with the inner adapter, removes includes that 48 - # would resolve to { }, and tags survivors for downstream propagation. 73 + # Extract what a metaAdapter transforms an aspect to (for substitution detection). 74 + # Assumes metaAdapter eventually calls its inner adapter exactly once. 75 + probeTransform = 76 + metaAdapter: args: resolved: 77 + (metaAdapter (_: { _probed = _.aspect; }) (args // { aspect = resolved; }))._probed or resolved; 78 + 79 + # Handles per-aspect meta.adapter composition. Probes each include to 80 + # determine: keep, exclude (tombstone), or substitute (tombstone + replacement). 81 + # Tags survivors with the adapter for downstream propagation. 49 82 filterIncludes = 50 83 inner: 51 84 args@{ aspect, resolveChild, ... }: ··· 55 88 if metaAdapter != null && aspect ? includes then 56 89 let 57 90 composed = metaAdapter (filterIncludes inner); 58 - keeps = 91 + 92 + processInclude = 59 93 i: 60 - composed ( 61 - args 62 - // { 63 - aspect = resolveChild i; 64 - classModule = [ ]; 65 - } 66 - ) != { }; 94 + let 95 + resolved = resolveChild i; 96 + result = composed ( 97 + args 98 + // { 99 + aspect = resolved; 100 + classModule = [ ]; 101 + } 102 + ); 103 + probed = probeTransform metaAdapter args resolved; 104 + in 105 + if result == { } then 106 + [ (tombstone resolved { }) ] 107 + else if aspectPath probed != aspectPath resolved then 108 + [ 109 + (tombstone resolved { replacedBy = probed.name or "<anon>"; }) 110 + probed 111 + ] 112 + else 113 + [ i ]; 114 + 67 115 tag = 68 116 i: 69 - if builtins.isAttrs i && i.meta.adapter or null == null then 117 + if builtins.isAttrs i && i.meta.adapter or null == null && !(i.meta.excluded or false) then 70 118 i 71 119 // { 72 120 meta = (i.meta or { }) // { ··· 80 128 args 81 129 // { 82 130 aspect = aspect // { 83 - includes = builtins.map tag (lib.filter keeps aspect.includes); 131 + includes = builtins.map tag (lib.concatMap processInclude aspect.includes); 84 132 }; 85 133 } 86 134 ) 87 135 else 88 136 inner args; 89 137 90 - # Utility for debugging. Traces aspect.name as nested lists per includes. 91 - traceName = 138 + default = filterIncludes module; 139 + 140 + # Traces aspect.name as nested lists per includes. Composed with filterIncludes 141 + # so tombstones and substitutions are visible. 142 + trace = filterIncludes ( 92 143 { aspect, recurse, ... }: 93 144 { 94 145 trace = [ aspect.name ] ++ builtins.map (i: (recurse i).trace or [ ]) (aspect.includes or [ ]); 95 - }; 146 + } 147 + ); 96 148 97 - trace = filterIncludes traceName; 98 149 in 99 150 { 100 151 inherit 152 + aspectPath 101 153 default 154 + excludeAspect 102 155 filter 103 156 filterIncludes 104 157 map 105 158 mapAspect 106 159 mapIncludes 107 160 module 161 + substituteAspect 162 + tombstone 108 163 trace 109 - traceName 110 164 ; 111 165 }
+15 -3
templates/ci/modules/features/adapter-propagation.nix
··· 27 27 den.aspects.baz.nixos = { }; 28 28 29 29 expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.parent; 30 + # baz tombstone visible in trace 30 31 expected.trace = [ 31 32 "parent" 32 - [ "child" ] 33 + [ 34 + "child" 35 + [ "~baz" ] 36 + ] 33 37 ]; 34 38 } 35 39 ); ··· 53 57 [ 54 58 "child" 55 59 [ "kept" ] 60 + [ "~excluded" ] 56 61 ] 57 62 ]; 58 63 } ··· 76 81 "a" 77 82 [ 78 83 "b" 84 + [ "~c" ] 79 85 [ "d" ] 80 86 ] 81 87 ]; ··· 98 104 expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.a; 99 105 expected.trace = [ 100 106 "a" 101 - [ "b" ] 102 - [ "c" ] 107 + [ 108 + "b" 109 + [ "~d" ] 110 + ] 111 + [ 112 + "c" 113 + [ "~d" ] 114 + ] 103 115 ]; 104 116 } 105 117 );
+9 -2
templates/ci/modules/features/aspect-adapter.nix
··· 15 15 den.aspects.baz.nixos = { }; 16 16 17 17 expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.foo; 18 + # baz tombstone visible in trace 18 19 expected.trace = [ 19 20 "foo" 20 21 [ "bar" ] 22 + [ "~baz" ] 21 23 ]; 22 24 } 23 25 ); ··· 36 38 den.aspects.baz.nixos = { }; 37 39 38 40 expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.root; 41 + # foo's adapter only affects its subtree; root's baz is unaffected 39 42 expected.trace = [ 40 43 "root" 41 44 [ ··· 62 65 expr = 63 66 let 64 67 inherit (den.lib.aspects) resolve adapters; 65 - outerAdapter = adapters.filter (a: a.name != "baz") adapters.traceName; 68 + outerTrace = adapters.filter (a: a.name != "baz") adapters.trace; 66 69 in 67 - resolve.withAdapter (adapters.filterIncludes outerAdapter) "nixos" den.aspects.foo; 70 + resolve.withAdapter outerTrace "nixos" den.aspects.foo; 71 + # bar tombstoned by meta.adapter, baz killed by outer filter (no tombstone 72 + # since the outer filter is not wrapped in filterIncludes) 68 73 expected.trace = [ 69 74 "foo" 75 + [ "~bar" ] 76 + [ ] 70 77 ]; 71 78 } 72 79 );
+199
templates/ci/modules/features/aspect-path.nix
··· 1 + { denTest, lib, ... }: 2 + { 3 + flake.tests.aspect-path = { 4 + 5 + test-aspectPath-named = denTest ( 6 + { den, ... }: 7 + { 8 + den.aspects.foo.nixos = { }; 9 + expr = den.lib.aspects.adapters.aspectPath den.aspects.foo; 10 + expected = [ "foo" ]; 11 + } 12 + ); 13 + 14 + test-aspectPath-with-provider = denTest ( 15 + { den, ... }: 16 + { 17 + den.aspects.monitoring = { 18 + nixos = { }; 19 + provides.node-exporter.nixos = { }; 20 + }; 21 + expr = den.lib.aspects.adapters.aspectPath den.aspects.monitoring._.node-exporter; 22 + expected = [ 23 + "monitoring" 24 + "node-exporter" 25 + ]; 26 + } 27 + ); 28 + 29 + # excludeAspect: excluded include becomes a tombstone (visible in trace) 30 + test-excludeAspect-tombstone-in-trace = denTest ( 31 + { den, ... }: 32 + { 33 + den.aspects.foo.includes = [ 34 + den.aspects.bar 35 + den.aspects.baz 36 + ]; 37 + den.aspects.foo.meta.adapter = 38 + inherited: den.lib.aspects.adapters.excludeAspect den.aspects.baz inherited; 39 + den.aspects.bar.nixos = { }; 40 + den.aspects.baz.nixos = { }; 41 + 42 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.foo; 43 + # baz appears as tombstone (~baz, no children) 44 + expected.trace = [ 45 + "foo" 46 + [ "bar" ] 47 + [ "~baz" ] 48 + ]; 49 + } 50 + ); 51 + 52 + # excludeAspect: tombstone contributes no modules to the build 53 + test-excludeAspect-no-modules = denTest ( 54 + { den, igloo, ... }: 55 + { 56 + den.hosts.x86_64-linux.igloo = { }; 57 + den.aspects.igloo.includes = [ 58 + den.aspects.bar 59 + den.aspects.baz 60 + ]; 61 + den.aspects.igloo.meta.adapter = 62 + inherited: den.lib.aspects.adapters.excludeAspect den.aspects.baz inherited; 63 + den.aspects.bar.nixos.environment.sessionVariables.msg = "bar"; 64 + den.aspects.baz.nixos.environment.sessionVariables.msg = "baz"; 65 + 66 + # only bar's module is included, baz is excluded 67 + expr = igloo.environment.sessionVariables.msg; 68 + expected = "bar"; 69 + } 70 + ); 71 + 72 + # excludeAspect: propagates through subtree 73 + test-excludeAspect-propagates-to-subtree = denTest ( 74 + { den, ... }: 75 + { 76 + den.aspects.root.includes = [ den.aspects.role ]; 77 + den.aspects.root.meta.adapter = 78 + inherited: den.lib.aspects.adapters.excludeAspect den.aspects.baz inherited; 79 + den.aspects.role.includes = [ 80 + den.aspects.bar 81 + den.aspects.baz 82 + ]; 83 + den.aspects.bar.nixos = { }; 84 + den.aspects.baz.nixos = { }; 85 + 86 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.root; 87 + # baz tombstone appears in role's subtree 88 + expected.trace = [ 89 + "root" 90 + [ 91 + "role" 92 + [ "bar" ] 93 + [ "~baz" ] 94 + ] 95 + ]; 96 + } 97 + ); 98 + 99 + # excludeAspect: by provider path 100 + test-excludeAspect-by-provider = denTest ( 101 + { den, ... }: 102 + { 103 + den.aspects.monitoring = { 104 + nixos = { }; 105 + provides.node-exporter.nixos = { }; 106 + provides.alerting.nixos = { }; 107 + }; 108 + den.aspects.server.includes = with den.aspects; [ 109 + monitoring 110 + monitoring._.node-exporter 111 + monitoring._.alerting 112 + ]; 113 + den.aspects.server.meta.adapter = 114 + inherited: den.lib.aspects.adapters.excludeAspect den.aspects.monitoring._.node-exporter inherited; 115 + 116 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.server; 117 + # node-exporter tombstone visible, alerting kept 118 + expected.trace = [ 119 + "server" 120 + [ "monitoring" ] 121 + [ "~node-exporter" ] 122 + [ "alerting" ] 123 + ]; 124 + } 125 + ); 126 + 127 + # substituteAspect: replaced include becomes tombstone + replacement 128 + test-substituteAspect-replaces = denTest ( 129 + { den, ... }: 130 + { 131 + den.aspects.foo.includes = [ 132 + den.aspects.bar 133 + den.aspects.baz 134 + ]; 135 + den.aspects.foo.meta.adapter = 136 + inherited: den.lib.aspects.adapters.substituteAspect den.aspects.bar den.aspects.qux inherited; 137 + den.aspects.bar.nixos = { }; 138 + den.aspects.baz.nixos = { }; 139 + den.aspects.qux.nixos = { }; 140 + 141 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.foo; 142 + # bar tombstone + qux replacement, baz unchanged 143 + expected.trace = [ 144 + "foo" 145 + [ "~bar" ] 146 + [ "qux" ] 147 + [ "baz" ] 148 + ]; 149 + } 150 + ); 151 + 152 + # substituteAspect: replacement modules are used in build 153 + test-substituteAspect-build-uses-replacement = denTest ( 154 + { den, igloo, ... }: 155 + { 156 + den.hosts.x86_64-linux.igloo = { }; 157 + den.aspects.igloo.includes = [ den.aspects.bar ]; 158 + den.aspects.igloo.meta.adapter = 159 + inherited: den.lib.aspects.adapters.substituteAspect den.aspects.bar den.aspects.qux inherited; 160 + den.aspects.bar.nixos.environment.sessionVariables.msg = "bar"; 161 + den.aspects.qux.nixos.environment.sessionVariables.msg = "qux"; 162 + 163 + # qux's module is used, not bar's 164 + expr = igloo.environment.sessionVariables.msg; 165 + expected = "qux"; 166 + } 167 + ); 168 + 169 + # substituteAspect: propagates through subtree 170 + test-substituteAspect-propagates = denTest ( 171 + { den, ... }: 172 + { 173 + den.aspects.root.includes = [ den.aspects.role ]; 174 + den.aspects.root.meta.adapter = 175 + inherited: den.lib.aspects.adapters.substituteAspect den.aspects.baz den.aspects.qux inherited; 176 + den.aspects.role.includes = [ 177 + den.aspects.bar 178 + den.aspects.baz 179 + ]; 180 + den.aspects.bar.nixos = { }; 181 + den.aspects.baz.nixos = { }; 182 + den.aspects.qux.nixos = { }; 183 + 184 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.root; 185 + # baz tombstone + qux in role's subtree 186 + expected.trace = [ 187 + "root" 188 + [ 189 + "role" 190 + [ "bar" ] 191 + [ "~baz" ] 192 + [ "qux" ] 193 + ] 194 + ]; 195 + } 196 + ); 197 + 198 + }; 199 + }
+2 -3
templates/ci/modules/features/resolve-adapters.nix
··· 10 10 den.aspects.bar.includes = [ den.aspects.baz ]; 11 11 den.aspects.baz.nixos = { }; 12 12 13 - expr = with den.lib.aspects; resolve.withAdapter adapters.traceName "nixos" den.aspects.foo; 13 + expr = with den.lib.aspects; resolve.withAdapter adapters.trace "nixos" den.aspects.foo; 14 14 expected.trace = [ 15 15 "foo" 16 16 [ ··· 32 32 expr = 33 33 let 34 34 inherit (den.lib.aspects) resolve adapters; 35 - notBar = adapters.filter (aspect: aspect.name != "bar"); 36 - composed = notBar adapters.traceName; 35 + composed = adapters.filter (aspect: aspect.name != "bar") adapters.trace; 37 36 in 38 37 resolve.withAdapter composed "nixos" den.aspects.foo; 39 38 expected.trace = [