From 6c73893ed210c9fef564fffd7b71e64ecd67d661 Mon Sep 17 00:00:00 2001 From: "Dylan R. Johnston" Date: Fri, 27 Mar 2026 15:32:29 +0800 Subject: [PATCH] feat: add fixedTo.{atLeast,exactly,upTo} --- nix/lib/can-take.nix | 1 + nix/lib/default.nix | 1 + nix/lib/parametric.nix | 8 +- nix/lib/recursive-functor.nix | 21 +++ nix/lib/take.nix | 11 +- templates/ci/modules/features/parametric.nix | 177 ++++++++++++++++++- 6 files changed, 205 insertions(+), 14 deletions(-) create mode 100644 nix/lib/recursive-functor.nix diff --git a/nix/lib/can-take.nix b/nix/lib/can-take.nix index b0aa669f..c1befff1 100644 --- a/nix/lib/can-take.nix +++ b/nix/lib/can-take.nix @@ -17,5 +17,6 @@ in { __functor = self: self.atLeast; atLeast = params: func: (check params func).satisfied; + upTo = params: func: (check params func).satisfied; exactly = params: func: (check params func).exactly; } diff --git a/nix/lib/default.nix b/nix/lib/default.nix index 63d0afbb..cb5ef3e1 100644 --- a/nix/lib/default.nix +++ b/nix/lib/default.nix @@ -24,6 +24,7 @@ let ctxTypes = ./ctx-types.nix; __findFile = ./den-brackets.nix; functor = ./functor.nix; + recursiveFunctor = ./recursive-functor.nix; fwTypes = ./types.nix; home-env = ./home-env.nix; nh = ./nh.nix; diff --git a/nix/lib/parametric.nix b/nix/lib/parametric.nix index 64828955..e5efb0c4 100644 --- a/nix/lib/parametric.nix +++ b/nix/lib/parametric.nix @@ -1,6 +1,6 @@ { lib, den, ... }: let - inherit (den.lib) take functor; + inherit (den.lib) take functor recursiveFunctor; inherit (den.lib.statics) owned statics isCtxStatic; fixedRecurse = @@ -19,7 +19,7 @@ let parametric.expands = attrs: parametric.withOwn (aspect: ctx: parametric.atLeast aspect (ctx // attrs)); - parametric.fixedTo = + parametric.fixedTo.__functor = attrs: aspect: aspect // { @@ -35,6 +35,10 @@ let }; }; + parametric.fixedTo.atLeast = recursiveFunctor (lib.flip take.atLeast); + parametric.fixedTo.exactly = recursiveFunctor (lib.flip take.exactly); + parametric.fixedTo.upTo = recursiveFunctor (lib.flip take.upTo); + parametric.withOwn = functor: aspect: aspect diff --git a/nix/lib/recursive-functor.nix b/nix/lib/recursive-functor.nix new file mode 100644 index 00000000..81dd9564 --- /dev/null +++ b/nix/lib/recursive-functor.nix @@ -0,0 +1,21 @@ +# "Just Give 'Em One of These" - Moe Szyslak +# A __functor that applies context to parametric includes (functions) and recurses into other included aspects +{ lib, ... }: +let + recursiveApply = + apply: ctx: include: + if include ? includes then recursiveFunctor apply include ctx else apply ctx include; + recursiveFunctor = + apply: aspect: + aspect + // { + __functor = self: ctx: { + includes = + self.includes or [ ] + |> builtins.filter lib.isFunction + |> map (recursiveApply apply ctx) + |> builtins.filter (x: x != { }); + }; + }; +in +recursiveFunctor diff --git a/nix/lib/take.nix b/nix/lib/take.nix index be7ed216..cef8459e 100644 --- a/nix/lib/take.nix +++ b/nix/lib/take.nix @@ -1,10 +1,11 @@ -{ den, ... }: +{ den, lib, ... }: let take.unused = _unused: used: used; - take.exactly = take den.lib.canTake.exactly; - take.atLeast = take den.lib.canTake.atLeast; + take.exactly = take (_fn: ctx: ctx) den.lib.canTake.exactly; + take.atLeast = take (_fn: ctx: ctx) den.lib.canTake.atLeast; + take.upTo = take (fn: fn |> lib.functionsArgs |> builtins.intersectAttrs) den.lib.canTake.upTo; take.__functor = - _: takes: fn: ctx: - if takes ctx fn then fn ctx else { }; + _: takes: adapter: fn: ctx: + if takes ctx fn then fn (adapter fn ctx) else { }; in take diff --git a/templates/ci/modules/features/parametric.nix b/templates/ci/modules/features/parametric.nix index c99b4c6d..5ecbfd11 100644 --- a/templates/ci/modules/features/parametric.nix +++ b/templates/ci/modules/features/parametric.nix @@ -1,9 +1,12 @@ { denTest, ... }: { flake.tests.parametric = { - test-parametric-forwards-context = denTest ( - { den, igloo, ... }: + { + den, + igloo, + ... + }: let foo = den.lib.parametric { includes = [ @@ -26,7 +29,11 @@ ); test-parametric-owned-config = denTest ( - { den, igloo, ... }: + { + den, + igloo, + ... + }: let foo = den.lib.parametric { nixos.networking.hostName = "from-parametric-owned"; @@ -43,7 +50,11 @@ ); test-parametric-fixedTo = denTest ( - { den, igloo, ... }: + { + den, + igloo, + ... + }: let foo = { host, ... }: @@ -68,12 +79,20 @@ ); test-parametric-expands = denTest ( - { den, igloo, ... }: + { + den, + igloo, + ... + }: let foo = den.lib.parametric.expands { planet = "Earth"; } { includes = [ ( - { host, planet, ... }: + { + host, + planet, + ... + }: { nixos.users.users.tux.description = "${host.name}/${planet}"; } @@ -91,7 +110,11 @@ ); test-never-matches-aspect-skipped = denTest ( - { den, igloo, ... }: + { + den, + igloo, + ... + }: let never-matches = { never-exists, ... }: @@ -113,5 +136,145 @@ } ); + test-parametric-fixedTo-atLeast = denTest ( + { + den, + lib, + inputs, + ... + }: + let + inherit (den.lib.parametric) fixedTo; + testAspect = name: include: { + nixos.test = [ "excluded-owned-${name}" ]; + + _.host = + { host }: + { + nixos.test = [ "${host}-${name}" ]; + }; + + _.host-user = + { host, user }: + { + nixos.test = [ "${host}-${user}-${name}" ]; + }; + + _.static = + { class, ... }: + { + ${class}.test = [ "excluded-static-${name}" ]; + }; + + includes = include ++ [ + den.aspects.${name}._.host + den.aspects.${name}._.host-user + den.aspects.${name}._.static + ]; + }; + testOptionProvider = args: aspect: { + includes = [ + aspect + { + __functor = self: _: { + nixos.options.test = lib.mkOption { type = lib.types.listOf lib.types.str; }; + }; + __functionArgs = + args + |> map (arg: { + name = arg; + value = false; + }) + |> builtins.listToAttrs; + } + ]; + }; + in + { + den.aspects.inner = testAspect "inner" [ ]; + den.aspects.outer = testAspect "outer" [ den.aspects.inner ]; + + expr = + { + exactlyHost = { + ctx = { + host = "igloo"; + }; + functor = fixedTo.exactly; + }; + exactlyHostUser = { + ctx = { + host = "igloo"; + user = "tux"; + }; + functor = fixedTo.exactly; + }; + upToHost = { + ctx = { + host = "igloo"; + }; + functor = fixedTo.upTo; + }; + upToHostUser = { + ctx = { + host = "igloo"; + user = "tux"; + }; + functor = fixedTo.upTo; + }; + atLeastHost = { + ctx = { + host = "igloo"; + }; + functor = fixedTo.atLeast; + }; + # This test case errors because atLeast tries to call { host }: with { host, user } causing an error + # this is IMO incorrect behaviour, but would technically be a breaking change if people are using + # args@{ host, ... } which is why I introduced a new kind "upTo" which uses the canTake.atLeast + # predicate but only calls the function with the attributes it expects + # atLeastHostUser = { + # ctx = { + # host = "igloo"; + # user = "tux"; + # }; + # parametricFunctor = atLeast.fixed; + # }; + } + |> lib.mapAttrs ( + _: test: + den.aspects.outer + |> testOptionProvider (builtins.attrNames test.ctx) + |> (lib.flip test.functor) test.ctx + |> den.lib.aspects.resolve "nixos" [ ] + |> (nixos: inputs.nixpkgs.lib.evalModules { modules = [ nixos ]; }) + |> (x: x.config.test) + ); + + expected = { + atLeastHost = [ + "igloo-inner" + "igloo-outer" + ]; + exactlyHost = [ + "igloo-inner" + "igloo-outer" + ]; + exactlyHostUser = [ + "igloo-tux-inner" + "igloo-tux-outer" + ]; + upToHost = [ + "igloo-inner" + "igloo-outer" + ]; + upToHostUser = [ + "igloo-tux-inner" + "igloo-inner" + "igloo-tux-outer" + "igloo-outer" + ]; + }; + } + ); }; }