Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
80 changes: 64 additions & 16 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,51 +102,58 @@ lib.pipe import-tree [
]
```

Here is a less readable equivalent:
Here is a simpler but less readable equivalent:

```nix
((import-tree.mapWith lib.traceVal).filtered (lib.hasInfix ".mod.")) ./modules
```

### `import-tree.withLib`
### `import-tree.filtered`

`filtered` takes a predicate function `path -> bool`. Only paths for which the filter returns `true` are selected:

> \[!NOTE\]
> `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo`.
> Because with the use of those functions the implementation does not have access to a `lib` that is provided as a module argument.
> Only files with suffix `.nix` are candidates.

```nix
# import-tree.withLib : lib -> import-tree
# import-tree.filtered : (path -> bool) -> import-tree

import-tree.withLib pkgs.lib
import-tree.filtered (lib.hasInfix ".mod.") ./some-dir
```

### `import-tree.filtered`
`filtered` can be applied multiple times, in which case only the files matching _all_ filters will be selected:

`filtered` takes a predicate function `path -> bool`. `true` means included.
```nix
lib.pipe import-tree [
(i: i.filtered (lib.hasInfix ".mod."))
(i: i.filtered (lib.hasSuffix "default.nix"))
(i: i ./some-dir)
]
```

> \[!NOTE\]
> Only files with suffix `.nix` are candidates.
Or, in a simpler but less readable way:

```nix
# import-tree.filtered : (path -> bool) -> import-tree

import-tree.filtered (lib.hasInfix ".mod.") ./some-dir
(import-tree.filtered (lib.hasInfix ".mod.")).filtered (lib.hasSuffix "default.nix") ./some-dir
```

### `import-tree.matching`

`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`;
`matching` takes a regular expression. The regex should match the full path for the path to be selected. Matching is done with `builtins.match`.

```nix
# import-tree.matching : regex -> import-tree

import-tree.matching ".*/[a-z]+@(foo|bar)\.nix" ./some-dir
```

`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.

### `import-tree.mapWith`

`mapWith` can be used to transform each path by providing a function.
e.g. to convert the path into a module explicitly.

e.g. to convert the path into a module explicitly:

```nix
# import-tree.mapWith : (path -> any) -> import-tree
Expand All @@ -158,6 +165,37 @@ import-tree.mapWith (path: {
})
```

`mapWith` can be applied multiple times, composing the transformations:

```nix
lib.pipe import-tree [
(i: i.mapWith (lib.removeSuffix ".nix"))
(i: i.mapWith builtins.stringLength)
] ./some-dir
```

The above example first removes the `.nix` suffix from all selected paths, then takes their lengths.

Or, in a simpler but less readable way:

```nix
((import-tree.mapWith (lib.removeSuffix ".nix")).mapWith builtins.stringLength) ./some-dir
```

`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.

### `import-tree.withLib`

> \[!NOTE\]
> `withLib` is required prior to invocation of any of `.leafs` or `.pipeTo`.
> Because with the use of those functions the implementation does not have access to a `lib` that is provided as a module argument.

```nix
# import-tree.withLib : lib -> import-tree

import-tree.withLib pkgs.lib
```

### `import-tree.pipeTo`

`pipeTo` takes a function that will receive the list of paths.
Expand All @@ -171,7 +209,7 @@ import-tree.pipeTo lib.id # equivalent to `.leafs`

### `import-tree.leafs`

`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.
`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.

```nix
# import-tree.leafs : import-tree
Expand Down Expand Up @@ -226,3 +264,13 @@ So, clearly this pattern is not for every situation, but most likely for sharing
However, one advantage of this is that the dependency tree would be flat,
giving the final user's flake absolute control on what inputs are used,
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.

## Testing

`import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing.

The test suite can be found in [`checkmate.nix`](checkmate.nix). To run it locally:

```sh
nix flake check ./checkmate
```
25 changes: 25 additions & 0 deletions checkmate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,11 @@ in
expected = [ ./tree/a/b/m.nix ];
};

filtered."test multiple `filtered`s compose" = {
expr = ((lit.filtered (lib.hasInfix "b/")).filtered (lib.hasInfix "_")).leafs ./tree;
expected = [ ./tree/a/b/b_a.nix ];
};

matching."test returns empty if no files matching regex" = {
expr = (lit.matching "badregex").leafs ./tree;
expected = [ ];
Expand All @@ -54,11 +59,31 @@ in
];
};

matching."test `matching` composes with `filtered`" = {
expr = ((lit.matching ".*/[^/]+_[^/]+\.nix").filtered (lib.hasSuffix "b.nix")).leafs ./tree;
expected = [ ./tree/a/a_b.nix ];
};

matching."test multiple `matching`s compose" = {
expr = ((lit.matching ".*/[^/]+_[^/]+\.nix").matching ".*b\.nix").leafs ./tree;
expected = [ ./tree/a/a_b.nix ];
};

mapWith."test transforms each matching file with function" = {
expr = (lit.mapWith import).leafs ./tree/x;
expected = [ "z" ];
};

mapWith."test `mapWith` composes with `filtered`" = {
expr = ((lit.filtered (lib.hasInfix "/x")).mapWith import).leafs ./tree;
expected = [ "z" ];
};

mapWith."test multiple `mapWith`s compose" = {
expr = ((lit.mapWith import).mapWith builtins.stringLength).leafs ./tree/x;
expected = [ 1 ];
};

pipeTo."test pipes list into a function" = {
expr = (lit.mapWith lib.pathType).pipeTo (lib.length) ./tree/x;
expected = 1;
Expand Down
65 changes: 31 additions & 34 deletions default.nix
Original file line number Diff line number Diff line change
@@ -1,10 +1,8 @@
let

perform =
{
lib ? null,
filter ? null,
regex ? null,
filterf ? null,
mapf ? null,
pipef ? null,
...
Expand All @@ -29,56 +27,55 @@ let
leafs =
lib: root:
let
isNixFile = lib.hasSuffix ".nix";
notIgnored = p: !lib.hasInfix "/_" p;
matchesRegex = a: b: (lib.strings.match a b) != null;

stringFilter = f: path: f (builtins.toString path);
filterWithS = f: lib.filter (stringFilter f);

userFilter =
if filter != null then
filter
else if regex != null then
matchesRegex regex
else
(_: true);

mapped = if mapf != null then lib.map mapf else (i: i);

initialFilter = p: lib.hasSuffix ".nix" p && !lib.hasInfix "/_" p;
in
lib.pipe root [
(lib.toList)
(lib.lists.flatten)
(lib.map lib.filesystem.listFilesRecursive)
(map lib.filesystem.listFilesRecursive)
(lib.lists.flatten)
(filterWithS isNixFile)
(filterWithS notIgnored)
(filterWithS userFilter)
(mapped)
(builtins.filter (compose (and filterf initialFilter) toString))
(map mapf)
];

in
result;

compose =
g: f: x:
g (f x);

# Applies the second function first, to allow partial application when building the configuration.
and =
g: f: x:
f x && g x;

matchesRegex = re: p: builtins.match re p != null;

mapAttr =
attrs: k: f:
attrs // { ${k} = f attrs.${k}; };

functor = self: perform self.config;

callable =
let
config = {
# Accumulated configuration
mapf = (i: i);
filterf = _: true;

__functor = self: f: {
config = (f self);
__functor = functor;

withLib = lib: self (c: (f c) // { inherit lib; });

filtered = filter: self (c: (f c) // { inherit filter; });

matching = regex: self (c: (f c) // { inherit regex; });

mapWith = mapf: self (c: (f c) // { inherit mapf; });
# Configuration updates (accumulating)
filtered = filterf: self (c: mapAttr (f c) "filterf" (and filterf));
matching = regex: self (c: mapAttr (f c) "filterf" (and (matchesRegex regex)));
mapWith = mapf: self (c: mapAttr (f c) "mapf" (compose mapf));

# Configuration updates (non-accumulating)
withLib = lib: self (c: (f c) // { inherit lib; });
pipeTo = pipef: self (c: (f c) // { inherit pipef; });

leafs = self (c: (f c) // { pipef = (i: i); });
};
};
Expand Down