lib.types.attrsWith: add placeholder parameter

This commit is contained in:
Johannes Kirschbauer 2024-12-09 13:30:36 +01:00
parent fd6bc1b590
commit d504a1e680
No known key found for this signature in database
7 changed files with 163 additions and 17 deletions

View File

@ -420,20 +420,17 @@ rec {
Placeholders will not be quoted as they are not actual values:
(showOption ["foo" "*" "bar"]) == "foo.*.bar"
(showOption ["foo" "<name>" "bar"]) == "foo.<name>.bar"
(showOption ["foo" "<myPlaceholder>" "bar"]) == "foo.<myPlaceholder>.bar"
*/
showOption = parts: let
# If the part is a named placeholder of the form "<...>" don't escape it.
# It may cause misleading escaping if somebody uses literally "<...>" in their option names.
# This is the trade-off to allow for placeholders in option names.
isNamedPlaceholder = builtins.match "\<(.*)\>";
escapeOptionPart = part:
let
# We assume that these are "special values" and not real configuration data.
# If it is real configuration data, it is rendered incorrectly.
specialIdentifiers = [
"<name>" # attrsOf (submodule {})
"*" # listOf (submodule {})
"<function body>" # functionTo
];
in if builtins.elem part specialIdentifiers
then part
else lib.strings.escapeNixIdentifier part;
if part == "*" || isNamedPlaceholder part != null
then part
else lib.strings.escapeNixIdentifier part;
in (concatStringsSep ".") (map escapeOptionPart parts);
showFiles = files: concatStringsSep " and " (map (f: "`${f}'") files);

View File

@ -1877,6 +1877,44 @@ runTests {
expected = [ [ "_module" "args" ] [ "foo" ] [ "foo" "<name>" "bar" ] [ "foo" "bar" ] ];
};
testAttrsWithName = {
expr = let
eval = evalModules {
modules = [
{
options = {
foo = lib.mkOption {
type = lib.types.attrsWith {
placeholder = "MyCustomPlaceholder";
elemType = lib.types.submodule {
options.bar = lib.mkOption {
type = lib.types.int;
default = 42;
};
};
};
};
};
}
];
};
opt = eval.options.foo;
in
(opt.type.getSubOptions opt.loc).bar.loc;
expected = [
"foo"
"<MyCustomPlaceholder>"
"bar"
];
};
testShowOptionWithPlaceholder = {
# <name>, *, should not be escaped. It is used as a placeholder by convention.
# Other symbols should be escaped. `{}`
expr = lib.showOption ["<name>" "<myName>" "*" "{foo}"];
expected = "<name>.<myName>.*.\"{foo}\"";
};
testCartesianProductOfEmptySet = {
expr = cartesianProduct {};
expected = [ {} ];

View File

@ -391,6 +391,10 @@ checkConfigError 'The option `mergedLazyNonLazy'\'' in `.*'\'' is already declar
checkConfigOutput '^11$' config.lazyResult ./lazy-attrsWith.nix
checkConfigError 'infinite recursion encountered' config.nonLazyResult ./lazy-attrsWith.nix
# AttrsWith placeholder tests
checkConfigOutput '^"mergedName.<id>.nested"$' config.result ./name-merge-attrsWith-1.nix
checkConfigError 'The option .mergedName. in .*\.nix. is already declared in .*\.nix' config.mergedName ./name-merge-attrsWith-2.nix
# Even with multiple assignments, a type error should be thrown if any of them aren't valid
checkConfigError 'A definition for option .* is not of type .*' \
config.value ./declare-int-unsigned-value.nix ./define-value-list.nix ./define-value-int-positive.nix

View File

@ -0,0 +1,51 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
in
{
imports = [
# Module A
(
{ ... }:
{
options.mergedName = mkOption {
default = { };
type = types.attrsWith {
placeholder = "id"; # <- This is beeing tested
elemType = types.submodule {
options.nested = mkOption {
type = types.int;
default = 1;
};
};
};
};
}
)
# Module B
(
{ ... }:
{
# defines the default placeholder "name"
# type merging should resolve to "id"
options.mergedName = mkOption {
type = types.attrsOf (types.submodule { });
};
}
)
# Output
(
{
options,
...
}:
{
options.result = mkOption {
default = lib.concatStringsSep "." (options.mergedName.type.getSubOptions options.mergedName.loc)
.nested.loc;
};
}
)
];
}

View File

@ -0,0 +1,38 @@
{ lib, ... }:
let
inherit (lib) types mkOption;
in
{
imports = [
# Module A
(
{ ... }:
{
options.mergedName = mkOption {
default = { };
type = types.attrsWith {
placeholder = "id"; # <- this is beeing tested
elemType = types.submodule {
options.nested = mkOption {
type = types.int;
default = 1;
};
};
};
};
}
)
# Module B
(
{ ... }:
{
options.mergedName = mkOption {
type = types.attrsWith {
placeholder = "other"; # <- define placeholder = "other" (conflict)
elemType = types.submodule { };
};
};
}
)
];
}

View File

@ -608,17 +608,27 @@ rec {
lhs.lazy
else
null;
placeholder =
if lhs.placeholder == rhs.placeholder then
lhs.placeholder
else if lhs.placeholder == "name" then
rhs.placeholder
else if rhs.placeholder == "name" then
lhs.placeholder
else
null;
in
if elemType == null || lazy == null then
if elemType == null || lazy == null || placeholder == null then
null
else
{
inherit elemType lazy;
inherit elemType lazy placeholder;
};
in
{
elemType,
lazy ? false,
placeholder ? "name",
}:
mkOptionType {
name = if lazy then "lazyAttrsOf" else "attrsOf";
@ -645,16 +655,16 @@ rec {
(pushPositions defs)))
);
emptyValue = { value = {}; };
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<${placeholder}>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: attrsWith { elemType = elemType.substSubModules m; inherit lazy; };
substSubModules = m: attrsWith { elemType = elemType.substSubModules m; inherit lazy placeholder; };
functor = defaultFunctor "attrsWith" // {
wrappedDeprecationMessage = { loc }: lib.warn ''
The deprecated `type.functor.wrapped` attribute of the option `${showOption loc}` is accessed, use `type.nestedTypes.elemType` instead.
'' elemType;
payload = {
# Important!: Add new function attributes here in case of future changes
inherit elemType lazy;
inherit elemType lazy placeholder;
};
inherit binOp;
};

View File

@ -399,7 +399,7 @@ Composed types are types that take a type as parameter. `listOf
returned instead for the same `mkIf false` definition.
:::
`types.attrsWith` { *`elemType`*, *`lazy`* ? false }
`types.attrsWith` { *`elemType`*, *`lazy`* ? false, *`placeholder`* ? "name" }
: An attribute set of where all the values are of *`elemType`* type.
@ -411,10 +411,18 @@ Composed types are types that take a type as parameter. `listOf
`lazy`
: Determines whether the attribute set is lazily evaluated. See: `types.lazyAttrsOf`
`placeholder` (`String`, default: `name` )
: Placeholder string in documentation for the attribute names.
The default value `name` results in the placeholder `<name>`
**Behavior**
- `attrsWith { elemType = t; }` is equivalent to `attrsOf t`
- `attrsWith { lazy = true; elemType = t; }` is equivalent to `lazyAttrsOf t`
- `attrsWith { placeholder = "id"; elemType = t; }`
Displays the option as `foo.<id>` in the manual.
`types.uniq` *`t`*