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
87 changes: 65 additions & 22 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,9 +35,7 @@ The following goes recursively through `./modules` and imports all `.nix` files.
}
```

For more advanced usage, `import-tree` can be configured via its builder API.
This means that the result of calling a function on an `import-tree` object
is itself another `import-tree` object.
For more advanced usage, `import-tree` can be configured via its extensible API.

</summary>

Expand Down Expand Up @@ -94,20 +92,28 @@ The following is valid usage:
```

As an special case, when the single argument given to an `import-tree` object is an
attribute-set *meaning it is _NOT_ a path or list of paths*, the `import-tree` object
assumes it is being imported as a module. This way, a pre-configured `import-tree` can
attribute-set *-it is _NOT_ a path or list of paths-*, the `import-tree` object
assumes it is being evaluated as a module. This way, a pre-configured `import-tree` can
also be used directly in a list of module imports.

This is useful for authors exposing pre-configured `import-tree`s that users can directly
add to their import list or continue configuring themselves using the API.
add to their import list or continue configuring themselves using its API.

```nix
let
# imagine this configured tree is actually provided by some flake or library.
# users can directly import it or continue using API methods on it.
configured-tree = import-tree.addPath [./a [./b]]; # paths are configured by library author.
# imagine this configured tree comes from some author's flake or library.
# library author can extend an import-tree with custom API methods
# according to the library's directory and file naming conventions.
configured-tree = import-tree.addAPI {
# the knowledge of where modules are located inside the library structure
# or which filters/regexes/transformations to apply are abstracted
# from the user by the author providing a meaningful API.
maximal = self: self.addPath ./modules;
minimal = self: self.maximal.filtered (lib.hasInfix "minimal");
};
in {
imports = [ configured-tree ]; # but then imported or further configured by the library user.
# the library user can directly import or further configure an import-tree.
imports = [ configured-tree.minimal ];
}
```

Expand Down Expand Up @@ -221,6 +227,26 @@ This function can be applied multiple times.
import-tree [./vendor ./modules]
```

### `import-tree.addAPI`

`addAPI` extends the current import-tree object with new methods.
The API is cumulative, meaning that this function can be called multiple times.

`addAPI` takes an attribute set of functions taking a single argument:
`self` which is the current import-tree object.

```nix
# import-tree.addAPI : api-attr-set -> import-tree

import-tree.addAPI {
maximal = self: self.addPath ./modules;
feature = self: featureName: self.maximal.filtered (lib.hasInfix feature);
minimal = self: self.feature "minimal";
}
```

on the previous API, users can call `import-tree.feature "+vim"` or `import-tree.minimal`, etc.

### `import-tree.withLib`

> \[!NOTE\]
Expand Down Expand Up @@ -283,26 +309,42 @@ to learn about the Dendritic pattern advantages.

### Sharing pre-configured subtrees of modules

Since the import-tree API lets you prepend paths and filter which files to include,
you could have flakes that output different sets of pre-configured trees.
<details>
<summary>

Since the import-tree API is _extensible_ and lets you add paths or
filters at configuration time, library authors can provide pre-configured
import-trees and custom API methods for their particular needs.

</summary>

This would allow us to have community-driven *sets* of configurations,
much like those popular for editors: spacemacs/lazy-vim distributions.

Imagine an editor distribution exposing the following `lib.trees` flake output:
Imagine an editor distribution exposing the following flake output:

```nix
# editor-distro's flakeModule
{inputs, lib, ...}:
let
flake.lib.trees = {
inherit root on off xor;
};
flake.lib.modules-tree = lib.pipe import-tree [
(i: i.addPath ./modules)
(i: i.addAPI { inherit hadDirMatching on off exclusive; })
(i: i.addAPI { ruby = self: self.on "ruby"; })
(i: i.addAPI { python = self: self.on "python"; })
(i: i.addAPI { old-school = self: self.off "copilot"; })
(i: i.addAPI { vim-btw = self: self.exclusive "vim" "emacs"; })
];

hasDirMatching = self: re: self.matching ".*/.*?${re}.*/.*";

root = inputs.import-tree.addPath ./modules;
on = self: flagName: self.hasDirMatching "\+${flagName}";
off = self: flagName: self.hasDirMatching "\-${flagName}";

on = flag: tree: tree.filter (lib.hasInfix "/+${flag}/");
off = flag: tree: tree.filter (lib.hasInfix "/-${flag}/");
exclusive = self: onFlag: offFlag: lib.pipe self [
(self: self.on onFlag)
(self: self.off offFlag)
];
in
{
inherit flake;
Expand All @@ -314,15 +356,16 @@ Users of such distribution can do:
```nix
# consumer flakeModule
{inputs, lib, ...}: let
inherit (inputs.editor-distro.lib.trees) on off root;
ed-tree = inputs.editor-distro.lib.modules-tree;
in {
imports = [
# files inside +vim -emacs directories
(lib.pipe root [(on "vim") (off "emacs") (i: i.result)])
(ed-tree.vim-btw.old-school.on "rust")
];
}
```

</details>

## Testing

`import-tree` uses [`checkmate`](https://github.com/vic/checkmate) for testing.
Expand Down
20 changes: 20 additions & 0 deletions checkmate.nix
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,26 @@ in
];
};

addAPI."test extends the API available on an import-tree object" = {
expr =
let
extended = lit.addAPI { helloOption = self: self.addPath ./tree/modules/hello-option; };
in
extended.helloOption.leafs.result;
expected = [ ./tree/modules/hello-option/mod.nix ];
};

addAPI."test preserves previous API extensions on an import-tree object" = {
expr =
let
first = lit.addAPI { helloOption = self: self.addPath ./tree/modules/hello-option; };
second = first.addAPI { helloWorld = self: self.addPath ./tree/modules/hello-world; };
extended = second.addAPI { res = self: self.helloOption.leafs.result; };
in
extended.res;
expected = [ ./tree/modules/hello-option/mod.nix ];
};

pipeTo."test pipes list into a function" = {
expr = (lit.mapWith lib.pathType).pipeTo (lib.length) ./tree/x;
expected = 1;
Expand Down
38 changes: 23 additions & 15 deletions default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -73,28 +73,36 @@ let
let
__config = {
# Accumulated configuration
api = { };
mapf = (i: i);
filterf = _: true;
paths = [ ];

__functor = self: f: {
__config = (f self);
__functor = functor;
__functor =
self: f:
let
__config = (f self);
in
__config.api
// {
inherit __config;
__functor = functor;

# 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));
addPath = path: self (c: mapAttr (f c) "paths" (p: p ++ [ path ]));
# 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));
addPath = path: self (c: mapAttr (f c) "paths" (p: p ++ [ path ]));
addAPI = api: self (c: mapAttr (f c) "api" (a: a // builtins.mapAttrs (_: g: g (self f)) api));

# 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); });
# 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); });

# Applies empty (for already path-configured trees)
result = (self f) [ ];
};
# Applies empty (for already path-configured trees)
result = (self f) [ ];
};
};
in
__config (c: c);
Expand Down