Is it possible to use either or oneOf with submodule? I’d like an option to use two different submodules depending on what attribute names are provided, such as in the following example:
{ lib, ... }: with lib; let
a = { ... }: {
option.a = mkOption {
type = types.bool;
};
};
b = { ... }: {
option = {
a = mkOption {
type = types.bool;
};
b = mkOption {
type = types.bool;
};
};
};
in {
options.test = mkOption {
type = with types; either (submodule a) (submodule b);
};
config = mkMerge [
{ test.a = true; }
{ test = { a = true; b = true; }; }
];
}
Sorry, I made an edit to my code block; when I try the second set in the mkMerge, if submodule b isn’t before submodule a, then it tells me that using b isn’t allowed.
Don’t you want to disallow this? Your type is either, not both at the same time.
I’m confused what you’re trying to accomplish here.
If you want both to be allowed, don’t use either. Just use mkOption for both a and b. If you only want one to be allowed, you should probably be using an enum instead.
And it would help if you actually showed us what you’re trying to accomplish, rather than these abstract things that don’t make sense, so that we can point you in a sensible direction.
Sorry, I’ve been conditioned by StackOverflow to provide minimal reproducible examples. I’ll post the code in a bit, but basically I want one config output if a is satisfied, and another output if b is satisfied.
Even on SO, they explicitly state that “clarity should not be sacrificed for brevity”, and encourage avoiding an XY problem. From what you’ve provided so far, I’m inferring that you’ve narrowed in on an obscure solution to a problem that can probably be solved in a more idiomatic way.
Fair enough! Would you like the entire module evaluation, or just this part? I would like to create different flake outputs based on whether the module passed to my function has a systems attribute or not; I could use a systemsOutput option, but I’d like to avoid that if possible. My previous solution was just checking whether the function passed to the outputs option had a system argument, but I like the type checking the module system has to offer, and using submodule evaluates the function immediately, and doesn’t allow me to check what arguments it was originally asking for.
I have switched to evaluating the modules manually again, but now I’m getting a different error:
error: The option `outputs' does not exist. Definition values:
- In `/nix/store/03x88svcvzx2y8iiphqp3snsbfnn39d6-source/flake.nix': <function>
It seems as if you're trying to declare an option by placing it into `config' rather than `options'!
This is with this outputs module:
outputs = { config, ... }: {
options = {
__flake = mkOption {
type = types.attrs;
default = { };
internal = true;
};
__parent = mkOption {
type = types.attrs;
default = config;
internal = true;
};
systems = mkOption {
type = with types;
coercedTo (either systemType (listOf systemType)) toList
(listOf systemType);
# TODO: Replace this with the flake-utils default systems.
default = [ system ];
};
outputs = mkOption {
type = with types;
let
attrTypes = either attrs (if config.maker == "default" then
(functionTo attrs)
else
anything);
outputTypes = oneOf [ strPath attrTypes ];
in coercedTo (either outputTypes (listOf outputTypes))
(compose [
toList
(map (f:
let e = silicon.import.should f;
in syvl.mif.null e.success e.value))
(remove null)
]) (listOf attrTypes);
};
};
config.__flake = mkMerge (map
(flip config.__parent.makers.${config.__parent.maker}
(config.__parent // config)) (unique config.outputs));
};
I haven’t figured out the details of what you’re trying to do here, but a couple of tips:
coercedTo already has either-like behavior; you don’t need to add another either to it. coercedTo (either a b) aToB b is just coercedTo a aToB b with more steps.
submodule is very lax in its check function; it’ll accept any attrset (or function or path) value, regardless of what attributes it has, because the error checking for missing or incorrect nested attributes will happen later when that submodule is evaluated. So eithering two submodules together is fairly pointless — either the first will succeed, or neither will.
Your full example is a bit complicated to try and trace through. If you ultimately just want to use a diferent submodule depending on whether or not there’s a systems attribute, you can still use either and just enhance the check on the first submodule (which will be the one that wants the systems attribute). You can do something like
let
submoduleA = types.submodule ({ ... }: {
options.systems = mkOption { … };
…
};
submoduleB = types.submodule ({ … }: {
…
};
outputs = mkOption {
type = with types; either (addCheck submoduleA (x: x ? systems)) submoduleB;
…
}
This way if the user supplies the systems key then it evaluates as submoduleA, and if they don’t then it evaluates as submoduleB. That said, your full example seems to have a default for systems and you can’t do that if you’re using the presence of the attribute to pick the submodule (and your full example seems to be including a generic attrs type in the either and you can’t do that either unless you add checks to the second submodule too). Also the check here forces the user to supply an attrset for the submodule, they can’t use a function (or a path).
Alternatively, you could make one submodule that accepts all attrs from both submodules but asserts that only one subset of attrs is used, though that’s a bit messy.
Got it. Though what is addCheck in your example? Is it an option type, like coercedTo?
I’ve switched to evaluating the functions later using lib.evalModules at the moment, so I can instead check whether a function is asking for a system argument. It seems to be working at the moment, and now users can access the specific system being used as well, like in the functions passed to flake-utils.eachSystem.
What it returns is a type identical to the one passed in, except its check function now returns a logical conjunction of calls to the existing check and your new check function. If the definitions don’t satisfy the condition the type check fails and either or oneOf will move on to check against the next type.
Example:
let
typeA = lib.types.submodule {
options.y = lib.mkOption {
type = lib.types.str;
};
};
typeB = lib.types.submodule {
options.z = lib.mkOption {
type = lib.types.str;
};
};
in
lib.evalModules {
modules = [
{
options.x = lib.mkOption {
type =
with lib.types;
oneOf [
(addCheck typeA (v: v ? y))
typeB
];
};
}
{
x.y = "hi";
x.z = "bye";
}
];
}
Since we’re setting x.y (of typeA) here, this will pass the check added to typeA and the definition will be evaluated against typeA. Since we’re also setting x.z (of typeB) we get an error (undeclared option). If you remove either one it will successfully evaluate against either typeA or typeB depending on whether x.y or x.z is defined, respectively.
Could I just load it into a file and nix eval it as well? Do I need any particular flags to do that? I’d like to avoid typing the evalModules call into the repl, since I tend to make a lot of mistakes.