zaphyra's git: haumea

fork of https://github.com/nix-community/haumea

commit 94d3322007b5cab8ec5539fabc7848fe5270b1c0
parent 7ee8f8c83df6a0eb66de6d8c4e27b16fdc4ddd32
Author: David Arnold <dgx.arnold@gmail.com>
Date: Mon, 10 Apr 2023 16:13:09 -0500

hoist list (#3)

load: make transformer accept cursor as an argument

load: accept a list of transformers

transformers: add hoistLists

Co-authored-by: figsoda <figsoda@pm.me>
11 files changed, 141 insertions(+), 8 deletions(-)
diff --git a/README.md b/README.md
@@ -41,9 +41,9 @@ Arguments:
   `self`, `super`, and `root` are reserved names that cannot be passed as an input.
   To work around that, remove them using `removeAttrs`, or pass them by overriding the loader.
 
-- (optional) `transformer` : `{ ... } -> a`
+- (optional) `transformer` : `(cursor : [ String ]) -> { ... } -> a`
 
-  Module transformer, defaults to `id` (no transformation).
+  Module transformer, defaults to `_: id` (no transformation).
   This will transform each directory module in `src`, including the root.
 
 The main entry point of haumea. This is probably the function you are looking for.

@@ -168,12 +168,29 @@ It is useful when the files being loaded are mostly functions that don't require
 
 ### [`transformers.liftDefault`](src/transformers/liftDefault.nix)
 
-Type: `{ ... } -> { ... }`
+Type: `[ String ]: { ... } -> { ... }`
 
 This transformer will lift the contents of `default` into the module.
 It will fail if `default` is not an attribute set,
 or has any overlapping attributes with the module.
 
+### [`transformers.hoistLists`](src/transformers/hoistLists.nix)
+
+Type: `(from : String) -> (to : String) -> [ String ] -> { ... } -> { ... }`
+
+This transformer will hoist any attribute of type List with key
+`${from}` up the chain. When the root node is reached, it will
+be renamed to an attribute of type List with key `${to}` and
+as such presented back to the consumer.
+
+Neighbouring lists are concatenated (`++`) during hoisting.
+Root doesn't concat `${from}` declarations, use `${to}` at
+the root.
+
+This can be used to declare `imports` locally at the leaves
+of the configuration tree, where the module system would
+not otherwise tolerate them.
+
 ## Alternatives
 
 [std](https://github.com/divnix/std) is a more full-featured framework that also has filesystem-based auto-importing.
diff --git a/src/load.nix b/src/load.nix
@@ -23,6 +23,7 @@ let
     pipe
     remove
     take
+    toList
     ;
 
   parsePath = suffix: path:

@@ -43,7 +44,7 @@ let
 
   view = { cursor ? [ ], node, pov, transformer }:
     if node.isDir then
-      transformer
+      transformer cursor
         (flip concatMapAttrs node.children
           (name: node: optionalAttrs
             {

@@ -121,8 +122,12 @@ in
 { src
 , loader ? root.loaders.default
 , inputs ? { }
-, transformer ? id
+, transformer ? _: id
 }:
+let
+  transformer' = cursor: flip pipe
+    (map (t: t cursor) (toList transformer));
+in
 
 assert all
   (name: inputs ? ${name}

@@ -130,15 +135,16 @@ assert all
   [ "self" "super" "root" ];
 
 view {
-  inherit transformer;
   pov = "external";
+  transformer = transformer';
   node = fix (node: {
     isDir = true;
     children = aggregate {
       inherit src loader inputs;
       tree = {
-        inherit node transformer;
         pov = [ ];
+        transformer = transformer';
+        inherit node;
       };
     };
   });
diff --git a/src/transformers/_utils/concatMapAttrsWith.nix b/src/transformers/_utils/concatMapAttrsWith.nix
@@ -0,0 +1,30 @@
+{ lib }:
+
+# map each attribute in the given set into
+# a list of attributes and subsequently merge them into
+# a new attribute set with the specified mergeFun.
+
+# Type: ({ ... } -> { ... } -> { ... }) -> (String -> a -> { ... }) -> { ... } -> { ... }
+
+# Example:
+#   concatMapAttrsWith (mergeAttrsButConcatOn "mykey")
+#     (name: value: {
+#       ${name} = value;
+#       ${key} = value ++ value;
+#     })
+#     { x = "a"; y = "b"; }
+#   => { x = "a"; y = "b"; mykey = [ "aa" "bb"]; }
+
+let
+  inherit (builtins)
+    attrValues
+    foldl'
+    mapAttrs
+    ;
+  inherit (lib)
+    flip
+    pipe
+    ;
+in
+
+merge: f: flip pipe [ (mapAttrs f) attrValues (foldl' merge { }) ]
diff --git a/src/transformers/hoistLists.nix b/src/transformers/hoistLists.nix
@@ -0,0 +1,47 @@
+{ lib, super }:
+
+from: to:
+
+# Example from / to
+# - Lifting `imports` from: _imports, to: imports
+#
+# Note:
+#   underscore used as mere convention to signalling to the user the "private"
+#   nature, they won't be part of the final view presented to the user
+
+let
+  inherit (builtins)
+    removeAttrs
+    ;
+  inherit (lib)
+    catAttrs
+    concatLists
+    ;
+  inherit (super.utils)
+    concatMapAttrsWith
+    ;
+
+  # merge attributes shallowly, but concat values of a specific key into a list in that key
+  # Type: ((key : String) -> { ... } -> { ... }) -> { ${key} : [ a ], ... }
+  mergeAttrsButConcatOn = key: x: y:
+    x // y // {
+      ${key} = concatLists (catAttrs key [ x y ]);
+    };
+in
+
+cursor:
+
+if cursor == [ ] # toplevel
+then
+  concatMapAttrsWith (mergeAttrsButConcatOn to)
+    (file: value: if ! value ? ${from} then { ${file} = value; } else {
+      ${file} = removeAttrs value [ from ];
+      # top level ${from} declarations are omitted from merging
+      ${to} = value.${from};
+    })
+else
+  concatMapAttrsWith (mergeAttrsButConcatOn from)
+    (file: value: if ! value ? ${from} then { ${file} = value; } else {
+      ${file} = removeAttrs value [ from ];
+      ${from} = value.${from};
+    })
diff --git a/src/transformers/liftDefault.nix b/src/transformers/liftDefault.nix
@@ -6,7 +6,7 @@ let
     ;
 in
 
-mod:
+_: mod:
 
 unionOfDisjoint
   (removeAttrs mod [ "default" ])
diff --git a/tests/hoistLists/__fixture/bar.nix b/tests/hoistLists/__fixture/bar.nix
@@ -0,0 +1,3 @@
+{
+  _imports = [ "bar" ];
+}
diff --git a/tests/hoistLists/__fixture/baz/default.nix b/tests/hoistLists/__fixture/baz/default.nix
@@ -0,0 +1,4 @@
+{
+  qux = "qux";
+  _imports = [ "baz" ];
+}
diff --git a/tests/hoistLists/__fixture/default.nix b/tests/hoistLists/__fixture/default.nix
@@ -0,0 +1,3 @@
+{
+  imports = [ "root" ];
+}
diff --git a/tests/hoistLists/__fixture/foo.nix b/tests/hoistLists/__fixture/foo.nix
@@ -0,0 +1 @@
+"foo"
diff --git a/tests/hoistLists/expected.nix b/tests/hoistLists/expected.nix
@@ -0,0 +1,6 @@
+{
+  imports = [ "bar" "baz" "root" ];
+  foo = "foo";
+  bar = { };
+  baz = { qux = "qux"; };
+}
diff --git a/tests/hoistLists/expr.nix b/tests/hoistLists/expr.nix
@@ -0,0 +1,16 @@
+{ haumea }:
+
+let
+  inherit (haumea.transformers)
+    liftDefault
+    hoistLists
+    ;
+in
+
+haumea.load {
+  src = ./__fixture;
+  transformer = [
+    liftDefault
+    (hoistLists "_imports" "imports")
+  ];
+}