Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

lib/types: init taggedSubmodule #254790

Draft
wants to merge 1 commit into
base: master
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -710,6 +710,23 @@ rec {
in mergedOption.type;
};

# A type that is one of several submodules, similiar to types.oneOf but is usable inside attrsOf or listOf
# submodules need an option with a type str which is used to find the corresponding type
taggedSubmodules =
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
taggedSubmodules =
taggedSubmodule =

When used by itself, the option value is just a single submodule.
The naming convention describes the option value rather than the definition syntax (or whether multiple defs are ok).

Also apply to name below.

{ types
, specialArgs ? {}
}: mkOptionType rec {
name = "taggedSubmodules";
description = "one of ${concatStringsSep "," (attrNames types)}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description = "one of ${concatStringsSep "," (attrNames types)}";
description = "submodule with type tag";
descriptionClass = "composite";

The valid type values can be documented in a generated type option.

check = x: if x ? type then types.${x.type}.check x else throw "No type option set in:\n${lib.generators.toPretty {} x}";
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

check should return a bool, as it's used by either to do rudimentary but lazy type checks.

Assuming we keep the syntax as { type = "gpt"; partitions = ...; }, this should do an evalModules to get type option value, with something like freeformType = lazyAttrsOf raw; to ignore the other options. Alternatively we could handle the module arguments in a custom manner if that leads to better error messages.

merge = loc: foldl'
(res: def: types.${def.value.type}.merge loc [
(lib.recursiveUpdate { value._module.args = specialArgs; } def)
])
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This makes it impossible btw to use mkMerge if one of the items to be merged is missing a type. Minimal example in disko where I encountered this ~yesterday:

{ lib, ... }:

with lib;

let
  mkDataset = cfg: mkMerge [ cfg { type = "zfs_fs"; } ];
  mkLegacyMount = p: { mountpoint = p; options.mountpoint = "none"; };
in {
  disko.devices.zpool.tank.datasets = mapAttrs (const mkDataset) {
    "foo" = mkLegacyMount "/foo";
    "bar" =mkLegacyMount "/bar";
  };
}

This gives the following error:

       error: No type option set in:
       {
         mountpoint = "/bar";
         options = {
           mountpoint = "none";
         };
       }

AFAIU this happens because each declaration on its own is passed into the wrapped type, i.e. the stuff from mkLegacyMount is passed unmerged, i.e. without the type from mkDataset.

Anyways, having attr-sets inside the module system where the surroundings (i.e. mkIf/mkMerge/etc) "silently" stop working[1] isn't very nice and it would be cool if this could get fixed. Alternatively I'd also be fine with a better error message that catches this issue properly (it's already rather simple to break a module evaluation in a way that it's hard to figure out the culprit and I'd rather not have yet another case).

[1] well, it fails, but it doesn't tell you that it fails because of mkMerge.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could probably be solved by doing an evalModules just to figure out the type and then evaluate the submodule again. (See https://github.com/NixOS/nixpkgs/pull/254790/files#r1323465281)

{ };
Comment on lines +722 to +726
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same solution as in check should be used to get the type here.

nestedTypes = types;
};
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TODO

  • return the options for documentation getSubOptions. Return an enum option for the type tag, or maybe it should be more recognizable.
  • type merging would be nice, but we can scope that out. If not, we should probably make it behave like enum, as in applying the union to the type tag, and merging modules for any overlapping definitions.


submoduleWith =
{ modules
, specialArgs ? {}
Expand Down
51 changes: 51 additions & 0 deletions nixos/doc/manual/development/option-types.section.md
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,57 @@ Submodules are detailed in [Submodule](#section-option-types-submodule).
options. This is equivalent to
`types.submoduleWith { modules = toList o; shorthandOnlyDefinesConfig = true; }`.

`types.taggedSubmodules` { *`types`*, *`specialArgs`* ? {} }

: Like `types.oneOf`, but takes an attrsSet of submodule in types. Those need to have a type option
which is used to find the correct submodule.

::: {#ex-tagged-submodules .example}
### Tagged submodules
```nix
let
submoduleA = submodule {
options = {
type = mkOption {
type = str;
};
foo = mkOption {
type = int;
};
};
};
submoduleB = submodule {
options = {
type = mkOption {
type = str;
};
bar = mkOption {
type = int;
};
};
};
in
options.mod = mkOption {
type = attrsOf (taggedSubmodule {
types = {
a = submoduleA;
b = submoduleB;
};
});
};
config.mod = {
someA = {
type = "a";
foo = 123;
};
someB = {
type = "b";
foo = 456;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
foo = 456;
bar = 456;

};
};
```
:::

`types.submoduleWith` { *`modules`*, *`specialArgs`* ? {}, *`shorthandOnlyDefinesConfig`* ? false }

: Like `types.submodule`, but more flexible and with better defaults.
Expand Down