lib.modules: init types checkAndMerge to allow adding 'valueMeta' (#391544)
This commit is contained in:
@@ -1121,6 +1121,7 @@ let
|
||||
files = map (def: def.file) res.defsFinal;
|
||||
definitionsWithLocations = res.defsFinal;
|
||||
inherit (res) isDefined;
|
||||
inherit (res.checkedAndMerged) valueMeta;
|
||||
# This allows options to be correctly displayed using `${options.path.to.it}`
|
||||
__toString = _: showOption loc;
|
||||
};
|
||||
@@ -1164,7 +1165,14 @@ let
|
||||
# Type-check the remaining definitions, and merge them. Or throw if no definitions.
|
||||
mergedValue =
|
||||
if isDefined then
|
||||
if all (def: type.check def.value) defsFinal then
|
||||
if type.merge ? v2 then
|
||||
# check and merge share the same closure
|
||||
# .headError is either not-present, null, or a string describing the error
|
||||
if checkedAndMerged.headError or null != null then
|
||||
throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError.message}"
|
||||
else
|
||||
checkedAndMerged.value
|
||||
else if all (def: type.check def.value) defsFinal then
|
||||
type.merge loc defsFinal
|
||||
else
|
||||
let
|
||||
@@ -1177,6 +1185,43 @@ let
|
||||
throw
|
||||
"The option `${showOption loc}' was accessed but has no value defined. Try setting the option.";
|
||||
|
||||
checkedAndMerged =
|
||||
(
|
||||
# This function (which is immediately applied) checks that type.merge
|
||||
# returns the proper attrset.
|
||||
# Once use of the merge.v2 feature has propagated, consider removing this
|
||||
# for an estimated one thousandth performance improvement (NixOS by nr.thunks).
|
||||
{
|
||||
headError,
|
||||
value,
|
||||
valueMeta,
|
||||
}@args:
|
||||
args
|
||||
)
|
||||
(
|
||||
if type.merge ? v2 then
|
||||
let
|
||||
r = type.merge.v2 {
|
||||
inherit loc;
|
||||
defs = defsFinal;
|
||||
};
|
||||
in
|
||||
r
|
||||
// {
|
||||
valueMeta = r.valueMeta // {
|
||||
_internal = {
|
||||
inherit type;
|
||||
};
|
||||
};
|
||||
}
|
||||
else
|
||||
{
|
||||
headError = null;
|
||||
value = mergedValue;
|
||||
valueMeta = { };
|
||||
}
|
||||
);
|
||||
|
||||
isDefined = defsFinal != [ ];
|
||||
|
||||
optionalValue = if isDefined then { value = mergedValue; } else { };
|
||||
@@ -1586,13 +1631,11 @@ let
|
||||
New option path as list of strings.
|
||||
*/
|
||||
to,
|
||||
|
||||
/**
|
||||
Release number of the first release that contains the rename, ignoring backports.
|
||||
Set it to the upcoming release, matching the nixpkgs/.version file.
|
||||
*/
|
||||
sinceRelease,
|
||||
|
||||
}:
|
||||
doRename {
|
||||
inherit from to;
|
||||
|
||||
379
lib/tests/checkAndMergeCompat.nix
Normal file
379
lib/tests/checkAndMergeCompat.nix
Normal file
@@ -0,0 +1,379 @@
|
||||
{
|
||||
pkgs ? import ../.. { },
|
||||
currLibPath ? ../.,
|
||||
prevLibPath ? "${
|
||||
pkgs.fetchFromGitHub {
|
||||
owner = "nixos";
|
||||
repo = "nixpkgs";
|
||||
# Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544)
|
||||
# Which was before the type.merge.v2 introduction
|
||||
rev = "bcf94dd3f07189b7475d823c8d67d08b58289905";
|
||||
hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI=";
|
||||
}
|
||||
}/lib",
|
||||
}:
|
||||
let
|
||||
lib = import currLibPath;
|
||||
|
||||
lib_with_merge_v2 = lib;
|
||||
lib_with_merge_v1 = import prevLibPath;
|
||||
|
||||
getMatrix =
|
||||
{
|
||||
getType ? null,
|
||||
# If getType is set this is only used as test prefix
|
||||
# And the type from getType is used
|
||||
outerTypeName,
|
||||
innerTypeName,
|
||||
value,
|
||||
testAttrs,
|
||||
}:
|
||||
let
|
||||
evalModules.call_v1 = lib_with_merge_v1.evalModules;
|
||||
evalModules.call_v2 = lib_with_merge_v2.evalModules;
|
||||
outerTypes.outer_v1 = lib_with_merge_v1.types;
|
||||
outerTypes.outer_v2 = lib_with_merge_v2.types;
|
||||
innerTypes.inner_v1 = lib_with_merge_v1.types;
|
||||
innerTypes.inner_v2 = lib_with_merge_v2.types;
|
||||
in
|
||||
lib.mapAttrs (
|
||||
_: evalModules:
|
||||
lib.mapAttrs (
|
||||
_: outerTypes:
|
||||
lib.mapAttrs (_: innerTypes: {
|
||||
"test_${outerTypeName}_${innerTypeName}" = testAttrs // {
|
||||
expr =
|
||||
(evalModules {
|
||||
modules = [
|
||||
(m: {
|
||||
options.foo = m.lib.mkOption {
|
||||
type =
|
||||
if getType != null then
|
||||
getType outerTypes innerTypes
|
||||
else
|
||||
outerTypes.${outerTypeName} innerTypes.${innerTypeName};
|
||||
default = value;
|
||||
};
|
||||
})
|
||||
];
|
||||
}).config.foo;
|
||||
};
|
||||
}) innerTypes
|
||||
) outerTypes
|
||||
) evalModules;
|
||||
in
|
||||
{
|
||||
# AttrsOf string
|
||||
attrsOf_str_ok = getMatrix {
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "str";
|
||||
value = {
|
||||
bar = "test";
|
||||
};
|
||||
testAttrs = {
|
||||
expected = {
|
||||
bar = "test";
|
||||
};
|
||||
};
|
||||
};
|
||||
attrsOf_str_err_inner = getMatrix {
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "str";
|
||||
value = {
|
||||
bar = 1; # not a string
|
||||
};
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo.bar' is not of type `string'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
attrsOf_str_err_outer = getMatrix {
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "str";
|
||||
value = [ "foo" ]; # not an attrset
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo' is not of type `attribute set of string'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# listOf string
|
||||
listOf_str_ok = getMatrix {
|
||||
outerTypeName = "listOf";
|
||||
innerTypeName = "str";
|
||||
value = [
|
||||
"foo"
|
||||
"bar"
|
||||
];
|
||||
testAttrs = {
|
||||
expected = [
|
||||
"foo"
|
||||
"bar"
|
||||
];
|
||||
};
|
||||
};
|
||||
listOf_str_err_inner = getMatrix {
|
||||
outerTypeName = "listOf";
|
||||
innerTypeName = "str";
|
||||
value = [
|
||||
"foo"
|
||||
1
|
||||
]; # not a string
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = ''A definition for option `foo."\[definition 1-entry 2\]"' is not of type `string'.'';
|
||||
};
|
||||
};
|
||||
};
|
||||
listOf_str_err_outer = getMatrix {
|
||||
outerTypeName = "listOf";
|
||||
innerTypeName = "str";
|
||||
value = {
|
||||
foo = 42;
|
||||
}; # not a list
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo' is not of type `list of string'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
attrsOf_submodule_ok = getMatrix {
|
||||
getType =
|
||||
a: b:
|
||||
a.attrsOf (
|
||||
b.submodule (m: {
|
||||
options.nested = m.lib.mkOption {
|
||||
type = m.lib.types.str;
|
||||
};
|
||||
})
|
||||
);
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "submodule";
|
||||
value = {
|
||||
foo = {
|
||||
nested = "test1";
|
||||
};
|
||||
bar = {
|
||||
nested = "test2";
|
||||
};
|
||||
};
|
||||
testAttrs = {
|
||||
expected = {
|
||||
foo = {
|
||||
nested = "test1";
|
||||
};
|
||||
bar = {
|
||||
nested = "test2";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
attrsOf_submodule_err_inner = getMatrix {
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "submodule";
|
||||
getType =
|
||||
a: b:
|
||||
a.attrsOf (
|
||||
b.submodule (m: {
|
||||
options.nested = m.lib.mkOption {
|
||||
type = m.lib.types.str;
|
||||
};
|
||||
})
|
||||
);
|
||||
value = {
|
||||
foo = [ 1 ]; # not a submodule
|
||||
bar = {
|
||||
nested = "test2";
|
||||
};
|
||||
};
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo.foo' is not of type `submodule'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
attrsOf_submodule_err_outer = getMatrix {
|
||||
outerTypeName = "attrsOf";
|
||||
innerTypeName = "submodule";
|
||||
getType =
|
||||
a: b:
|
||||
a.attrsOf (
|
||||
b.submodule (m: {
|
||||
options.nested = m.lib.mkOption {
|
||||
type = m.lib.types.str;
|
||||
};
|
||||
})
|
||||
);
|
||||
value = [ 123 ]; # not an attrsOf
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = ''A definition for option `foo' is not of type `attribute set of \(submodule\).*'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# either
|
||||
either_str_attrsOf_ok = getMatrix {
|
||||
outerTypeName = "either";
|
||||
innerTypeName = "str_or_attrsOf_str";
|
||||
|
||||
getType = a: b: a.either b.str (b.attrsOf a.str);
|
||||
value = "string value";
|
||||
testAttrs = {
|
||||
expected = "string value";
|
||||
};
|
||||
};
|
||||
either_str_attrsOf_err_1 = getMatrix {
|
||||
outerTypeName = "either";
|
||||
innerTypeName = "str_or_attrsOf_str";
|
||||
|
||||
getType = a: b: a.either b.str (b.attrsOf a.str);
|
||||
value = 1;
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo' is not of type `string or attribute set of string'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
either_str_attrsOf_err_2 = getMatrix {
|
||||
outerTypeName = "either";
|
||||
innerTypeName = "str_or_attrsOf_str";
|
||||
|
||||
getType = a: b: a.either b.str (b.attrsOf a.str);
|
||||
value = {
|
||||
bar = 1; # not a string
|
||||
};
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = "A definition for option `foo.bar' is not of type `string'.*";
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Coereced to
|
||||
coerce_attrsOf_str_to_listOf_str_run = getMatrix {
|
||||
outerTypeName = "coercedTo";
|
||||
innerTypeName = "attrsOf_str->listOf_str";
|
||||
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
|
||||
value = {
|
||||
bar = "test1"; # coerced to listOf string
|
||||
foo = "test2"; # coerced to listOf string
|
||||
};
|
||||
testAttrs = {
|
||||
expected = [
|
||||
"test1"
|
||||
"test2"
|
||||
];
|
||||
};
|
||||
};
|
||||
coerce_attrsOf_str_to_listOf_str_final = getMatrix {
|
||||
outerTypeName = "coercedTo";
|
||||
innerTypeName = "attrsOf_str->listOf_str";
|
||||
getType = a: b: a.coercedTo (b.attrsOf b.str) (abort "This shouldnt run") (b.listOf b.str);
|
||||
value = [
|
||||
"test1"
|
||||
"test2"
|
||||
]; # already a listOf string
|
||||
testAttrs = {
|
||||
expected = [
|
||||
"test1"
|
||||
"test2"
|
||||
]; # Order should be kept
|
||||
};
|
||||
};
|
||||
coerce_attrsOf_str_to_listOf_err_coercer_input = getMatrix {
|
||||
outerTypeName = "coercedTo";
|
||||
innerTypeName = "attrsOf_str->listOf_str";
|
||||
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
|
||||
value = [
|
||||
{ }
|
||||
{ }
|
||||
]; # not coercible to listOf string, with the given coercer
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*'';
|
||||
};
|
||||
};
|
||||
};
|
||||
coerce_attrsOf_str_to_listOf_err_coercer_ouput = getMatrix {
|
||||
outerTypeName = "coercedTo";
|
||||
innerTypeName = "attrsOf_str->listOf_str";
|
||||
getType = a: b: a.coercedTo (b.attrsOf b.str) builtins.attrValues (b.listOf b.str);
|
||||
value = {
|
||||
foo = {
|
||||
bar = 1;
|
||||
}; # coercer produces wrong type -> [ { bar = 1; } ]
|
||||
};
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = ''A definition for option `foo."\[definition 1-entry 1\]"' is not of type `string'.*'';
|
||||
};
|
||||
};
|
||||
};
|
||||
coerce_str_to_int_coercer_ouput = getMatrix {
|
||||
outerTypeName = "coercedTo";
|
||||
innerTypeName = "int->str";
|
||||
getType = a: b: a.coercedTo b.int builtins.toString a.str;
|
||||
value = [ ];
|
||||
testAttrs = {
|
||||
expectedError = {
|
||||
type = "ThrownError";
|
||||
msg = ''A definition for option `foo' is not of type `string or signed integer convertible to it.*'';
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
# Submodule
|
||||
submodule_with_ok = getMatrix {
|
||||
outerTypeName = "submoduleWith";
|
||||
innerTypeName = "mixed_types";
|
||||
getType =
|
||||
a: b:
|
||||
a.submodule (m: {
|
||||
options.attrs = m.lib.mkOption {
|
||||
type = b.attrsOf b.str;
|
||||
};
|
||||
options.list = m.lib.mkOption {
|
||||
type = b.listOf b.str;
|
||||
};
|
||||
options.either = m.lib.mkOption {
|
||||
type = b.either a.str a.int;
|
||||
};
|
||||
});
|
||||
value = {
|
||||
attrs = {
|
||||
foo = "bar";
|
||||
};
|
||||
list = [
|
||||
"foo"
|
||||
"bar"
|
||||
];
|
||||
either = 123; # int
|
||||
};
|
||||
testAttrs = {
|
||||
expected = {
|
||||
attrs = {
|
||||
foo = "bar";
|
||||
};
|
||||
list = [
|
||||
"foo"
|
||||
"bar"
|
||||
];
|
||||
either = 123;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -345,14 +345,14 @@ checkConfigOutput '^true$' "$@" ./define-module-check.nix
|
||||
set --
|
||||
checkConfigOutput '^"42"$' config.value ./declare-coerced-value.nix
|
||||
checkConfigOutput '^"24"$' config.value ./declare-coerced-value.nix ./define-value-string.nix
|
||||
checkConfigError 'A definition for option .* is not.*string or signed integer convertible to it.*. Definition values:\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix
|
||||
checkConfigError 'A definition for option .*. is not of type .*.\n\s*- In .*: \[ \]' config.value ./declare-coerced-value.nix ./define-value-list.nix
|
||||
|
||||
# Check coerced option merging.
|
||||
checkConfigError 'The option .value. in .*/declare-coerced-value.nix. is already declared in .*/declare-coerced-value-no-default.nix.' config.value ./declare-coerced-value.nix ./declare-coerced-value-no-default.nix
|
||||
|
||||
# Check coerced value with unsound coercion
|
||||
checkConfigOutput '^12$' config.value ./declare-coerced-value-unsound.nix
|
||||
checkConfigError 'A definition for option .* is not of type .*. Definition values:\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix
|
||||
checkConfigError 'A definition for option .* is not of type .*.\n\s*- In .*: "1000"' config.value ./declare-coerced-value-unsound.nix ./define-value-string-bigint.nix
|
||||
checkConfigError 'toInt: Could not convert .* to int' config.value ./declare-coerced-value-unsound.nix ./define-value-string-arbitrary.nix
|
||||
|
||||
# Check `graph` attribute
|
||||
@@ -761,6 +761,26 @@ checkConfigOutput '"bar"' config.sub.conditionalImportAsNixos.foo ./specialArgs-
|
||||
checkConfigError 'attribute .*bar.* not found' config.sub.conditionalImportAsNixos.bar ./specialArgs-class.nix
|
||||
checkConfigError 'attribute .*foo.* not found' config.sub.conditionalImportAsDarwin.foo ./specialArgs-class.nix
|
||||
checkConfigOutput '"foo"' config.sub.conditionalImportAsDarwin.bar ./specialArgs-class.nix
|
||||
# Check that some types expose the 'valueMeta'
|
||||
checkConfigOutput '\{\}' options.str.valueMeta ./types-valueMeta.nix
|
||||
checkConfigOutput '["foo", "bar"]' config.attrsOfResult ./types-valueMeta.nix
|
||||
checkConfigOutput '2' config.listOfResult ./types-valueMeta.nix
|
||||
|
||||
# Check that composed types expose the 'valueMeta'
|
||||
# attrsOf submodule (also on merged options,types)
|
||||
checkConfigOutput '42' options.attrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix
|
||||
checkConfigOutput '42' options.mergedAttrsOfModule.valueMeta.attrs.foo.configuration.options.bar.value ./composed-types-valueMeta.nix
|
||||
|
||||
# listOf submodule (also on merged options,types)
|
||||
checkConfigOutput '42' config.listResult ./composed-types-valueMeta.nix
|
||||
checkConfigOutput '42' config.mergedListResult ./composed-types-valueMeta.nix
|
||||
|
||||
# Add check
|
||||
checkConfigOutput '^0$' config.v1CheckedPass ./add-check.nix
|
||||
checkConfigError 'A definition for option .* is not of type .signed integer.*' config.v1CheckedFail ./add-check.nix
|
||||
checkConfigOutput '^true$' config.v2checkedPass ./add-check.nix
|
||||
checkConfigError 'A definition for option .* is not of type .attribute set of signed integer.*' config.v2checkedFail ./add-check.nix
|
||||
|
||||
|
||||
cat <<EOF
|
||||
====== module tests ======
|
||||
|
||||
36
lib/tests/modules/add-check.nix
Normal file
36
lib/tests/modules/add-check.nix
Normal file
@@ -0,0 +1,36 @@
|
||||
(
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) types mkOption;
|
||||
inherit (types) addCheck int attrsOf;
|
||||
|
||||
# type with a v1 merge
|
||||
v1Type = addCheck int (v: v == 0);
|
||||
|
||||
# type with a v2 merge
|
||||
v2Type = addCheck (attrsOf int) (v: v ? foo);
|
||||
in
|
||||
{
|
||||
options.v1CheckedPass = mkOption {
|
||||
type = v1Type;
|
||||
default = 0;
|
||||
};
|
||||
options.v1CheckedFail = mkOption {
|
||||
type = v1Type;
|
||||
default = 1;
|
||||
};
|
||||
options.v2checkedPass = mkOption {
|
||||
type = v2Type;
|
||||
default = {
|
||||
foo = 1;
|
||||
};
|
||||
# plug the value to make test script regex simple
|
||||
apply = v: v.foo == 1;
|
||||
};
|
||||
options.v2checkedFail = mkOption {
|
||||
type = v2Type;
|
||||
default = { };
|
||||
apply = v: lib.deepSeq v v;
|
||||
};
|
||||
}
|
||||
)
|
||||
75
lib/tests/modules/composed-types-valueMeta.nix
Normal file
75
lib/tests/modules/composed-types-valueMeta.nix
Normal file
@@ -0,0 +1,75 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) types mkOption;
|
||||
|
||||
attrsOfModule = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.submodule {
|
||||
options.bar = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
listOfModule = mkOption {
|
||||
type = types.listOf (
|
||||
types.submodule {
|
||||
options.bar = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
# Module A
|
||||
({
|
||||
options.attrsOfModule = attrsOfModule;
|
||||
options.mergedAttrsOfModule = attrsOfModule;
|
||||
options.listOfModule = listOfModule;
|
||||
options.mergedListOfModule = listOfModule;
|
||||
})
|
||||
# Module B
|
||||
({
|
||||
options.mergedAttrsOfModule = attrsOfModule;
|
||||
options.mergedListOfModule = listOfModule;
|
||||
})
|
||||
# Values
|
||||
# It is important that the value is defined in a separate module
|
||||
# Without valueMeta the actual value and sub-options wouldn't be accessible via:
|
||||
# options.attrsOfModule.type.getSubOptions
|
||||
({
|
||||
attrsOfModule = {
|
||||
foo.bar = 42;
|
||||
};
|
||||
mergedAttrsOfModule = {
|
||||
foo.bar = 42;
|
||||
};
|
||||
})
|
||||
(
|
||||
{ options, ... }:
|
||||
{
|
||||
config.listOfModule = [
|
||||
{
|
||||
bar = 42;
|
||||
}
|
||||
];
|
||||
config.mergedListOfModule = [
|
||||
{
|
||||
bar = 42;
|
||||
}
|
||||
];
|
||||
# Result options to expose the list module to bash as plain attribute path
|
||||
options.listResult = mkOption {
|
||||
default = (builtins.head options.listOfModule.valueMeta.list).configuration.options.bar.value;
|
||||
};
|
||||
options.mergedListResult = mkOption {
|
||||
default = (builtins.head options.mergedListOfModule.valueMeta.list).configuration.options.bar.value;
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
60
lib/tests/modules/types-valueMeta.nix
Normal file
60
lib/tests/modules/types-valueMeta.nix
Normal file
@@ -0,0 +1,60 @@
|
||||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) types mkOption;
|
||||
|
||||
inherit (types)
|
||||
# attrsOf uses attrsWith internally
|
||||
attrsOf
|
||||
listOf
|
||||
submoduleOf
|
||||
str
|
||||
;
|
||||
in
|
||||
{
|
||||
imports = [
|
||||
(
|
||||
{ options, ... }:
|
||||
{
|
||||
# Should have an empty valueMeta
|
||||
options.str = mkOption {
|
||||
type = str;
|
||||
};
|
||||
|
||||
# Should have some valueMeta which is an attribute set of the nested valueMeta
|
||||
options.attrsOf = mkOption {
|
||||
type = attrsOf str;
|
||||
default = {
|
||||
foo = "foo";
|
||||
bar = "bar";
|
||||
};
|
||||
};
|
||||
options.attrsOfResult = mkOption {
|
||||
default = builtins.attrNames options.attrsOf.valueMeta.attrs;
|
||||
};
|
||||
|
||||
# Should have some valueMeta which is the list of the nested valueMeta of types.str
|
||||
# [ {} {} ]
|
||||
options.listOf = mkOption {
|
||||
type = listOf str;
|
||||
default = [
|
||||
"foo"
|
||||
"bar"
|
||||
];
|
||||
};
|
||||
options.listOfResult = mkOption {
|
||||
default = builtins.length options.listOf.valueMeta.list;
|
||||
};
|
||||
|
||||
# Should have some valueMeta which is the submodule evaluation
|
||||
# { _module, options, config, ...}
|
||||
options.submoduleOf = mkOption {
|
||||
type = submoduleOf {
|
||||
options.str = mkOption {
|
||||
type = str;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
)
|
||||
];
|
||||
}
|
||||
25
lib/tests/nix-unit.nix
Normal file
25
lib/tests/nix-unit.nix
Normal file
@@ -0,0 +1,25 @@
|
||||
{
|
||||
pkgs ? import ../.. { },
|
||||
}:
|
||||
let
|
||||
prevNixpkgs = pkgs.fetchFromGitHub {
|
||||
owner = "nixos";
|
||||
repo = "nixpkgs";
|
||||
# Parent commit of [#391544](https://github.com/NixOS/nixpkgs/pull/391544)
|
||||
# Which was before the type.merge.v2 introduction
|
||||
rev = "bcf94dd3f07189b7475d823c8d67d08b58289905";
|
||||
hash = "sha256-MuMiIY3MX5pFSOCvutmmRhV6RD0R3CG0Hmazkg8cMFI=";
|
||||
};
|
||||
in
|
||||
(pkgs.runCommand "lib-cross-eval-merge-v2"
|
||||
{
|
||||
nativeBuildInputs = [ pkgs.nix-unit ];
|
||||
}
|
||||
''
|
||||
export HOME=$TMPDIR
|
||||
nix-unit --eval-store "$HOME" ${./checkAndMergeCompat.nix} \
|
||||
--arg currLibPath "${../.}" \
|
||||
--arg prevLibPath "${prevNixpkgs}/lib"
|
||||
mkdir $out
|
||||
''
|
||||
)
|
||||
@@ -29,6 +29,9 @@ in
|
||||
pkgsBB.symlinkJoin {
|
||||
name = "nixpkgs-lib-tests";
|
||||
paths = map testWithNix nixVersions ++ [
|
||||
(import ./nix-unit.nix {
|
||||
inherit pkgs;
|
||||
})
|
||||
(import ./maintainers.nix {
|
||||
inherit pkgs;
|
||||
lib = import ../.;
|
||||
|
||||
233
lib/types.nix
233
lib/types.nix
@@ -20,7 +20,6 @@ let
|
||||
toList
|
||||
;
|
||||
inherit (lib.lists)
|
||||
all
|
||||
concatLists
|
||||
count
|
||||
elemAt
|
||||
@@ -48,6 +47,7 @@ let
|
||||
mergeOneOption
|
||||
mergeUniqueOption
|
||||
showFiles
|
||||
showDefs
|
||||
showOption
|
||||
;
|
||||
inherit (lib.strings)
|
||||
@@ -99,6 +99,13 @@ let
|
||||
}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
|
||||
'' payload.elemType;
|
||||
|
||||
checkDefsForError =
|
||||
check: loc: defs:
|
||||
let
|
||||
invalidDefs = filter (def: !check def.value) defs;
|
||||
in
|
||||
if invalidDefs != [ ] then { message = "Definition values: ${showDefs invalidDefs}"; } else null;
|
||||
|
||||
outer_types = rec {
|
||||
isType = type: x: (x._type or "") == type;
|
||||
|
||||
@@ -705,10 +712,14 @@ let
|
||||
}";
|
||||
descriptionClass = "composite";
|
||||
check = isList;
|
||||
merge =
|
||||
loc: defs:
|
||||
map (x: x.value) (
|
||||
filter (x: x ? value) (
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
evals = filter (x: x.optionalValue ? value) (
|
||||
concatLists (
|
||||
imap1 (
|
||||
n: def:
|
||||
@@ -719,12 +730,18 @@ let
|
||||
inherit (def) file;
|
||||
value = def';
|
||||
}
|
||||
]).optionalValue
|
||||
])
|
||||
) def.value
|
||||
) defs
|
||||
)
|
||||
)
|
||||
);
|
||||
in
|
||||
{
|
||||
headError = checkDefsForError check loc defs;
|
||||
value = map (x: x.optionalValue.value or x.mergedValue) evals;
|
||||
valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals;
|
||||
};
|
||||
};
|
||||
emptyValue = {
|
||||
value = [ ];
|
||||
};
|
||||
@@ -801,42 +818,43 @@ let
|
||||
lazy ? false,
|
||||
placeholder ? "name",
|
||||
}:
|
||||
mkOptionType {
|
||||
mkOptionType rec {
|
||||
name = if lazy then "lazyAttrsOf" else "attrsOf";
|
||||
description =
|
||||
(if lazy then "lazy attribute set" else "attribute set")
|
||||
+ " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
|
||||
descriptionClass = "composite";
|
||||
check = isAttrs;
|
||||
merge =
|
||||
if lazy then
|
||||
(
|
||||
# Lazy merge Function
|
||||
loc: defs:
|
||||
zipAttrsWith
|
||||
(
|
||||
name: defs:
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
merged = mergeDefinitions (loc ++ [ name ]) elemType defs;
|
||||
# mergedValue will trigger an appropriate error when accessed
|
||||
in
|
||||
merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
|
||||
)
|
||||
# Push down position info.
|
||||
(pushPositions defs)
|
||||
)
|
||||
evals =
|
||||
if lazy then
|
||||
zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
|
||||
else
|
||||
(
|
||||
# Non-lazy merge Function
|
||||
loc: defs:
|
||||
mapAttrs (n: v: v.value) (
|
||||
filterAttrs (n: v: v ? value) (
|
||||
zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue)
|
||||
# Push down position info.
|
||||
(pushPositions defs)
|
||||
)
|
||||
)
|
||||
# Filtering makes the merge function more strict
|
||||
# Meaning it is less lazy
|
||||
filterAttrs (n: v: v.optionalValue ? value) (
|
||||
zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
|
||||
);
|
||||
in
|
||||
{
|
||||
headError = checkDefsForError check loc defs;
|
||||
value = mapAttrs (
|
||||
n: v:
|
||||
if lazy then
|
||||
v.optionalValue.value or elemType.emptyValue.value or v.mergedValue
|
||||
else
|
||||
v.optionalValue.value
|
||||
) evals;
|
||||
valueMeta.attrs = mapAttrs (n: v: v.checkedAndMerged.valueMeta) evals;
|
||||
};
|
||||
};
|
||||
|
||||
emptyValue = {
|
||||
value = { };
|
||||
};
|
||||
@@ -1218,6 +1236,7 @@ let
|
||||
|
||||
name = "submodule";
|
||||
|
||||
check = x: isAttrs x || isFunction x || path.check x;
|
||||
in
|
||||
mkOptionType {
|
||||
inherit name;
|
||||
@@ -1229,13 +1248,25 @@ let
|
||||
docsEval = base.extendModules { modules = [ noCheckForDocsModule ]; };
|
||||
in
|
||||
docsEval._module.freeformType.description or name;
|
||||
check = x: isAttrs x || isFunction x || path.check x;
|
||||
merge =
|
||||
loc: defs:
|
||||
(base.extendModules {
|
||||
inherit check;
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
configuration = base.extendModules {
|
||||
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
|
||||
prefix = loc;
|
||||
}).config;
|
||||
};
|
||||
in
|
||||
{
|
||||
headError = checkDefsForError check loc defs;
|
||||
value = configuration.config;
|
||||
valueMeta = { inherit configuration; };
|
||||
};
|
||||
};
|
||||
emptyValue = {
|
||||
value = { };
|
||||
};
|
||||
@@ -1383,17 +1414,50 @@ let
|
||||
}";
|
||||
descriptionClass = "conjunction";
|
||||
check = x: t1.check x || t2.check x;
|
||||
merge =
|
||||
loc: defs:
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
defList = map (d: d.value) defs;
|
||||
in
|
||||
if all (x: t1.check x) defList then
|
||||
t1.merge loc defs
|
||||
else if all (x: t2.check x) defList then
|
||||
t2.merge loc defs
|
||||
t1CheckedAndMerged =
|
||||
if t1.merge ? v2 then
|
||||
t1.merge.v2 { inherit loc defs; }
|
||||
else
|
||||
mergeOneOption loc defs;
|
||||
{
|
||||
value = t1.merge loc defs;
|
||||
headError = checkDefsForError t1.check loc defs;
|
||||
valueMeta = { };
|
||||
};
|
||||
t2CheckedAndMerged =
|
||||
if t2.merge ? v2 then
|
||||
t2.merge.v2 { inherit loc defs; }
|
||||
else
|
||||
{
|
||||
value = t2.merge loc defs;
|
||||
headError = checkDefsForError t2.check loc defs;
|
||||
valueMeta = { };
|
||||
};
|
||||
|
||||
checkedAndMerged =
|
||||
if t1CheckedAndMerged.headError == null then
|
||||
t1CheckedAndMerged
|
||||
else if t2CheckedAndMerged.headError == null then
|
||||
t2CheckedAndMerged
|
||||
else
|
||||
rec {
|
||||
valueMeta = {
|
||||
inherit headError;
|
||||
};
|
||||
headError = {
|
||||
message = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}";
|
||||
};
|
||||
value = abort "(t.merge.v2 defs).value must only be accessed when `.headError == null`. This is a bug in code that consumes a module system type.";
|
||||
};
|
||||
in
|
||||
checkedAndMerged;
|
||||
};
|
||||
typeMerge =
|
||||
f':
|
||||
let
|
||||
@@ -1434,12 +1498,47 @@ let
|
||||
optionDescriptionPhrase (class: class == "noun") coercedType
|
||||
} convertible to it";
|
||||
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
|
||||
merge =
|
||||
loc: defs:
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
coerceVal = val: if coercedType.check val then coerceFunc val else val;
|
||||
finalDefs = (
|
||||
map (
|
||||
def:
|
||||
def
|
||||
// {
|
||||
value =
|
||||
let
|
||||
merged = coercedType.merge.v2 {
|
||||
inherit loc;
|
||||
defs = [ def ];
|
||||
};
|
||||
in
|
||||
finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
|
||||
if coercedType.merge ? v2 then
|
||||
if merged.headError == null then coerceFunc def.value else def.value
|
||||
else if coercedType.check def.value then
|
||||
coerceFunc def.value
|
||||
else
|
||||
def.value;
|
||||
}
|
||||
) defs
|
||||
);
|
||||
in
|
||||
if finalType.merge ? v2 then
|
||||
finalType.merge.v2 {
|
||||
inherit loc;
|
||||
defs = finalDefs;
|
||||
}
|
||||
else
|
||||
{
|
||||
value = finalType.merge loc finalDefs;
|
||||
valueMeta = { };
|
||||
headError = checkDefsForError check loc defs;
|
||||
};
|
||||
};
|
||||
emptyValue = finalType.emptyValue;
|
||||
getSubOptions = finalType.getSubOptions;
|
||||
getSubModules = finalType.getSubModules;
|
||||
@@ -1451,6 +1550,7 @@ let
|
||||
nestedTypes.coercedType = coercedType;
|
||||
nestedTypes.finalType = finalType;
|
||||
};
|
||||
|
||||
/**
|
||||
Augment the given type with an additional type check function.
|
||||
|
||||
@@ -1459,8 +1559,33 @@ let
|
||||
Fixing is not trivial, we appreciate any help!
|
||||
:::
|
||||
*/
|
||||
addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
|
||||
|
||||
addCheck =
|
||||
elemType: check:
|
||||
if elemType.merge ? v2 then
|
||||
elemType
|
||||
// {
|
||||
check = x: elemType.check x && check x;
|
||||
merge = {
|
||||
__functor =
|
||||
self: loc: defs:
|
||||
(self.v2 { inherit loc defs; }).value;
|
||||
v2 =
|
||||
{ loc, defs }:
|
||||
let
|
||||
orig = elemType.merge.v2 { inherit loc defs; };
|
||||
headError' = if orig.headError != null then orig.headError else checkDefsForError check loc defs;
|
||||
in
|
||||
orig
|
||||
// {
|
||||
headError = headError';
|
||||
};
|
||||
};
|
||||
}
|
||||
else
|
||||
elemType
|
||||
// {
|
||||
check = x: elemType.check x && check x;
|
||||
};
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user