lib.modules: init types checkAndMerge to allow adding 'valueMeta' attributes

This allows individual types to add attributes that would be discarded during normal evaluation.
Some examples:

types.submodule performs a submodule evluation which yields an 'evalModules' result.
It returns '.config' but makes the original result accessible via 'valueMeta' allowing introspection of '.options' and all other kinds of module evaluation results

types.attrsOf returns an attribute set of the nestedType.
It makes each valueMeta available under the corresponding attribute name.

Changed files
+98 -45
lib
+13 -1
lib/modules.nix
···
files = map (def: def.file) res.defsFinal;
definitionsWithLocations = res.defsFinal;
inherit (res) isDefined;
# This allows options to be correctly displayed using `${options.path.to.it}`
__toString = _: showOption loc;
};
···
# 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
type.merge loc defsFinal
else
let
···
# handling. If changed here, please change it there too.)
throw
"The option `${showOption loc}' was accessed but has no value defined. Try setting the option.";
isDefined = defsFinal != [ ];
···
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;
};
···
# Type-check the remaining definitions, and merge them. Or throw if no definitions.
mergedValue =
if isDefined then
+
if type.checkAndMerge or null != null then
+
checkedAndMerged.value
+
else if all (def: type.check def.value) defsFinal then
type.merge loc defsFinal
else
let
···
# handling. If changed here, please change it there too.)
throw
"The option `${showOption loc}' was accessed but has no value defined. Try setting the option.";
+
+
checkedAndMerged =
+
if type.checkAndMerge or null != null then
+
type.checkAndMerge loc defsFinal
+
else
+
{
+
value = mergedValue;
+
valueMeta = { };
+
};
isDefined = defsFinal != [ ];
+85 -44
lib/types.nix
···
mergeOneOption
mergeUniqueOption
showFiles
showOption
;
inherit (lib.strings)
···
# definition values and locations (e.g. [ { file = "/foo.nix";
# value = 1; } { file = "/bar.nix"; value = 2 } ]).
merge ? mergeDefaultOption,
# Whether this type has a value representing nothingness. If it does,
# this should be a value of the form { value = <the nothing value>; }
# If it doesn't, this should be {}
···
deprecationMessage
nestedTypes
descriptionClass
;
functor =
if functor ? wrappedDeprecationMessage then
···
}";
descriptionClass = "composite";
check = isList;
-
merge =
loc: defs:
-
map (x: x.value) (
-
filter (x: x ? value) (
concatLists (
imap1 (
n: def:
···
inherit (def) file;
value = def';
}
-
]).optionalValue
) def.value
) defs
)
-
)
-
);
emptyValue = {
value = [ ];
};
···
nonEmptyListOf =
elemType:
let
-
list = addCheck (types.listOf elemType) (l: l != [ ]);
in
-
list
-
// {
-
description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
-
emptyValue = { }; # no .value attr, meaning unset
-
substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
-
};
attrsOf = elemType: attrsWith { inherit elemType; };
···
lazy ? false,
placeholder ? "name",
}:
-
mkOptionType {
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:
-
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)
-
)
-
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)
-
)
-
)
-
);
emptyValue = {
value = { };
};
···
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
prefix = loc;
}).config;
emptyValue = {
value = { };
};
···
nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType;
};
/**
Augment the given type with an additional type check function.
···
Fixing is not trivial, we appreciate any help!
:::
*/
-
addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
};
···
mergeOneOption
mergeUniqueOption
showFiles
+
showDefs
showOption
;
inherit (lib.strings)
···
# definition values and locations (e.g. [ { file = "/foo.nix";
# value = 1; } { file = "/bar.nix"; value = 2 } ]).
merge ? mergeDefaultOption,
+
#
+
# This field does not have a default implementation, so that users' changes
+
# to `check` and `merge` are propagated.
+
checkAndMerge ? null,
# Whether this type has a value representing nothingness. If it does,
# this should be a value of the form { value = <the nothing value>; }
# If it doesn't, this should be {}
···
deprecationMessage
nestedTypes
descriptionClass
+
checkAndMerge
;
functor =
if functor ? wrappedDeprecationMessage then
···
}";
descriptionClass = "composite";
check = isList;
+
merge = loc: defs: (checkAndMerge loc defs).value;
+
checkAndMerge =
loc: defs:
+
let
+
evals = filter (x: x.optionalValue ? value) (
concatLists (
imap1 (
n: def:
···
inherit (def) file;
value = def';
}
+
])
) def.value
) defs
)
+
);
+
in
+
{
+
value = map (x: x.optionalValue.value or x.mergedValue) evals;
+
valueMeta.list = map (v: v.checkedAndMerged.valueMeta) evals;
+
};
emptyValue = {
value = [ ];
};
···
nonEmptyListOf =
elemType:
let
+
list = types.listOf elemType;
in
+
addCheck (
+
list
+
// {
+
description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
+
emptyValue = { }; # no .value attr, meaning unset
+
substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
+
}
+
) (l: l != [ ]);
attrsOf = elemType: attrsWith { inherit elemType; };
···
lazy ? false,
placeholder ? "name",
}:
+
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 = loc: defs: (checkAndMerge loc defs).value;
+
checkAndMerge =
+
loc: defs:
+
let
+
evals =
+
if lazy then
+
zipAttrsWith (name: defs: mergeDefinitions (loc ++ [ name ]) elemType defs) (pushPositions defs)
+
else
+
# 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
+
{
+
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 = { };
};
···
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
prefix = loc;
}).config;
+
checkAndMerge =
+
loc: defs:
+
let
+
configuration = base.extendModules {
+
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
+
prefix = loc;
+
};
+
in
+
{
+
value = configuration.config;
+
valueMeta = configuration;
+
};
emptyValue = {
value = { };
};
···
nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType;
};
+
/**
Augment the given type with an additional type check function.
···
Fixing is not trivial, we appreciate any help!
:::
*/
+
addCheck =
+
elemType: check:
+
elemType
+
// {
+
check = x: elemType.check x && check x;
+
}
+
// (lib.optionalAttrs (elemType.checkAndMerge != null) {
+
checkAndMerge =
+
loc: defs:
+
let
+
v = (elemType.checkAndMerge loc defs);
+
in
+
if all (def: elemType.check def.value && check def.value) defs then
+
v
+
else
+
let
+
allInvalid = filter (def: !elemType.check def.value || !check def.value) defs;
+
in
+
throw "A definition for option `${showOption loc}' is not of type `${elemType.description}'. Definition values:${showDefs allInvalid}";
+
});
};