summary refs log tree commit diff
path: root/lib
diff options
context:
space:
mode:
authorSilvan Mosberger <contact@infinisil.com>2020-03-20 00:17:41 +0100
committerSilvan Mosberger <contact@infinisil.com>2020-08-03 22:37:00 +0200
commit65e25deb06cef43d4868785a4b6a4a49dbb6cfe2 (patch)
treedb4c340d11a8f8608a15b758b4576df85b1f5d10 /lib
parentfd75dc876586bde8cdb683a6952a41132e8db166 (diff)
downloadnixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar.gz
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar.bz2
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar.lz
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar.xz
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.tar.zst
nixpkgs-65e25deb06cef43d4868785a4b6a4a49dbb6cfe2.zip
lib/modules: Implement freeform modules
For programs that have a lot of (Nix-representable) configuration options,
a simple way to represent this in a NixOS module is to declare an
option of a type like `attrsOf str`, representing a key-value mapping
which then gets generated into a config file. However with such a type,
there's no way to add type checking for only some key values.

On the other end of the spectrum, one can declare a single separate
option for every key value that the program supports, ending up with a module
with potentially 100s of options. This has the benefit that every value
gets type checked, catching mistakes at evaluation time already. However
the disadvantage is that the module becomes big, becomes coupled to the
program version and takes a lot of effort to write and maintain.

Previously there was a middle ground between these two
extremes: Declare an option of a type like `attrsOf str`, but declare
additional separate options for the values you wish to have type
checked, and assign their values to the `attrsOf str` option. While this
works decently, it has the problem of duplicated options, since now both
the additional options and the `attrsOf str` option can be used to set a
key value. This leads to confusion about what should happen if both are
set, which defaults should apply, and more.

Now with this change, a middle ground becomes available that solves above
problems: The module system now supports setting a freeform type, which
gets used for all definitions that don't have an associated option. This
means that you can now declare all options you wish to have type
checked, while for the rest a freeform type like `attrsOf str` can be
used.
Diffstat (limited to 'lib')
-rw-r--r--lib/modules.nix40
1 files changed, 38 insertions, 2 deletions
diff --git a/lib/modules.nix b/lib/modules.nix
index ecf5ef3f491..daffe5224ab 100644
--- a/lib/modules.nix
+++ b/lib/modules.nix
@@ -58,6 +58,23 @@ rec {
             default = check;
             description = "Whether to check whether all option definitions have matching declarations.";
           };
+
+          _module.freeformType = mkOption {
+            # Disallow merging for now, but could be implemented nicely with a `types.optionType`
+            type = types.nullOr (types.uniq types.attrs);
+            internal = true;
+            default = null;
+            description = ''
+              If set, merge all definitions that don't have an associated option
+              together using this type. The result then gets combined with the
+              values of all declared options to produce the final <literal>
+              config</literal> value.
+
+              If this is <literal>null</literal>, definitions without an option
+              will throw an error unless <option>_module.check</option> is
+              turned off.
+            '';
+          };
         };
 
         config = {
@@ -74,10 +91,29 @@ rec {
 
       options = merged.matchedOptions;
 
-      config = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options;
+      config =
+        let
+
+          # For definitions that have an associated option
+          declaredConfig = mapAttrsRecursiveCond (v: ! isOption v) (_: v: v.value) options;
+
+          # If freeformType is set, this is for definitions that don't have an associated option
+          freeformConfig =
+            let
+              defs = map (def: {
+                file = def.file;
+                value = setAttrByPath def.prefix def.value;
+              }) merged.unmatchedDefns;
+            in declaredConfig._module.freeformType.merge prefix defs;
+
+        in if declaredConfig._module.freeformType == null then declaredConfig
+          # Because all definitions that had an associated option ended in
+          # declaredConfig, freeformConfig can only contain the non-option
+          # paths, meaning recursiveUpdate will never override any value
+          else recursiveUpdate freeformConfig declaredConfig;
 
       checkUnmatched =
-        if config._module.check && merged.unmatchedDefns != [] then
+        if config._module.check && config._module.freeformType == null && merged.unmatchedDefns != [] then
           let inherit (head merged.unmatchedDefns) file prefix;
           in throw "The option `${showOption prefix}' defined in `${file}' does not exist."
         else null;