From 65e25deb06cef43d4868785a4b6a4a49dbb6cfe2 Mon Sep 17 00:00:00 2001 From: Silvan Mosberger Date: Fri, 20 Mar 2020 00:17:41 +0100 Subject: [PATCH] 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. --- lib/modules.nix | 40 ++++++++++++++++++++++++++++++++++++++-- 1 file 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 + config value. + + If this is null, definitions without an option + will throw an error unless 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;