WIP: feat: Namespaced includes #3
|
@ -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
|
|||||||
|
];
|
||||||
|
|
||||||
## 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,12 +189,62 @@ 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;
|
||||||
|
|
||||||
|
normalizeIncludes =
|
||||||
|
includes:
|
||||||
|
let
|
||||||
|
normalized = lib.lists.mapWithIndex1 (
|
||||||
|
n: include:
|
||||||
|
let
|
||||||
|
invalid = builtins.removeAttrs module lib.modules.VALID_INCLUDE_KEYS;
|
||||||
|
invalidKeys = builtins.concatStringsSep ", " (builtins.attrNames invalid);
|
||||||
|
hasInvalidKeys = invalid != { };
|
||||||
|
throwError' = msg: throwError ("has invalid include at position ${builtins.toString n}" + msg);
|
||||||
|
in
|
||||||
|
if lib.modules.validate.include include then
|
||||||
austreelis marked this conversation as resolved
jakehamilton
commented
nit: maybe a name like nit: maybe a name like `createNamespacedModule` would be informative here?
|
|||||||
|
if builtins.isAttrs include then
|
||||||
|
{
|
||||||
|
inherit (include) module;
|
||||||
|
namespace = include.namespace or null;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
module = include;
|
||||||
|
namespace = null;
|
||||||
|
}
|
||||||
|
else if !include ? module then
|
||||||
|
{
|
||||||
|
module = include;
|
||||||
|
namespace = null;
|
||||||
|
}
|
||||||
austreelis marked this conversation as resolved
jakehamilton
commented
Is this comment needed? Is this comment needed?
austreelis
commented
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
jakehamilton
commented
Can we extract the 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 =
|
||||||
|
let
|
||||||
|
filter =
|
||||||
austreelis marked this conversation as resolved
jakehamilton
commented
Can we wrap usage of Can we wrap usage of `or` in parens here? It can be a bit confusing otherwise.
|
|||||||
|
namespaces:
|
||||||
|
{ namespace, ... }:
|
||||||
|
if namespace != null && builtins.elem namespace namespaces then
|
||||||
|
throwError "declares several includes under the same namespace '${namespace}'"
|
||||||
|
else
|
||||||
|
namespaces ++ [ namespace ];
|
||||||
|
in
|
||||||
|
builtins.foldl' filter [ ] normalized;
|
||||||
|
in
|
||||||
|
builtins.seq throwOnConflict normalized;
|
||||||
in
|
in
|
||||||
if lib.modules.validate.keys module then
|
if lib.modules.validate.keys module then
|
||||||
{
|
{
|
||||||
__file__ = builtins.toString module.__file__ or file;
|
inherit __key__;
|
||||||
__key__ = builtins.toString module.__key__ or key;
|
__file__ = builtins.toString (module.__file__ or file);
|
||||||
includes = module.includes or [ ];
|
includes = normalizeIncludes (module.includes or [ ]);
|
||||||
excludes = module.excludes or [ ];
|
excludes = module.excludes or [ ];
|
||||||
options = module.options or { };
|
options = module.options or { };
|
||||||
config =
|
config =
|
||||||
|
@ -201,7 +272,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
|
||||||
|
@ -497,19 +568,36 @@ lib: {
|
||||||
collect =
|
collect =
|
||||||
let
|
let
|
||||||
load =
|
load =
|
||||||
args: file: key: module:
|
args: file: key: module':
|
||||||
let
|
let
|
||||||
|
namespace = module'.namespace or null;
|
||||||
|
module = module'.module or module';
|
||||||
|
|
||||||
moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args);
|
moduleFromValue = lib.modules.normalize file key (lib.modules.resolve key module args);
|
||||||
moduleFromPath = lib.modules.normalize (builtins.toString module) (builtins.toString module) (
|
moduleFromPath = lib.modules.normalize (builtins.toString module) (builtins.toString module) (
|
||||||
lib.modules.resolve (builtins.toString module) (import module) args
|
lib.modules.resolve (builtins.toString module) (import module) args
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
|
if namespace == null then
|
||||||
if builtins.isAttrs module || builtins.isFunction module then
|
if builtins.isAttrs module || builtins.isFunction module then
|
||||||
moduleFromValue
|
moduleFromValue
|
||||||
else if builtins.isString module || builtins.isPath module then
|
else if builtins.isString module || builtins.isPath module then
|
||||||
moduleFromPath
|
moduleFromPath
|
||||||
else
|
else
|
||||||
builtins.throw "The provided module must be either an attribute set, function, or path but got ${builtins.typeOf module}";
|
builtins.throw "The provided module must be either an attribute set, function, or path but got ${builtins.typeOf module}"
|
||||||
|
# We don't resolve the module yet if we're under a namespace
|
||||||
|
else
|
||||||
|
lib.modules.normalize file key {
|
||||||
|
__key__ = "${key}:include-${namespace}";
|
||||||
|
options.${namespace} = lib.options.create {
|
||||||
|
description = "options and configuration included from ${namespace}";
|
||||||
|
default.value = { };
|
||||||
|
type = lib.types.submodules.of {
|
||||||
|
modules = [ module ];
|
||||||
|
description = "include ${namespace}";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
normalize =
|
normalize =
|
||||||
parentFile: parentKey: modules: args:
|
parentFile: parentKey: modules: args:
|
||||||
|
|
|
@ -200,6 +200,62 @@ in
|
||||||
in
|
in
|
||||||
!value;
|
!value;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
"cannot mix include and module keys" =
|
||||||
|
let
|
||||||
|
# Simple backtracking algorithm to generate all combinations of a list's
|
||||||
|
# elements without ordering, in .
|
||||||
|
combinations =
|
||||||
|
set:
|
||||||
|
let
|
||||||
|
n = builtins.length set;
|
||||||
|
genIndices =
|
||||||
|
combinations':
|
||||||
|
let
|
||||||
|
prev = builtins.head combinations';
|
||||||
|
k = builtins.length prev;
|
||||||
|
first = builtins.head prev;
|
||||||
|
next' =
|
||||||
|
# Generate the next combination of length k
|
||||||
|
if k > 0 && first + 1 < n then
|
||||||
|
[ (first + 1) ] ++ (builtins.tail prev)
|
||||||
|
# We have generated all combinations of length k, generate the first
|
||||||
|
# (trivial) combination of length k+1
|
||||||
|
else
|
||||||
|
builtins.genList (i: i) (k + 1);
|
||||||
|
next = builtins.deepSeq next' next';
|
||||||
|
in
|
||||||
|
# Generate the very first (trivial) combination
|
||||||
|
if combinations' == [ ] then
|
||||||
|
genIndices [ [ ] ]
|
||||||
|
# We have generated all combinations of length 0 to n, return
|
||||||
|
else if k >= n then
|
||||||
|
combinations'
|
||||||
|
# Generate the next combination and continue
|
||||||
|
else
|
||||||
|
genIndices ([ next ] ++ combinations');
|
||||||
|
select = indices: builtins.map (builtins.elemAt set) indices;
|
||||||
|
in
|
||||||
|
if set == [ ] then [ ] else builtins.map select (genIndices [ ]);
|
||||||
|
|
||||||
|
keys = combinations (lib.modules.VALID_KEYS ++ lib.modules.VALID_INCLUDE_KEYS);
|
||||||
|
sets = builtins.map (
|
||||||
|
names:
|
||||||
|
builtins.listToAttrs (
|
||||||
|
builtins.map (name: {
|
||||||
|
value = null;
|
||||||
|
inherit name;
|
||||||
|
}) names
|
||||||
|
)
|
||||||
|
) keys;
|
||||||
|
validated = builtins.map (set: {
|
||||||
|
inherit set;
|
||||||
|
include = lib.modules.validate.include set;
|
||||||
|
module = lib.modules.validate.keys set;
|
||||||
|
}) sets;
|
||||||
|
bothValid = { include, module, ... }: include && module;
|
||||||
|
in
|
||||||
|
!builtins.any bothValid validated;
|
||||||
};
|
};
|
||||||
|
|
||||||
"normalize" = {
|
"normalize" = {
|
||||||
|
@ -236,6 +292,91 @@ in
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
actual == expected;
|
actual == expected;
|
||||||
|
|
||||||
|
"includes" = {
|
||||||
|
|
||||||
|
"handles an empty list" =
|
||||||
|
let
|
||||||
|
expected = [ ];
|
||||||
|
actual = (lib.modules.normalize "/aux/example.nix" "example" { includes = [ ]; }).includes;
|
||||||
|
in
|
||||||
|
expected == actual;
|
||||||
|
|
||||||
|
"handles a mixed list" =
|
||||||
|
let
|
||||||
|
expected = [
|
||||||
|
{
|
||||||
|
module = { };
|
||||||
|
namespace = null;
|
||||||
|
}
|
||||||
|
{
|
||||||
|
module = { };
|
||||||
|
namespace = "a";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
actual =
|
||||||
|
(lib.modules.normalize "/aux/example.nix" "example" {
|
||||||
|
includes = [
|
||||||
|
{ }
|
||||||
|
{
|
||||||
|
module = { };
|
||||||
|
namespace = "a";
|
||||||
|
}
|
||||||
|
];
|
||||||
|
}).includes;
|
||||||
|
in
|
||||||
|
expected == actual;
|
||||||
|
|
||||||
|
"rejects conflicting namespaces" =
|
||||||
|
let
|
||||||
|
normalized = lib.modules.normalize "/aux/example.nix" "example" {
|
||||||
|
includes = [
|
||||||
|
{
|
||||||
|
module = { };
|
||||||
|
namespace = "a";
|
||||||
|
}
|
||||||
|
{
|
||||||
|
module = { };
|
||||||
|
namespace = "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 = [
|
||||||
|
{
|
||||||
|
namespace = null;
|
||||||
|
module = { };
|
||||||
|
}
|
||||||
|
{
|
||||||
|
namespace = null;
|
||||||
|
module = { };
|
||||||
|
}
|
||||||
|
];
|
||||||
|
actual =
|
||||||
|
(lib.modules.normalize "/aux/example.nix" "example" {
|
||||||
|
includes = [
|
||||||
|
{ }
|
||||||
|
{ }
|
||||||
|
];
|
||||||
|
}).includes;
|
||||||
|
in
|
||||||
|
expected == actual;
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
"resolve" = {
|
"resolve" = {
|
||||||
|
@ -699,5 +840,28 @@ in
|
||||||
};
|
};
|
||||||
in
|
in
|
||||||
(evaluated.config.aux.message == evaluated.config.aux.message2) && evaluated.config.aux.exists;
|
(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 = [
|
||||||
|
{
|
||||||
|
namespace = "myinclude";
|
||||||
|
module.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
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:
At the very least we can switch on whether
include ? namespace
is a thing.