diff --git a/lib/tests/modules/declare-attrList.nix b/lib/tests/modules/declare-attrList.nix index b83599de7a77..b6e664422af1 100644 --- a/lib/tests/modules/declare-attrList.nix +++ b/lib/tests/modules/declare-attrList.nix @@ -42,6 +42,27 @@ in ); }; + # asAttrs: value is a merged attrset, ordered list in valueMeta + asAttrs = mkOption { + type = types.lazyAttrsOf ( + types.attrListWith { + elemType = types.str; + asAttrs = true; + mergeAttrValues = _name: values: lib.last values; + } + ); + }; + + # asAttrs with default mergeAttrValues: duplicates collected into lists + asAttrsDefault = mkOption { + type = types.lazyAttrsOf ( + types.attrListWith { + elemType = types.int; + asAttrs = true; + } + ); + }; + # Strict wrappers that force deep evaluation, for testing error cases attrListStrict = mkOption { type = types.lazyAttrsOf types.raw; @@ -381,6 +402,53 @@ in } ]; + # asAttrs: unique keys — value is a plain attrset + asAttrs.unique = [ + { a = "alpha"; } + { b = "beta"; } + ]; + + # asAttrs: duplicate keys — last in order wins + asAttrs.duplicateKeys = mkMerge [ + { x = mkOrder 500 "first"; } + { x = mkOrder 1500 "last"; } + { y = "only"; } + ]; + + # asAttrs: with ordering — value is attrset, ordered list in valueMeta + asAttrs.ordered = { + z = mkOrder 200 "z-val"; + a = mkOrder 100 "a-val"; + }; + + # asAttrs: with mkForce — forced key overrides + asAttrs.withForce = mkMerge [ + { x = "unused: overridden by mkForce"; } + { + x = mkForce "forced"; + y = "kept"; + } + ]; + + # asAttrs: empty + asAttrs.empty = [ ]; + + # asAttrsDefault: unique keys + asAttrsDefault.unique = [ + { a = 1; } + { b = 2; } + ]; + + # asAttrsDefault: duplicate keys — default collects into lists + asAttrsDefault.duplicates = mkMerge [ + { x = mkOrder 500 10; } + { x = mkOrder 1500 30; } + { y = 99; } + [ + { x = 20; } + ] + ]; + # either: attrList branch matches for list input eitherAttrListOrInt = [ { a = "hello"; } @@ -723,6 +791,79 @@ in { a = "hello"; } ]; + # asAttrs: unique keys — value is a plain attrset + assert + cfg.asAttrs.unique == { + a = "alpha"; + b = "beta"; + }; + # ordered list preserved in valueMeta + assert + c.options.asAttrs.valueMeta.attrs.unique.attrListValue == [ + { a = "alpha"; } + { b = "beta"; } + ]; + + # asAttrs: duplicate keys — last in order wins + assert + cfg.asAttrs.duplicateKeys == { + x = "last"; + y = "only"; + }; + assert + c.options.asAttrs.valueMeta.attrs.duplicateKeys.attrListValue == [ + { x = "first"; } + { y = "only"; } + { x = "last"; } + ]; + + # asAttrs: ordered — value is attrset (unordered), list in valueMeta preserves order + assert + cfg.asAttrs.ordered == { + a = "a-val"; + z = "z-val"; + }; + assert + c.options.asAttrs.valueMeta.attrs.ordered.attrListValue == [ + { a = "a-val"; } + { z = "z-val"; } + ]; + + # asAttrs: mkForce — forced key overrides, value is attrset + assert + cfg.asAttrs.withForce == { + x = "forced"; + y = "kept"; + }; + + # asAttrs: empty — value is empty attrset + assert cfg.asAttrs.empty == { }; + + # asAttrsDefault: unique keys — each value wrapped in singleton list + assert + cfg.asAttrsDefault.unique == { + a = [ 1 ]; + b = [ 2 ]; + }; + + # asAttrsDefault: duplicate keys — values collected into list in order + assert + cfg.asAttrsDefault.duplicates == { + x = [ + 10 + 20 + 30 + ]; + y = [ 99 ]; + }; + assert + c.options.asAttrsDefault.valueMeta.attrs.duplicates.attrListValue == [ + { x = 10; } + { y = 99; } + { x = 20; } + { x = 30; } + ]; + # Error cases are tested via checkConfigError in modules.sh "ok"; diff --git a/lib/types.nix b/lib/types.nix index 836efdf41831..6079ea11b410 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -813,7 +813,11 @@ rec { attrListOf = elemType: attrListWith { inherit elemType; }; attrListWith = - { elemType }: + { + elemType, + asAttrs ? false, + mergeAttrValues ? _name: values: values, + }: mkOptionType rec { name = "attrListOf"; description = "attribute list of ${ @@ -932,21 +936,37 @@ rec { ]; }) items ); + + attrListValue = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals; in { headError = checkDefsForError check loc defs; - value = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals; + value = if asAttrs then zipAttrsWith mergeAttrValues attrListValue else attrListValue; valueMeta.attrList = map (e: e.eval.checkedAndMerged.valueMeta) evals; + /** + The ordered list representation, especially useful when asAttrs is set. + */ + valueMeta.attrListValue = attrListValue; }; }; emptyValue = { - value = [ ]; + value = if asAttrs then { } else [ ]; }; getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]); getSubModules = elemType.getSubModules; - substSubModules = m: attrListOf (elemType.substSubModules m); + substSubModules = + m: + attrListWith { + inherit asAttrs mergeAttrValues; + elemType = elemType.substSubModules m; + }; functor = elemTypeFunctor name { inherit elemType; } // { - type = payload: types.attrListOf payload.elemType; + type = + payload: + types.attrListWith { + inherit asAttrs mergeAttrValues; + inherit (payload) elemType; + }; }; nestedTypes.elemType = elemType; }; diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index ff35193a746c..720f5823d068 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -512,7 +512,7 @@ Composed types are types that take a type as parameter. `listOf Multiple definitions of the same option are concatenated and then sorted by priority. Entries at the same priority level preserve their definition order. -`types.attrListWith` { *`elemType`* } +`types.attrListWith` { *`elemType`*, *`asAttrs`* ? false, *`mergeAttrValues`* ? _name: values: values } : An ordered list of single-attribute attribute sets, where each value is of *`elemType`* type. @@ -521,6 +521,17 @@ Composed types are types that take a type as parameter. `listOf `elemType` (Required) : Specifies the type of each value in the attribute list. + `asAttrs` + : When `true`, the option value is an attribute set instead of a list. + Duplicate keys are merged using `mergeAttrValues`. + The ordered list is always available via `valueMeta.attrListValue`. + + `mergeAttrValues` + : A function `name: values: mergedValue` that controls how duplicate keys + are combined when `asAttrs = true`. This is passed as the callback to + `lib.zipAttrsWith`. The `values` list is in order of priority. + By default, all values are collected into a list. + **Behavior** - `attrListWith { elemType = t; }` is equivalent to `attrListOf t`