Merge pull request #284512 from hercules-ci/lib-types-unique-merge

lib.types.unique: Check inner type deeply

Changed files
+63 -19
lib
nixos
doc
manual
+23 -5
lib/options.nix
···
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:
+10
lib/tests/modules.sh
···
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
+27
lib/tests/modules/types-unique.nix
···
+
{ 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;
+
};
+
};
+
}
+2 -13
lib/types.nix
···
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;
+1 -1
nixos/doc/manual/development/option-types.section.md
···
`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`*