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(namespaces): Allow sharing custom ctx and schemas on namespaces (#265)

authored by

Victor Borja and committed by
GitHub
2f1cfa01 7c858826

+365 -78
+4 -8
modules/aspects.nix
··· 1 - { config, lib, ... }: 2 - let 3 - inherit (config.den.lib.aspects.types) aspectsType; 4 - denfulType = lib.types.attrsOf aspectsType; 5 - in 1 + { den, lib, ... }: 6 2 { 7 3 options.den.ful = lib.mkOption { 8 - default = { }; # namespaces (local or merged from inputs) 9 - type = denfulType; 4 + default = { }; 5 + type = lib.types.attrsOf den.lib.nsTypes.namespaceType; 10 6 }; 11 7 options.flake.denful = lib.mkOption { 12 - default = { }; # flake output (assigned via den.namespace) 8 + default = { }; 13 9 type = lib.types.attrsOf lib.types.raw; 14 10 }; 15 11 }
+8 -7
nix/ctx-apply.nix
··· 1 1 { lib, den, ... }: 2 + ctxNs: 2 3 let 3 4 inherit (den.lib) parametric; 4 5 ··· 17 18 cleanCtx = self: builtins.removeAttrs self ctxKeys; 18 19 19 20 # Flatten nested into result to [ { path = [str]; into = [ctx_value]; } ]. 20 - # Leaf nodes are lists; intermediate nodes recurse deeper. 21 21 flattenInto = 22 22 attrset: prefix: 23 23 lib.concatLists ( ··· 39 39 ); 40 40 41 41 transformAll = 42 - source: self: ctx: key: 42 + source: self: ctxValue: key: 43 43 [ 44 44 { 45 - inherit ctx source key; 45 + ctx = ctxValue; 46 + inherit source key; 46 47 ctxDef = self; 47 48 } 48 49 ] ··· 50 51 map ( 51 52 { path, into }: 52 53 let 53 - target = lib.attrByPath path null den.ctx; 54 + target = lib.attrByPath path null ctxNs; 54 55 tkey = lib.concatStringsSep "." path; 55 56 in 56 57 if target != null then ··· 68 69 ) into 69 70 else 70 71 [ ] 71 - ) (flattenInto (self.into ctx) [ ]) 72 + ) (flattenInto (self.into ctxValue) [ ]) 72 73 ); 73 74 74 75 noop = _: { }; ··· 108 109 result = [ ]; 109 110 }; 110 111 111 - ctxApply = self: ctx: { 112 - includes = dedupIncludes (transformAll null self ctx self.name); 112 + ctxApply = self: ctxValue: { 113 + includes = dedupIncludes (transformAll null self ctxValue self.name); 113 114 }; 114 115 115 116 in
+55
nix/ctx-types.nix
··· 1 + { lib, den, ... }: 2 + ctxApply: 3 + let 4 + 5 + intoType = 6 + let 7 + intoAttrsType = lib.types.lazyAttrsOf lib.types.raw; 8 + intoFnType = lib.types.functionTo (lib.types.lazyAttrsOf lib.types.raw); 9 + in 10 + lib.types.either intoFnType intoAttrsType; 11 + 12 + applyIntoNode = 13 + ctxValue: v: 14 + if lib.isFunction v then 15 + (den.lib.take.atLeast v) ctxValue 16 + else if builtins.isAttrs v then 17 + lib.mapAttrs (_: applyIntoNode ctxValue) v 18 + else 19 + v; 20 + 21 + normalizeInto = 22 + value: 23 + if lib.isFunction value then value else ctxValue: lib.mapAttrs (_: applyIntoNode ctxValue) value; 24 + 25 + ctxSubmodule = lib.types.submodule { 26 + imports = den.lib.aspects.types.aspectSubmodule.getSubModules; 27 + options.into = lib.mkOption { 28 + description = "Context transformations to other context types"; 29 + type = intoType; 30 + default = _: { }; 31 + apply = normalizeInto; 32 + }; 33 + config.__functor = lib.mkForce ctxApply; 34 + }; 35 + 36 + ctxTreeType = lib.types.mkOptionType { 37 + name = "ctxTree"; 38 + description = "ctx definition or namespace"; 39 + check = lib.isAttrs; 40 + merge = 41 + loc: defs: 42 + let 43 + hasKey = x: x ? into || x ? provides || x ? _ || x ? includes || x ? __functor || x ? _module; 44 + isLeaf = lib.any (d: hasKey d.value) defs; 45 + in 46 + if isLeaf then ctxSubmodule.merge loc defs else (lib.types.lazyAttrsOf ctxTreeType).merge loc defs; 47 + emptyValue = { 48 + value = { }; 49 + }; 50 + }; 51 + 52 + in 53 + { 54 + inherit ctxTreeType; 55 + }
+4
nix/lib.nix
··· 120 120 ; 121 121 }; 122 122 123 + nsTypes = import ./namespace-types.nix { inherit lib den; }; 124 + ctxTypes = import ./ctx-types.nix { inherit lib den; }; 123 125 ctxApply = import ./ctx-apply.nix { inherit lib den; }; 124 126 125 127 home-env = import ./home-env.nix { inherit lib den inputs; }; ··· 145 147 take 146 148 isStatic 147 149 ctxApply 150 + ctxTypes 151 + nsTypes 148 152 nh 149 153 home-env 150 154 nixModule
+31
nix/namespace-types.nix
··· 1 + { den, lib, ... }: 2 + let 3 + inherit (den.lib) take parametric ctxApply; 4 + inherit (den.lib.aspects.types) aspectsType aspectSubmodule; 5 + 6 + namespaceType = lib.types.submodule ( 7 + nsArgs: 8 + let 9 + nsCtxApply = ctxApply nsArgs.config.ctx; 10 + inherit (den.lib.ctxTypes nsCtxApply) ctxTreeType; 11 + in 12 + { 13 + options.ctx = lib.mkOption { 14 + description = "namespace context pipeline"; 15 + default = { }; 16 + type = lib.types.lazyAttrsOf ctxTreeType; 17 + }; 18 + options.schema = lib.mkOption { 19 + description = "namespace schema — freeform deferred modules per entity kind"; 20 + default = { }; 21 + type = lib.types.submodule { 22 + freeformType = lib.types.lazyAttrsOf lib.types.deferredModule; 23 + }; 24 + }; 25 + freeformType = aspectsType; 26 + } 27 + ); 28 + in 29 + { 30 + inherit namespaceType; 31 + }
+17 -2
nix/namespace.nix
··· 10 10 11 11 internals = [ 12 12 "_" 13 + "_module" 13 14 "modules" 14 15 "resolve" 15 16 "__functor" ··· 20 21 if !builtins.isAttrs v then 21 22 v 22 23 else 23 - (builtins.removeAttrs v internals) 24 - // lib.optionalAttrs (v ? provides) { provides = lib.mapAttrs (_: stripAspect) v.provides; }; 24 + let 25 + stripped = builtins.removeAttrs v internals; 26 + withProvides = lib.optionalAttrs (stripped ? provides) { 27 + provides = lib.mapAttrs (_: stripAspect) stripped.provides; 28 + }; 29 + deepStripped = lib.mapAttrs ( 30 + n: child: 31 + if n == "provides" then 32 + child 33 + else if builtins.isAttrs child then 34 + builtins.removeAttrs child internals 35 + else 36 + child 37 + ) stripped; 38 + in 39 + deepStripped // withProvides; 25 40 26 41 stripNamespace = lib.mapAttrs (_: stripAspect); 27 42
+4 -59
nix/nixModule/ctx.nix
··· 1 - { config, lib, ... }: 1 + { den, lib, ... }: 2 2 let 3 - inherit (config) den; 3 + inherit (den.lib) take; 4 4 inherit (den.lib.aspects.types) aspectSubmodule; 5 - inherit (den.lib) ctxApply take; 6 - 7 - intoType = 8 - let 9 - # into = { x = ctx→[]; y = ctx→[]; } 10 - # Also supports nested namespace keys: into."foo.bar" = fn 11 - intoAttrsType = lib.types.lazyAttrsOf lib.types.raw; 12 - 13 - # into = ctx → { x = []; y = []; } 14 - intoFnType = lib.types.functionTo (lib.types.lazyAttrsOf lib.types.raw); 15 - in 16 - lib.types.either intoFnType intoAttrsType; 17 5 18 - # Recrusively apply ctx to leaf functions in a (possibly nested) attrset. 19 - applyIntoNode = 20 - ctx: v: 21 - if lib.isFunction v then 22 - (take.atLeast v) ctx 23 - else if builtins.isAttrs v then 24 - lib.mapAttrs (_: applyIntoNode ctx) v 25 - else 26 - v; 27 - 28 - normalizeInto = 29 - value: if lib.isFunction value then value else ctx: lib.mapAttrs (_: applyIntoNode ctx) value; 30 - 31 - ctxSubmodule = lib.types.submodule ( 32 - { ... }: 33 - { 34 - imports = aspectSubmodule.getSubModules; 35 - options.into = lib.mkOption { 36 - description = "Context transformations to other context types"; 37 - type = intoType; 38 - default = _: { }; 39 - apply = normalizeInto; 40 - }; 41 - config.__functor = lib.mkForce ctxApply; 42 - } 43 - ); 44 - 45 - # A ctx tree node: either a leaf ctx definition (has `into`/`__functor`) 46 - # or a namespace container holding more nodes. Detected at merge time, 47 - # so the self-reference in the namespace branch is lazily evaluated. 48 - ctxTreeType = lib.types.mkOptionType { 49 - name = "ctxTree"; 50 - description = "ctx definition or namespace"; 51 - check = lib.isAttrs; 52 - merge = 53 - loc: defs: 54 - let 55 - hasKey = x: x ? into || x ? provides || x ? _ || x ? includes || x ? __functor || x ? _module; 56 - isLeaf = lib.any (d: hasKey d.value) defs; 57 - in 58 - if isLeaf then ctxSubmodule.merge loc defs else (lib.types.lazyAttrsOf ctxTreeType).merge loc defs; 59 - emptyValue = { 60 - value = { }; 61 - }; 62 - }; 6 + denCtxApply = den.lib.ctxApply den.ctx; 7 + inherit (den.lib.ctxTypes denCtxApply) ctxTreeType; 63 8 64 9 in 65 10 {
+1 -1
nix/nixModule/default.nix
··· 7 7 }@args: 8 8 { 9 9 _module.args.den = config.den; 10 - imports = map (f: import f args) [ 10 + imports = map (f: import f (args // { den = config.den; })) [ 11 11 ./lib.nix 12 12 ./ctx.nix 13 13 ./aspects.nix
+42
templates/ci/modules/features/namespace-provider.nix
··· 1 + { denTest, ... }: 2 + { 3 + flake.tests.namespace-provider = { 4 + 5 + # ctx defined in a provider flake is callable in the consumer 6 + # and produces the correct names — no doubling. 7 + test-shared-namespace-ctx = denTest ( 8 + { 9 + inputs, 10 + provider, 11 + funnyNames, 12 + ... 13 + }: 14 + { 15 + imports = [ (inputs.den.namespace "provider" [ inputs.provider ]) ]; 16 + expr = funnyNames (provider.ctx.simple { }); # call as function 17 + expected = [ "from-provider-ctx" ]; 18 + } 19 + ); 20 + 21 + # schema defined in a provider flake is usable in consumers. 22 + test-shared-namespace-schema = denTest ( 23 + { 24 + inputs, 25 + provider, 26 + lib, 27 + ... 28 + }: 29 + { 30 + imports = [ (inputs.den.namespace "provider" [ inputs.provider ]) ]; 31 + expr = 32 + let 33 + # provider exposes a deferred module that we can eval here in the consumer, and it has the expected config. 34 + mod = lib.evalModules { modules = [ provider.schema.entity ]; }; 35 + in 36 + mod.config.names; 37 + expected = [ "provider-entity-schema" ]; 38 + } 39 + ); 40 + 41 + }; 42 + }
+181
templates/ci/modules/features/namespace-schemas.nix
··· 1 + { denTest, ... }: 2 + { 3 + flake.tests.namespace-schemas = { 4 + 5 + # A namespace has its own aspects that can be resolved. 6 + test-namespace-aspects-resolve = denTest ( 7 + { 8 + den, 9 + aux, 10 + inputs, 11 + funnyNames, 12 + ... 13 + }: 14 + { 15 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 16 + aux.my-aspect.funny.names = [ "hello" ]; 17 + 18 + expr = funnyNames aux.my-aspect; 19 + expected = [ "hello" ]; 20 + } 21 + ); 22 + 23 + # Namespace ctx dispatches includes through its own pipeline. 24 + test-namespace-ctx-dispatches = denTest ( 25 + { 26 + den, 27 + aux, 28 + funnyNames, 29 + inputs, 30 + ... 31 + }: 32 + { 33 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 34 + aux.my-aspect.includes = [ 35 + ( 36 + { word, ... }: 37 + { 38 + funny.names = [ word ]; 39 + } 40 + ) 41 + ]; 42 + aux.ctx.entry.provides.entry = { word }: den.lib.parametric.fixedTo { inherit word; } aux.my-aspect; 43 + 44 + expr = funnyNames (aux.ctx.entry { word = "greet"; }); 45 + expected = [ "greet" ]; 46 + } 47 + ); 48 + 49 + # Two independent namespaces don't share aspects or ctx. 50 + test-two-namespaces-independent = denTest ( 51 + { 52 + alpha, 53 + beta, 54 + funnyNames, 55 + inputs, 56 + ... 57 + }: 58 + { 59 + imports = [ 60 + (inputs.den.namespace "alpha" [ ]) 61 + (inputs.den.namespace "beta" [ ]) 62 + ]; 63 + alpha.shared.funny.names = [ "alpha" ]; 64 + beta.shared.funny.names = [ "beta" ]; 65 + 66 + expr = (funnyNames alpha.shared) ++ (funnyNames beta.shared); 67 + expected = [ 68 + "alpha" 69 + "beta" 70 + ]; 71 + } 72 + ); 73 + 74 + # Namespace aspects are fully independent from den.aspects. 75 + test-namespace-independent-from-den = denTest ( 76 + { 77 + den, 78 + aux, 79 + igloo, 80 + funnyNames, 81 + inputs, 82 + ... 83 + }: 84 + { 85 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 86 + aux.igloo.funny.names = [ "aux" ]; 87 + den.aspects.igloo.funny.names = [ "den" ]; 88 + den.hosts.x86_64-linux.igloo = { }; 89 + 90 + expr = (funnyNames aux.igloo) ++ (funnyNames den.aspects.igloo); 91 + expected = [ 92 + "aux" 93 + "den" 94 + ]; 95 + } 96 + ); 97 + 98 + # Nested ctx works within a namespace. 99 + test-namespace-nested-ctx = denTest ( 100 + { 101 + den, 102 + aux, 103 + funnyNames, 104 + inputs, 105 + ... 106 + }: 107 + { 108 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 109 + aux.my-aspect.includes = [ 110 + ( 111 + { word, ... }: 112 + { 113 + funny.names = [ word ]; 114 + } 115 + ) 116 + ]; 117 + aux.ctx.ns.leaf.provides.leaf = 118 + { word }: den.lib.parametric.fixedTo { inherit word; } aux.my-aspect; 119 + 120 + expr = funnyNames (aux.ctx.ns.leaf { word = "nested"; }); 121 + expected = [ "nested" ]; 122 + } 123 + ); 124 + 125 + # Namespace schema accepts freeform deferredModule entries per entity kind. 126 + test-namespace-schema-freeform = denTest ( 127 + { 128 + aux, 129 + funnyNames, 130 + inputs, 131 + lib, 132 + ... 133 + }: 134 + { 135 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 136 + aux.schema.widget = 137 + { lib, ... }: 138 + { 139 + options.funny.names = lib.mkOption { 140 + type = lib.types.listOf lib.types.str; 141 + default = [ ]; 142 + }; 143 + config.funny.names = [ "widget-schema" ]; 144 + }; 145 + 146 + expr = 147 + let 148 + mod = lib.evalModules { modules = [ aux.schema.widget ]; }; 149 + in 150 + mod.config.funny.names; 151 + expected = [ "widget-schema" ]; 152 + } 153 + ); 154 + 155 + # Multiple schema entries for different entity kinds coexist. 156 + test-namespace-schema-multiple-keys = denTest ( 157 + { 158 + aux, 159 + inputs, 160 + lib, 161 + ... 162 + }: 163 + { 164 + imports = [ (inputs.den.namespace "aux" [ ]) ]; 165 + aux.schema.alpha = { 166 + _secret = "alpha"; 167 + }; 168 + aux.schema.beta = { 169 + _secret = "beta"; 170 + }; 171 + 172 + expr = builtins.attrNames aux.schema; 173 + expected = [ 174 + "alpha" 175 + "beta" 176 + ]; 177 + } 178 + ); 179 + 180 + }; 181 + }
+18 -1
templates/ci/provider/modules/den.nix
··· 1 - { inputs, den, ... }: 1 + { 2 + inputs, 3 + den, 4 + lib, 5 + ... 6 + }: 2 7 { 3 8 systems = [ 4 9 "x86_64-linux" ··· 40 45 } 41 46 ) 42 47 ]; 48 + }; 49 + 50 + # A ctx entry shared to consumers — provides a self-provider function. 51 + provider.ctx.simple._.simple = _: { funny.names = [ "from-provider-ctx" ]; }; 52 + 53 + # A schema entry that can be shared to consumers. 54 + provider.schema.entity = { 55 + options.names = lib.mkOption { 56 + type = lib.types.listOf lib.types.str; 57 + default = [ ]; 58 + }; 59 + config.names = lib.mkDefault [ "provider-entity-schema" ]; 43 60 }; 44 61 }