WIP: feat: Namespaced includes #3

Closed
austreelis wants to merge 6 commits from austreelis/feat/namespaced-includes into main AGit
2 changed files with 86 additions and 46 deletions
Showing only changes of commit feb9987300 - Show all commits

View file

@ -142,6 +142,18 @@ lib: {
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
in in
invalid == { }; invalid == { };
## Check that an include object has the required attributes and otherwise
## only specifies supported attributes.
##
## @type (Attrs | String | Path | Function) -> Bool
include =
include:
let
invalidKeys = builtins.removeAttrs include lib.modules.VALID_INCLUDE_KEYS;
namespaceIsString = include ? namespace -> builtins.isString include.namespace;
in
builtins.isAttrs include -> (include ? module && namespaceIsString && invalidKeys == { });
}; };
## Modules only support certain keys at the root level. This list determines ## Modules only support certain keys at the root level. This list determines
@ -159,6 +171,15 @@ lib: {
"meta" "meta"
]; ];
## Include objects only support certain keys. This list determines the
## valid attributes that users can supply.
##
## @type List String
VALID_INCLUDE_KEYS = [
"module"
"namespace"
austreelis marked this conversation as resolved
Review

There may be issues with this approach if we end up doing shorthand syntax for all modules. I don't think that we will, but submodules do support it for convenience. I wonder if we can have a different kind of object used for namespaces includes. What about something like:

includes = [
  { namespace = "my-namespace"; module = ./mod.nix }
];

At the very least we can switch on whether include ? namespace is a thing.

There may be issues with this approach if we end up doing shorthand syntax for all modules. I don't _think_ that we will, but submodules do support it for convenience. I wonder if we can have a different kind of object used for namespaces includes. What about something like: ```nix includes = [ { namespace = "my-namespace"; module = ./mod.nix } ]; ``` At the very least we can switch on whether `include ? namespace` is a thing.
];
## Normalize a module to a standard structure. All other information will be ## Normalize a module to a standard structure. All other information will be
## lost in the conversion. ## lost in the conversion.
## ##
@ -168,48 +189,59 @@ lib: {
let let
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS; invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid); invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
throwError = msg: builtins.throw ("Module ${key} (${file}) " + msg);
__key__ = builtins.toString module.__key__ or key; __key__ = builtins.toString module.__key__ or key;
normalizeIncludes = normalizeIncludes =
includes: includes:
let let
flattened = builtins.concatMap ( normalized = lib.lists.mapWithIndex1 (
include: n: include:
if let
builtins.isAttrs include invalid = builtins.removeAttrs module lib.modules.VALID_INCLUDE_KEYS;
&& include != { } invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
&& !builtins.any (n: builtins.elem n lib.modules.VALID_KEYS) (builtins.attrNames include) hasInvalidKeys = invalid != { };
then throwError' = msg: throwError ("has invalid include at position ${builtins.toString n}" + msg);
lib.attrs.mapToList (namespace: module: { in
inherit namespace; if lib.modules.validate.include include then
austreelis marked this conversation as resolved
Review

nit: maybe a name like createNamespacedModule would be informative here?

nit: maybe a name like `createNamespacedModule` would be informative here?
include = module; if builtins.isAttrs include then
}) include
else
[
{ {
inherit include; inherit (include) module;
namespace = include.namespace or null;
}
else
{
module = include;
namespace = null; namespace = null;
} }
] else if !include ? module then
) (lib.lists.when (includes != { }) includes); {
module = include;
namespace = null;
}
austreelis marked this conversation as resolved
Review

Is this comment needed?

Is this comment needed?
Review

No, I thought I left it out when cleaning up my patch.

No, I thought I left it out when cleaning up my patch.
else if hasInvalidKeys then
throwError' " with unsupported attribute(s): ${invalidKeys}"
else
austreelis marked this conversation as resolved
Review

Can we extract the builtins.map doNamespace flattened bit to a variable so that this value is easier to read? Something like builtins.seq throwOnConflict namespacedModules might make it easier to understand.

Can we extract the `builtins.map doNamespace flattened` bit to a variable so that this value is easier to read? Something like `builtins.seq throwOnConflict namespacedModules` might make it easier to understand.
throwError' ": namespace is not a string"
) includes;
throwOnConflict = throwOnConflict =
let let
filter = filter =
austreelis marked this conversation as resolved
Review

Can we wrap usage of or in parens here? It can be a bit confusing otherwise.

Can we wrap usage of `or` in parens here? It can be a bit confusing otherwise.
namespaces: namespaces:
{ namespace, include }: { namespace, ... }:
if namespace != null && builtins.elem namespace namespaces then if namespace != null && builtins.elem namespace namespaces then
builtins.throw "Module ${key} declares several includes under the same namespace '${namespace}'" throwError "declares several includes under the same namespace '${namespace}'"
else else
namespaces ++ [ namespace ]; namespaces ++ [ namespace ];
in in
builtins.foldl' filter [ ] flattened; builtins.foldl' filter [ ] normalized;
createNamespacedModule = createNamespacedModule =
{ namespace, include }: { namespace, module, ... }:
if namespace == null then if namespace == null then
include module
else else
{ {
__key__ = "${__key__}:include-${namespace}"; __key__ = "${__key__}:include-${namespace}";
@ -217,13 +249,13 @@ lib: {
description = "options and configuration included from ${namespace}"; description = "options and configuration included from ${namespace}";
default.value = { }; default.value = { };
type = lib.types.submodules.of { type = lib.types.submodules.of {
modules = [ include ]; modules = [ module ];
description = "include ${namespace}"; description = "include ${namespace}";
}; };
}; };
}; };
namespacedModules = builtins.map createNamespacedModule flattened; namespacedModules = builtins.map createNamespacedModule normalized;
in in
builtins.seq throwOnConflict namespacedModules; builtins.seq throwOnConflict namespacedModules;
in in
@ -259,7 +291,7 @@ lib: {
withFreeform (withMeta base); withFreeform (withMeta base);
} }
else else
builtins.throw "Module `${key}` (${file}) has unsupported attribute(s): ${invalidKeys}"; throwError "has unsupported attribute(s): ${invalidKeys}";
## Convert a module that is either a function or an attribute set into ## Convert a module that is either a function or an attribute set into
## a resolved attribute set. If the module was a function then it will ## a resolved attribute set. If the module was a function then it will

View file

@ -246,28 +246,31 @@ in
in in
expected == actual; expected == actual;
"handles an empty set" =
let
expected = [ ];
actual = (lib.modules.normalize "/aux/example.nix" "example" { includes = { }; }).includes;
in
expected == actual;
"handles a mixed list" = "handles a mixed list" =
let let
expected = [ expected = [
{ } null
{ a = null; } { a = null; }
]; ];
actual = actual =
# Because includes leverage submodules, we can't match the actual # Because includes leverage submodules, we can't match the actual
# included namespaced submodule under "a". So we just assert the # included namespaced submodule under "a". So we just assert the
# namespace was gotten right and do not evaluate the included value. # namespace was gotten right and do not evaluate the included value.
builtins.map (include: builtins.mapAttrs (_: _: null) include.options or include) builtins.map
(
include:
if builtins.isAttrs include then
builtins.mapAttrs (_: _: null) include.options or include
else
include
)
(lib.modules.normalize "/aux/example.nix" "example" { (lib.modules.normalize "/aux/example.nix" "example" {
includes = [ includes = [
{ } null
{ a = null; } {
module = null;
namespace = "a";
}
]; ];
}).includes; }).includes;
in in
@ -277,8 +280,14 @@ in
let let
normalized = lib.modules.normalize "/aux/example.nix" "example" { normalized = lib.modules.normalize "/aux/example.nix" "example" {
includes = [ includes = [
{ a = { }; } {
{ a = { }; } module = null;
namespace = "a";
}
{
module = null;
namespace = "a";
}
]; ];
}; };
in in
@ -288,8 +297,8 @@ in
let let
normalized = lib.modules.normalize "/aux/example.nix" "example" { normalized = lib.modules.normalize "/aux/example.nix" "example" {
includes = [ includes = [
{ } null
{ } null
]; ];
}; };
in in
@ -298,14 +307,14 @@ in
"handles multiple without namespace" = "handles multiple without namespace" =
let let
expected = [ expected = [
{ } null
{ } null
]; ];
actual = actual =
(lib.modules.normalize "/aux/example.nix" "example" { (lib.modules.normalize "/aux/example.nix" "example" {
includes = [ includes = [
{ } null
{ } null
]; ];
}).includes; }).includes;
in in
@ -786,9 +795,8 @@ in
{ {
includes = [ includes = [
{ {
myinclude = { namespace = "myinclude";
options.msg = lib.options.create { default.value = "hello"; }; module.options.msg = lib.options.create { default.value = "hello"; };
};
} }
{ options.msg = lib.options.create { default.value = "hello"; }; } { options.msg = lib.options.create { default.value = "hello"; }; }
]; ];