summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorSilvan Mosberger <contact@infinisil.com>2024-02-10 02:52:45 +0100
committerGitHub <noreply@github.com>2024-02-10 02:52:45 +0100
commitf37ba1976518f61217dcee655412288b09441cde (patch)
tree06392f647e18d6b283f46e49bf66abb9d1b71910
parentddc9133e53aa4a48135a6e9e50277c327ac73b72 (diff)
parent542f5d4f4d80a35d8f03aa5cf2a2a0b1a0345c41 (diff)
Merge pull request #284512 from hercules-ci/lib-types-unique-merge
lib.types.unique: Check inner type deeply
-rw-r--r--lib/options.nix28
-rwxr-xr-xlib/tests/modules.sh10
-rw-r--r--lib/tests/modules/types-unique.nix27
-rw-r--r--lib/types.nix15
-rw-r--r--nixos/doc/manual/development/option-types.section.md2
5 files changed, 63 insertions, 19 deletions
diff --git a/lib/options.nix b/lib/options.nix
index f5012848b05a..0d1d90efe217 100644
--- a/lib/options.nix
+++ b/lib/options.nix
@@ -254,13 +254,31 @@ rec {
else if all isInt list && all (x: x == head list) list then head list
else throw "Cannot merge definitions of `${showOption loc}'. Definition values:${showDefs defs}";
+ /*
+ Require a single definition.
+
+ WARNING: Does not perform nested checks, as this does not run the merge function!
+ */
mergeOneOption = mergeUniqueOption { message = ""; };
- mergeUniqueOption = { message }: loc: defs:
- if length defs == 1
- then (head defs).value
- else assert length defs > 1;
- throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
+ /*
+ Require a single definition.
+
+ NOTE: When the type is not checked completely by check, pass a merge function for further checking (of sub-attributes, etc).
+ */
+ mergeUniqueOption = args@{
+ message,
+ # WARNING: the default merge function assumes that the definition is a valid (option) value. You MUST pass a merge function if the return value needs to be
+ # - type checked beyond what .check does (which should be very litte; only on the value head; not attribute values, etc)
+ # - if you want attribute values to be checked, or list items
+ # - if you want coercedTo-like behavior to work
+ merge ? loc: defs: (head defs).value }:
+ loc: defs:
+ if length defs == 1
+ then merge loc defs
+ else
+ assert length defs > 1;
+ throw "The option `${showOption loc}' is defined multiple times while it's expected to be unique.\n${message}\nDefinition values:${showDefs defs}\n${prioritySuggestion}";
/* "Merge" option definitions by checking that they all have the same value. */
mergeEqualOption = loc: defs:
diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh
index 072b92b38365..b3bbdf9485ac 100755
--- a/lib/tests/modules.sh
+++ b/lib/tests/modules.sh
@@ -407,6 +407,16 @@ checkConfigOutput "{}" config.submodule.a ./emptyValues.nix
checkConfigError 'The option .int.a. is used but not defined' config.int.a ./emptyValues.nix
checkConfigError 'The option .nonEmptyList.a. is used but not defined' config.nonEmptyList.a ./emptyValues.nix
+# types.unique
+# requires a single definition
+checkConfigError 'The option .examples\.merged. is defined multiple times while it.s expected to be unique' config.examples.merged.a ./types-unique.nix
+# user message is printed
+checkConfigError 'We require a single definition, because seeing the whole value at once helps us maintain critical invariants of our system.' config.examples.merged.a ./types-unique.nix
+# let the inner merge function check the values (on demand)
+checkConfigError 'A definition for option .examples\.badLazyType\.a. is not of type .string.' config.examples.badLazyType.a ./types-unique.nix
+# overriding still works (unlike option uniqueness)
+checkConfigOutput '^"bee"$' config.examples.override.b ./types-unique.nix
+
## types.raw
checkConfigOutput '^true$' config.unprocessedNestingEvaluates.success ./raw.nix
checkConfigOutput "10" config.processedToplevel ./raw.nix
diff --git a/lib/tests/modules/types-unique.nix b/lib/tests/modules/types-unique.nix
new file mode 100644
index 000000000000..115be0126975
--- /dev/null
+++ b/lib/tests/modules/types-unique.nix
@@ -0,0 +1,27 @@
+{ lib, ... }:
+let
+ inherit (lib) mkOption types;
+in
+{
+ options.examples = mkOption {
+ type = types.lazyAttrsOf
+ (types.unique
+ { message = "We require a single definition, because seeing the whole value at once helps us maintain critical invariants of our system."; }
+ (types.attrsOf types.str));
+ };
+ imports = [
+ { examples.merged = { b = "bee"; }; }
+ { examples.override = lib.mkForce { b = "bee"; }; }
+ ];
+ config.examples = {
+ merged = {
+ a = "aye";
+ };
+ override = {
+ a = "aye";
+ };
+ badLazyType = {
+ a = true;
+ };
+ };
+}
diff --git a/lib/types.nix b/lib/types.nix
index 7b2062f13059..12bf18633e3a 100644
--- a/lib/types.nix
+++ b/lib/types.nix
@@ -614,23 +614,12 @@ rec {
nestedTypes.elemType = elemType;
};
- # Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
- uniq = elemType: mkOptionType rec {
- name = "uniq";
- inherit (elemType) description descriptionClass check;
- merge = mergeOneOption;
- emptyValue = elemType.emptyValue;
- getSubOptions = elemType.getSubOptions;
- getSubModules = elemType.getSubModules;
- substSubModules = m: uniq (elemType.substSubModules m);
- functor = (defaultFunctor name) // { wrapped = elemType; };
- nestedTypes.elemType = elemType;
- };
+ uniq = unique { message = ""; };
unique = { message }: type: mkOptionType rec {
name = "unique";
inherit (type) description descriptionClass check;
- merge = mergeUniqueOption { inherit message; };
+ merge = mergeUniqueOption { inherit message; inherit (type) merge; };
emptyValue = type.emptyValue;
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md
index f9c7ac80018e..04edf99e70b0 100644
--- a/nixos/doc/manual/development/option-types.section.md
+++ b/nixos/doc/manual/development/option-types.section.md
@@ -326,7 +326,7 @@ Composed types are types that take a type as parameter. `listOf
`types.uniq` *`t`*
: Ensures that type *`t`* cannot be merged. It is used to ensure option
- definitions are declared only once.
+ definitions are provided only once.
`types.unique` `{ message = m }` *`t`*