I’m trying to define a NixOS module with an option that has a type like this:
test = mkOption {
type = types.oneOf [
(types.submodule {
options = {
type = mkOption {
type = types.enum [ "a" ];
};
a1 = mkOption {
type = types.int;
};
a2 = mkOption {
type = types.int;
};
};
})
(types.submodule {
options = {
type = mkOption {
type = types.enum [ "b" ];
};
b1 = mkOption {
type = types.int;
};
b2 = mkOption {
type = types.int;
};
};
})
];
};
but when I try to provide a value for this like:
test = {
type = "b";
b1 = 3;
b2 = 3;
};
it says that the option test.b1 does not exist. However this does work:
test = {
type = "a";
a1 = 3;
a2 = 3;
};
Also, if I don’t use two submodules but instead a primitive and a submodule like:
test = mkOption {
type = types.oneOf [
types.int
(types.submodule {
options = {
type = mkOption {
type = types.enum [ "b" ];
};
b1 = mkOption {
type = types.int;
};
b2 = mkOption {
type = types.int;
};
};
})
];
};
then a configuration like
test = {
type = "b";
b1 = 3;
b2 = 3;
};
does work!
So it seems like types.oneOf has problems dealing with multiple submodule options with different sets of attributes. Is there any way around this? Or some other approach to representing mutually exclusive sets of submodule options?
I just had the same issue and found this post. After a couple hours of digging through the sources of types.nix I think I understand what’s happening. There’s two pertinent points where it goes wrong.
First the merge function of either / oneOf at
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;
It checks whether all defs that are to be merged are either of the left type (t1) or the right type (t2). This makes sense since if we’re merging we only want to have one side of the either, presumably else the merging is probably not well defined.
Now if submodules come into play inside the either we’ll have to look at how those do check at:
check = x: isAttrs x || isFunction x || path.check x;
This is where it breaks all appart if both your type parameters to either are submodules because isAttrs is going to be true thus the merge in either will always think it’s the left type parameter (t1).
I’ve made this thing mostly for fun. I don’t recommend you actually use it.
# typecheckSubmoduleByTryEval :: Submodule -> Submodule
typecheckSubmoduleByTryEval = submodule: let
check = x: (builtins.tryEval ((lib.evalModules {
modules = submodule.getSubModules ++ [ x ];
}).config)).success;
in lib.types.addCheck submodule check;
If you apply it to a submodule, it will try to eval the module with the value given as a part of the typecheck. It lets you skip over the submodule in an oneOf or either if it doesn’t seem to fit. With a big downside where any typo or small mistake might lead to errors that can be very hard to debug, because it skipped past the module and now throws type errors for a different part of the oneOf/either expression.
{
options = {
nestedTreeOfStuff = lib.mkOption {
type = let
leafNodeSubmodule = lib.types.submodule {
options = {
x = lib.mkOption { type = lib.types.int; };
y = lib.mkOption { type = lib.types.bool; };
z = lib.mkOption { type = lib.types.str; };
};
};
treeType = let
leafOrBranch = lib.types.oneOf [
(typecheckSubmoduleByTryEval leafNodeSubmodule)
(attrsOf leafOrBranch)
];
in leafOrBranch;
in treeType;
};
};
config.nestedTreeOfStuff = {
a.b.c = {
x = 1;
y = false;
z = "hello :)";
};
q.w.e.r.t.y = {
x = 2;
y = true;
z = "world :3";
};
};
}