From d504a1e68085dbe27bd32978130ff9006af8067f Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Mon, 9 Dec 2024 13:30:36 +0100 Subject: [PATCH] lib.types.attrsWith: add placeholder parameter --- lib/options.nix | 19 +++---- lib/tests/misc.nix | 38 ++++++++++++++ lib/tests/modules.sh | 4 ++ lib/tests/modules/name-merge-attrsWith-1.nix | 51 +++++++++++++++++++ lib/tests/modules/name-merge-attrsWith-2.nix | 38 ++++++++++++++ lib/types.nix | 20 ++++++-- .../development/option-types.section.md | 10 +++- 7 files changed, 163 insertions(+), 17 deletions(-) create mode 100644 lib/tests/modules/name-merge-attrsWith-1.nix create mode 100644 lib/tests/modules/name-merge-attrsWith-2.nix diff --git a/lib/options.nix b/lib/options.nix index 4f75da5ad51a8..5b22b1b37b86d 100644 --- a/lib/options.nix +++ b/lib/options.nix @@ -420,20 +420,17 @@ rec { Placeholders will not be quoted as they are not actual values: (showOption ["foo" "*" "bar"]) == "foo.*.bar" (showOption ["foo" "" "bar"]) == "foo..bar" + (showOption ["foo" "" "bar"]) == "foo..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 = [ - "" # attrsOf (submodule {}) - "*" # listOf (submodule {}) - "" # 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); diff --git a/lib/tests/misc.nix b/lib/tests/misc.nix index 6357cc3ec1cf5..62f231cc51fd9 100644 --- a/lib/tests/misc.nix +++ b/lib/tests/misc.nix @@ -1877,6 +1877,44 @@ runTests { expected = [ [ "_module" "args" ] [ "foo" ] [ "foo" "" "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" + "" + "bar" + ]; + }; + + testShowOptionWithPlaceholder = { + # , *, should not be escaped. It is used as a placeholder by convention. + # Other symbols should be escaped. `{}` + expr = lib.showOption ["" "" "*" "{foo}"]; + expected = "..*.\"{foo}\""; + }; + testCartesianProductOfEmptySet = { expr = cartesianProduct {}; expected = [ {} ]; diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 4c00ecaab605b..dd3c1e573abdb 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -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..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 diff --git a/lib/tests/modules/name-merge-attrsWith-1.nix b/lib/tests/modules/name-merge-attrsWith-1.nix new file mode 100644 index 0000000000000..bbfd5fe0bddec --- /dev/null +++ b/lib/tests/modules/name-merge-attrsWith-1.nix @@ -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; + }; + } + ) + ]; +} diff --git a/lib/tests/modules/name-merge-attrsWith-2.nix b/lib/tests/modules/name-merge-attrsWith-2.nix new file mode 100644 index 0000000000000..6c6a91ce3850f --- /dev/null +++ b/lib/tests/modules/name-merge-attrsWith-2.nix @@ -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 { }; + }; + }; + } + ) + ]; +} diff --git a/lib/types.nix b/lib/types.nix index dd1e8ab9d0165..097ab1b42dcbe 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -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 ++ [""]); + 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; }; diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index a9c8f2f8a88dc..1e5c747b73401 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -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 `` + **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.` in the manual. + `types.uniq` *`t`*