feat: namespaced includes
This commit adds some logic to the modules normalization process to allow including modules under a user-defined namespace. It achieves it by: - flattening any attribute sets in `includes` that are non-empty nor contain any of a module's valid attributes (`lib.modules.VALID_KEYS`). - Erroring out on dupplicate namespaces. - Mapping namespaced includes do normal modules declaring an option `${namespace}` of the include as a submodule. This allows specifying includes in a module like: ``` { includes.mynamespace0 = ./mymodule0.nix; includes.mynamespace1 = ./mymodule1.nix; } ``` and is approximatively desugared by `lib.modules.normalize` into: ``` { includes = [ { options.mynamespace0 = lib.submodule mymodule0.nix; } { options.mynamespace1 = lib.submodule mymodule1.nix; } ]; } ``` This was inspired by nixpkgs' `lib.modules.doRename`.
This commit is contained in:
parent
8e9cb4fbe9
commit
211518bb61
|
@ -166,12 +166,69 @@ lib: {
|
|||
normalize = file: key: module: let
|
||||
invalid = builtins.removeAttrs module lib.modules.VALID_KEYS;
|
||||
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
|
||||
|
||||
__key__ = builtins.toString module.__key__ or key;
|
||||
|
||||
normalizeIncludes =
|
||||
includes:
|
||||
let
|
||||
flattened = builtins.concatMap (
|
||||
include:
|
||||
if
|
||||
builtins.isAttrs include
|
||||
&& include != { }
|
||||
&& !builtins.any (n: builtins.elem n lib.modules.VALID_KEYS) (builtins.attrNames include)
|
||||
then
|
||||
lib.attrs.mapToList (namespace: module: {
|
||||
inherit namespace;
|
||||
include = module;
|
||||
}) include
|
||||
else
|
||||
[
|
||||
{
|
||||
inherit include;
|
||||
namespace = null;
|
||||
}
|
||||
]
|
||||
) (lib.lists.when (includes != { }) includes);
|
||||
|
||||
throwOnConflict =
|
||||
let
|
||||
filter =
|
||||
namespaces:
|
||||
{ namespace, include }:
|
||||
if namespace != null && builtins.elem namespace namespaces then
|
||||
builtins.throw "Module ${key} declares several includes under the same namespace '${namespace}'"
|
||||
else
|
||||
namespaces ++ [ namespace ];
|
||||
in
|
||||
builtins.foldl' filter [ ] flattened;
|
||||
|
||||
doNamespace =
|
||||
{ namespace, include }:
|
||||
if namespace == null then
|
||||
include
|
||||
else
|
||||
{
|
||||
__key__ = "${__key__}:include-${namespace}";
|
||||
options.${namespace} = lib.options.create {
|
||||
description = "options and configuration included from ${namespace}";
|
||||
default.value = { };
|
||||
type = lib.types.submodules.of {
|
||||
modules = [ include ];
|
||||
description = "include ${namespace}";
|
||||
};
|
||||
};
|
||||
# config.${namespace} = lib.modules.override 1000 {};
|
||||
};
|
||||
in
|
||||
builtins.seq throwOnConflict builtins.map doNamespace flattened;
|
||||
in
|
||||
if lib.modules.validate.keys module
|
||||
then {
|
||||
inherit __key__;
|
||||
__file__ = builtins.toString module.__file__ or file;
|
||||
__key__ = builtins.toString module.__key__ or key;
|
||||
includes = module.includes or [];
|
||||
includes = normalizeIncludes module.includes or [ ];
|
||||
excludes = module.excludes or [];
|
||||
options = module.options or {};
|
||||
config = let
|
||||
|
|
|
@ -216,6 +216,81 @@ in {
|
|||
};
|
||||
in
|
||||
actual == expected;
|
||||
|
||||
"includes" = {
|
||||
|
||||
"handles an empty list" =
|
||||
let
|
||||
expected = [ ];
|
||||
actual = (lib.modules.normalize "/aux/example.nix" "example" { includes = [ ]; }).includes;
|
||||
in
|
||||
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" =
|
||||
let
|
||||
expected = [
|
||||
{ }
|
||||
{ a = null; }
|
||||
];
|
||||
actual =
|
||||
# Because includes leverage submodules, we can't match the actual
|
||||
# included namespaced submodule under "a". So we just assert the
|
||||
# namespace was gotten right and do not evaluate the included value.
|
||||
builtins.map (include: builtins.mapAttrs (_: _: null) include.options or include)
|
||||
(lib.modules.normalize "/aux/example.nix" "example" {
|
||||
includes = [
|
||||
{ }
|
||||
{ a = null; }
|
||||
];
|
||||
}).includes;
|
||||
in
|
||||
expected == actual;
|
||||
|
||||
"rejects conflicting namespaces" =
|
||||
let
|
||||
normalized = lib.modules.normalize "/aux/example.nix" "example" {
|
||||
includes = [
|
||||
{ a = { }; }
|
||||
{ a = { }; }
|
||||
];
|
||||
};
|
||||
in
|
||||
!(builtins.tryEval normalized.includes).success;
|
||||
|
||||
"allows multiple without namespace" =
|
||||
let
|
||||
normalized = lib.modules.normalize "/aux/example.nix" "example" {
|
||||
includes = [
|
||||
{ }
|
||||
{ }
|
||||
];
|
||||
};
|
||||
in
|
||||
(builtins.tryEval normalized.includes).success;
|
||||
|
||||
"handles multiple without namespace" =
|
||||
let
|
||||
expected = [
|
||||
{ }
|
||||
{ }
|
||||
];
|
||||
actual =
|
||||
(lib.modules.normalize "/aux/example.nix" "example" {
|
||||
includes = [
|
||||
{ }
|
||||
{ }
|
||||
];
|
||||
}).includes;
|
||||
in
|
||||
expected == actual;
|
||||
};
|
||||
};
|
||||
|
||||
"resolve" = {
|
||||
|
@ -711,5 +786,29 @@ in {
|
|||
in
|
||||
(evaluated.config.aux.message == evaluated.config.aux.message2)
|
||||
&& evaluated.config.aux.exists;
|
||||
|
||||
"namespaced includes" =
|
||||
let
|
||||
expected = {
|
||||
msg = "hello";
|
||||
myinclude.msg = "hello";
|
||||
};
|
||||
evaluated = lib.modules.run {
|
||||
modules = [
|
||||
{
|
||||
includes = [
|
||||
{
|
||||
myinclude = {
|
||||
options.msg = lib.options.create { default.value = "hello"; };
|
||||
};
|
||||
}
|
||||
{ options.msg = lib.options.create { default.value = "hello"; }; }
|
||||
];
|
||||
}
|
||||
];
|
||||
};
|
||||
actual = evaluated.config;
|
||||
in
|
||||
expected == actual;
|
||||
};
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue