lib/modules: add new merge.v2 for 'types.{either,coercedTo}'

This commit is contained in:
Johannes Kirschbauer
2025-05-30 17:25:51 +02:00
parent 648dbed1d6
commit 70ab11c2f2
2 changed files with 193 additions and 115 deletions

View File

@@ -1165,8 +1165,13 @@ let
# 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
if type.merge ? v2 then
# check and merge share the same closure
# .headError is either non-present null or an error describing string
if checkedAndMerged.headError or null != null then
throw "A definition for option `${showOption loc}' is not of type `${type.description}'. TypeError: ${checkedAndMerged.headError}"
else
checkedAndMerged.value
else if all (def: type.check def.value) defsFinal then
type.merge loc defsFinal
else
@@ -1180,14 +1185,29 @@ let
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
ensureMergedValueEnvelope =
v:
if attrNames v == attrNames defaultCheckedAndMerged then
v
else
{
value = mergedValue;
valueMeta = { };
};
throw "Invalid 'type.merge.v2' of type: '${type.name}' must return exactly the following attributes: ${builtins.toJSON (attrNames defaultCheckedAndMerged)} but got ${builtins.toJSON (attrNames v)}";
defaultCheckedAndMerged = {
headError = null;
value = mergedValue;
valueMeta = { };
};
checkedAndMerged = ensureMergedValueEnvelope (
if type.merge ? v2 then
type.merge.v2 {
inherit loc;
defs = defsFinal;
}
else
defaultCheckedAndMerged
);
isDefined = defsFinal != [ ];

View File

@@ -212,10 +212,6 @@ let
# 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 {}
@@ -264,7 +260,6 @@ let
deprecationMessage
nestedTypes
descriptionClass
checkAndMerge
;
functor =
if functor ? wrappedDeprecationMessage then
@@ -718,31 +713,36 @@ let
}";
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:
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
evals = filter (x: x.optionalValue ? value) (
concatLists (
imap1 (
m: def':
(mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
{
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;
};
n: def:
imap1 (
m: def':
(mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
{
inherit (def) file;
value = def';
}
])
) 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 = [ ];
};
@@ -828,30 +828,35 @@ let
+ " 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) (
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
evals =
if lazy then
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;
};
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
{
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 = { };
@@ -1234,6 +1239,7 @@ let
name = "submodule";
check = x: isAttrs x || isFunction x || path.check x;
in
mkOptionType {
inherit name;
@@ -1245,25 +1251,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 {
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;
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;
};
in
{
headError = checkDefsForError check loc defs;
value = configuration.config;
valueMeta = configuration;
};
in
{
value = configuration.config;
valueMeta = configuration;
};
};
emptyValue = {
value = { };
};
@@ -1411,17 +1417,48 @@ let
}";
descriptionClass = "conjunction";
check = x: t1.check x || t2.check x;
merge =
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
else
mergeOneOption loc defs;
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
t1CheckedAndMerged =
if t1.merge ? v2 then
t1.merge.v2 { inherit loc defs; }
else
{
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 = "The option `${showOption loc}` is neither a value of type `${t1.description}` nor `${t2.description}`, Definition values: ${showDefs defs}";
value = null;
};
in
checkedAndMerged;
};
typeMerge =
f':
let
@@ -1462,12 +1499,43 @@ let
optionDescriptionPhrase (class: class == "noun") coercedType
} convertible to it";
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
merge =
loc: defs:
let
coerceVal = val: if coercedType.check val then coerceFunc val else val;
in
finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
merge = {
__funtor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
isMergeV2 = coercedType.merge ? v2;
coerceDef =
def:
let
merged = coercedType.merge.v2 {
inherit loc;
defs = [ def ];
};
in
if isMergeV2 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;
finalDefs = (map (def: def // { value = coerceDef def; }) 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 finalDefs;
};
};
emptyValue = finalType.emptyValue;
getSubOptions = finalType.getSubOptions;
getSubModules = finalType.getSubModules;
@@ -1490,25 +1558,15 @@ let
*/
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}";
});
let
final = elemType // {
check = x: elemType.check x && check x;
# addCheck discards the merge.v2 function
#
merge = if elemType.merge ? v2 then elemType.merge.__functor { inherit final; } else elemType.merge;
};
in
final;
};
/**