diff --git a/lib/attrsets.nix b/lib/attrsets.nix index 8482887023f76..9cd70f79a5484 100644 --- a/lib/attrsets.nix +++ b/lib/attrsets.nix @@ -4,7 +4,8 @@ { lib }: let - inherit (builtins) head length; + inherit (builtins) head length typeOf; + inherit (lib.asserts) assertMsg; inherit (lib.trivial) oldestSupportedReleaseIsAtLeast mergeAttrs @@ -1185,6 +1186,125 @@ rec { in recurse [ ] set; + /** + Apply a function to each leaf (non‐attribute‐set attribute) of a tree of + nested attribute sets, returning the results of the function as a list, + ordered lexicographically by their attribute paths. + + Like `mapAttrsRecursive`, but concatenates the mapping function results + into a list. + + # Inputs + + `f` + + : Mapping function which, given an attribute’s path and value, returns a + new value. + + This value will be an element of the list returned by + `mapAttrsToListRecursive`. + + The first argument to the mapping function is a list of attribute names + forming the path to the leaf attribute. The second argument is the leaf + attribute value, which will never be an attribute set. + + `set` + + : Attribute set to map over. + + # Type + + ``` + mapAttrsToListRecursive :: ([String] -> a -> b) -> AttrSet -> [b] + ``` + + # Examples + :::{.example} + ## `lib.attrsets.mapAttrsToListRecursive` usage example + + ```nix + mapAttrsToListRecursive (path: value: "${concatStringsSep "." path}=${value}") + { n = { a = "A"; m = { b = "B"; c = "C"; }; }; d = "D"; } + => [ "n.a=A" "n.m.b=B" "n.m.c=C" "d=D" ] + ``` + ::: + */ + mapAttrsToListRecursive = mapAttrsToListRecursiveCond (_: _: true); + + /** + Determine the nodes of a tree of nested attribute sets by applying a + predicate, then apply a function to the leaves, returning the results + as a list, ordered lexicographically by their attribute paths. + + Like `mapAttrsToListRecursive`, but takes an additional predicate to + decide whether to recurse into an attribute set. + + Unlike `mapAttrsRecursiveCond` this predicate receives the attribute path + as its first argument, in addition to the attribute set. + + # Inputs + + `pred` + + : Predicate to decide whether to recurse into an attribute set. + + If the predicate returns true, `mapAttrsToListRecursiveCond` recurses into + the attribute set. If the predicate returns false, it does not recurse + but instead applies the mapping function, treating the attribute set as + a leaf. + + The first argument to the predicate is a list of attribute names forming + the path to the attribute set. The second argument is the attribute set. + + `f` + + : Mapping function which, given an attribute’s path and value, returns a + new value. + + This value will be an element of the list returned by + `mapAttrsToListRecursiveCond`. + + The first argument to the mapping function is a list of attribute names + forming the path to the leaf attribute. The second argument is the leaf + attribute value, which may be an attribute set if the predicate returned + false. + + `set` + + : Attribute set to map over. + + # Type + ``` + mapAttrsToListRecursiveCond :: ([String] -> AttrSet -> Bool) -> ([String] -> a -> b) -> AttrSet -> [b] + ``` + + # Examples + :::{.example} + ## `lib.attrsets.mapAttrsToListRecursiveCond` usage example + + ```nix + mapAttrsToListRecursiveCond + (path: as: !(lib.isDerivation as)) + (path: value: "--set=${lib.concatStringsSep "." path}=${toString value}") + { + rust.optimize = 2; + target = { + riscv64-unknown-linux-gnu.linker = pkgs.lld; + }; + } + => [ "--set=rust.optimize=2" "--set=target.riscv64-unknown-linux-gnu.linker=/nix/store/sjw4h1k…" ] + ``` + ::: + */ + mapAttrsToListRecursiveCond = + pred: f: set: + let + mapRecursive = + path: value: if isAttrs value && pred path value then recurse path value else [ (f path value) ]; + recurse = path: set: concatMap (name: mapRecursive (path ++ [ name ]) set.${name}) (attrNames set); + in + recurse [ ] set; + /** Generate an attribute set by mapping a function over a list of attribute names. diff --git a/lib/default.nix b/lib/default.nix index e671fcf9546d9..7c5fffc76dd62 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -206,6 +206,8 @@ let concatMapAttrs mapAttrsRecursive mapAttrsRecursiveCond + mapAttrsToListRecursive + mapAttrsToListRecursiveCond genAttrs isDerivation toDerivation diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index f5f1fb5e7c2d5..e195d10eeb784 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -77,6 +77,8 @@ let makeIncludePath makeOverridable mapAttrs + mapAttrsToListRecursive + mapAttrsToListRecursiveCond mapCartesianProduct matchAttrs mergeAttrs @@ -3237,6 +3239,118 @@ runTests { ]; }; + testMapAttrsToListRecursive = { + expr = mapAttrsToListRecursive (p: v: "${concatStringsSep "." p}=${v}") { + a = { + b = "A"; + }; + c = { + d = "B"; + e = { + f = "C"; + g = "D"; + }; + }; + h = { + i = { + j = { + k = "E"; + }; + }; + }; + }; + expected = [ + "a.b=A" + "c.d=B" + "c.e.f=C" + "c.e.g=D" + "h.i.j.k=E" + ]; + }; + + testMapAttrsToListRecursiveWithLists = { + expr = mapAttrsToListRecursive (p: v: v) { + a = [ ]; + b = { + c = [ [ ] ]; + }; + d = { + e = { + f = [ [ [ ] ] ]; + }; + }; + }; + expected = [ + [ ] + [ [ ] ] + [ [ [ ] ] ] + ]; + }; + + testMapAttrsToListRecursiveCond = { + expr = mapAttrsToListRecursiveCond (p: as: !(as ? stop)) (p: v: v) { + a = { + b = "A"; + }; + c = { + d = "B"; + e = { + stop = null; + f = "C"; + g = { + h = "D"; + }; + }; + }; + }; + expected = [ + "A" + "B" + { + stop = null; + f = "C"; + g = { + h = "D"; + }; + } + ]; + }; + + testMapAttrsToListRecursiveCondPath = { + expr = mapAttrsToListRecursiveCond (p: as: length p < 2) (p: v: v) { + a = { + b = "A"; + }; + c = { + d = "B"; + e = { + f = "C"; + g = "D"; + }; + }; + h = { + i = { + j = { + k = "E"; + }; + }; + }; + }; + expected = [ + "A" + "B" + { + f = "C"; + g = "D"; + } + { + j = { + k = "E"; + }; + } + ]; + }; + # The example from the showAttrPath documentation testShowAttrPathExample = { expr = showAttrPath [