Import all nix files in a directory tree. Discussions: https://oeiuwq.zulipchat.com/join/nqp26cd4kngon6mo3ncgnuap/ dendrix.oeiuwq.com/Dendritic.html
dendritic inputs
3
fork

Configure Feed

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

Merge pull request #3 from aionescu/compose-config

Allow repeated applications of `filtered`, `matching`, and `mapWith`

authored by oeiuwq.com and committed by

GitHub 39623fae 6fcfa909

+120 -50
+64 -16
README.md
··· 102 102 ] 103 103 ``` 104 104 105 - Here is a less readable equivalent: 105 + Here is a simpler but less readable equivalent: 106 106 107 107 ```nix 108 108 ((import-tree.mapWith lib.traceVal).filtered (lib.hasInfix ".mod.")) ./modules 109 109 ``` 110 110 111 - ### `import-tree.withLib` 111 + ### `import-tree.filtered` 112 + 113 + `filtered` takes a predicate function `path -> bool`. Only paths for which the filter returns `true` are selected: 112 114 113 115 > \[!NOTE\] 114 - > `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo`. 115 - > Because with the use of those functions the implementation does not have access to a `lib` that is provided as a module argument. 116 + > Only files with suffix `.nix` are candidates. 116 117 117 118 ```nix 118 - # import-tree.withLib : lib -> import-tree 119 + # import-tree.filtered : (path -> bool) -> import-tree 119 120 120 - import-tree.withLib pkgs.lib 121 + import-tree.filtered (lib.hasInfix ".mod.") ./some-dir 121 122 ``` 122 123 123 - ### `import-tree.filtered` 124 + `filtered` can be applied multiple times, in which case only the files matching _all_ filters will be selected: 124 125 125 - `filtered` takes a predicate function `path -> bool`. `true` means included. 126 + ```nix 127 + lib.pipe import-tree [ 128 + (i: i.filtered (lib.hasInfix ".mod.")) 129 + (i: i.filtered (lib.hasSuffix "default.nix")) 130 + (i: i ./some-dir) 131 + ] 132 + ``` 126 133 127 - > \[!NOTE\] 128 - > Only files with suffix `.nix` are candidates. 134 + Or, in a simpler but less readable way: 129 135 130 136 ```nix 131 - # import-tree.filtered : (path -> bool) -> import-tree 132 - 133 - import-tree.filtered (lib.hasInfix ".mod.") ./some-dir 137 + (import-tree.filtered (lib.hasInfix ".mod.")).filtered (lib.hasSuffix "default.nix") ./some-dir 134 138 ``` 135 139 136 140 ### `import-tree.matching` 137 141 138 - `matching` takes a regular expression. The regex should match the full path for the path to be selected. Match is done with `lib.strings.match`; 142 + `matching` takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with `builtins.match`. 139 143 140 144 ```nix 141 145 # import-tree.matching : regex -> import-tree ··· 143 147 import-tree.matching ".*/[a-z]+@(foo|bar)\.nix" ./some-dir 144 148 ``` 145 149 150 + `matching` can be applied multiple times, in which case only the paths matching _all_ regex patterns will be selected, and can be combined with any number of `filtered`, in any order. 151 + 146 152 ### `import-tree.mapWith` 147 153 148 154 `mapWith` can be used to transform each path by providing a function. 149 - e.g. to convert the path into a module explicitly. 155 + 156 + e.g. to convert the path into a module explicitly: 150 157 151 158 ```nix 152 159 # import-tree.mapWith : (path -> any) -> import-tree ··· 158 165 }) 159 166 ``` 160 167 168 + `mapWith` can be applied multiple times, composing the transformations: 169 + 170 + ```nix 171 + lib.pipe import-tree [ 172 + (i: i.mapWith (lib.removeSuffix ".nix")) 173 + (i: i.mapWith builtins.stringLength) 174 + ] ./some-dir 175 + ``` 176 + 177 + The above example first removes the `.nix` suffix from all selected paths, then takes their lengths. 178 + 179 + Or, in a simpler but less readable way: 180 + 181 + ```nix 182 + ((import-tree.mapWith (lib.removeSuffix ".nix")).mapWith builtins.stringLength) ./some-dir 183 + ``` 184 + 185 + `mapWith` can be combined with any number of `filtered` and `matching` calls, in any order, but the (composed) transformation is applied _after_ the filters, and only to the paths that match all of them. 186 + 187 + ### `import-tree.withLib` 188 + 189 + > \[!NOTE\] 190 + > `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo`. 191 + > Because with the use of those functions the implementation does not have access to a `lib` that is provided as a module argument. 192 + 193 + ```nix 194 + # import-tree.withLib : lib -> import-tree 195 + 196 + import-tree.withLib pkgs.lib 197 + ``` 198 + 161 199 ### `import-tree.pipeTo` 162 200 163 201 `pipeTo` takes a function that will receive the list of paths. ··· 171 209 172 210 ### `import-tree.leafs` 173 211 174 - `leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`, that is, instead of producing a nix module, just return the list of results. 212 + `leafs` takes no arguments, it is equivalent to calling `import-tree.pipeTo lib.id`. That is, instead of producing a nix module, just return the list of results. 175 213 176 214 ```nix 177 215 # import-tree.leafs : import-tree ··· 226 264 However, one advantage of this is that the dependency tree would be flat, 227 265 giving the final user's flake absolute control on what inputs are used, 228 266 without having to worry whether some third-party forgot to use `foo.inputs.nixpkgs.follows = "nixpkgs";` on any flake we are trying to re-use. 267 + 268 + ## Testing 269 + 270 + `import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing. 271 + 272 + The test suite can be found in [`checkmate.nix`](checkmate.nix). To run it locally: 273 + 274 + ```sh 275 + nix flake check ./checkmate 276 + ```
+25
checkmate.nix
··· 41 41 expected = [ ./tree/a/b/m.nix ]; 42 42 }; 43 43 44 + filtered."test multiple `filtered`s compose" = { 45 + expr = ((lit.filtered (lib.hasInfix "b/")).filtered (lib.hasInfix "_")).leafs ./tree; 46 + expected = [ ./tree/a/b/b_a.nix ]; 47 + }; 48 + 44 49 matching."test returns empty if no files matching regex" = { 45 50 expr = (lit.matching "badregex").leafs ./tree; 46 51 expected = [ ]; ··· 54 59 ]; 55 60 }; 56 61 62 + matching."test `matching` composes with `filtered`" = { 63 + expr = ((lit.matching ".*/[^/]+_[^/]+\.nix").filtered (lib.hasSuffix "b.nix")).leafs ./tree; 64 + expected = [ ./tree/a/a_b.nix ]; 65 + }; 66 + 67 + matching."test multiple `matching`s compose" = { 68 + expr = ((lit.matching ".*/[^/]+_[^/]+\.nix").matching ".*b\.nix").leafs ./tree; 69 + expected = [ ./tree/a/a_b.nix ]; 70 + }; 71 + 57 72 mapWith."test transforms each matching file with function" = { 58 73 expr = (lit.mapWith import).leafs ./tree/x; 59 74 expected = [ "z" ]; 75 + }; 76 + 77 + mapWith."test `mapWith` composes with `filtered`" = { 78 + expr = ((lit.filtered (lib.hasInfix "/x")).mapWith import).leafs ./tree; 79 + expected = [ "z" ]; 80 + }; 81 + 82 + mapWith."test multiple `mapWith`s compose" = { 83 + expr = ((lit.mapWith import).mapWith builtins.stringLength).leafs ./tree/x; 84 + expected = [ 1 ]; 60 85 }; 61 86 62 87 pipeTo."test pipes list into a function" = {
+31 -34
default.nix
··· 1 1 let 2 - 3 2 perform = 4 3 { 5 4 lib ? null, 6 - filter ? null, 7 - regex ? null, 5 + filterf ? null, 8 6 mapf ? null, 9 7 pipef ? null, 10 8 ... ··· 29 27 leafs = 30 28 lib: root: 31 29 let 32 - isNixFile = lib.hasSuffix ".nix"; 33 - notIgnored = p: !lib.hasInfix "/_" p; 34 - matchesRegex = a: b: (lib.strings.match a b) != null; 35 - 36 - stringFilter = f: path: f (builtins.toString path); 37 - filterWithS = f: lib.filter (stringFilter f); 38 - 39 - userFilter = 40 - if filter != null then 41 - filter 42 - else if regex != null then 43 - matchesRegex regex 44 - else 45 - (_: true); 46 - 47 - mapped = if mapf != null then lib.map mapf else (i: i); 48 - 30 + initialFilter = p: lib.hasSuffix ".nix" p && !lib.hasInfix "/_" p; 49 31 in 50 32 lib.pipe root [ 51 - (lib.toList) 52 33 (lib.lists.flatten) 53 - (lib.map lib.filesystem.listFilesRecursive) 34 + (map lib.filesystem.listFilesRecursive) 54 35 (lib.lists.flatten) 55 - (filterWithS isNixFile) 56 - (filterWithS notIgnored) 57 - (filterWithS userFilter) 58 - (mapped) 36 + (builtins.filter (compose (and filterf initialFilter) toString)) 37 + (map mapf) 59 38 ]; 60 39 61 40 in 62 41 result; 63 42 43 + compose = 44 + g: f: x: 45 + g (f x); 46 + 47 + # Applies the second function first, to allow partial application when building the configuration. 48 + and = 49 + g: f: x: 50 + f x && g x; 51 + 52 + matchesRegex = re: p: builtins.match re p != null; 53 + 54 + mapAttr = 55 + attrs: k: f: 56 + attrs // { ${k} = f attrs.${k}; }; 57 + 64 58 functor = self: perform self.config; 59 + 65 60 callable = 66 61 let 67 62 config = { 63 + # Accumulated configuration 64 + mapf = (i: i); 65 + filterf = _: true; 66 + 68 67 __functor = self: f: { 69 68 config = (f self); 70 69 __functor = functor; 71 70 72 - withLib = lib: self (c: (f c) // { inherit lib; }); 73 - 74 - filtered = filter: self (c: (f c) // { inherit filter; }); 75 - 76 - matching = regex: self (c: (f c) // { inherit regex; }); 77 - 78 - mapWith = mapf: self (c: (f c) // { inherit mapf; }); 71 + # Configuration updates (accumulating) 72 + filtered = filterf: self (c: mapAttr (f c) "filterf" (and filterf)); 73 + matching = regex: self (c: mapAttr (f c) "filterf" (and (matchesRegex regex))); 74 + mapWith = mapf: self (c: mapAttr (f c) "mapf" (compose mapf)); 79 75 76 + # Configuration updates (non-accumulating) 77 + withLib = lib: self (c: (f c) // { inherit lib; }); 80 78 pipeTo = pipef: self (c: (f c) // { inherit pipef; }); 81 - 82 79 leafs = self (c: (f c) // { pipef = (i: i); }); 83 80 }; 84 81 };