diff --git a/lib/src/modules/default.nix b/lib/src/modules/default.nix index 63105ef..286564b 100644 --- a/lib/src/modules/default.nix +++ b/lib/src/modules/default.nix @@ -168,12 +168,69 @@ lib: { 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 = diff --git a/lib/src/modules/default.test.nix b/lib/src/modules/default.test.nix index ff98eb5..bbe82db 100644 --- a/lib/src/modules/default.test.nix +++ b/lib/src/modules/default.test.nix @@ -236,6 +236,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" = { @@ -699,5 +774,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; }; }