types.attrListOf: init

This adds a type for name-value mappings that preserve ordering.

Motivating use case: command line flags for package modules /
wrappers / modular services.
The option value can be transformed into a command line in the
correct order.
Additionally, a convenience readOnly option could be provided
to give easy introspection access to the values in an ad hoc
manner.
This commit is contained in:
Robert Hensing
2026-04-16 20:43:32 +01:00
committed by Johannes Kirschbauer
parent b3c2035bbd
commit 43d998e6c0
5 changed files with 920 additions and 0 deletions

View File

@@ -899,6 +899,13 @@ checkConfigError 'Did you mean .enable., .ebe. or .enabled.\?' config ./error-ty
checkConfigError 'Did you mean .services\.myservice\.port. or .services\.myservice\.enable.\?' config.services.myservice ./error-typo-submodule.nix
checkConfigError 'Did you mean .services\.nginx\.virtualHosts\."example\.com"\.ssl\.certificate. or .services\.nginx\.virtualHosts\."example\.com"\.ssl\.certificateKey.\?' config.services.nginx.virtualHosts.\"example.com\" ./error-typo-deeply-nested.nix
# types.attrListOf
checkConfigOutput '"ok"' config.assertions ./declare-attrList.nix
checkConfigError 'A definition for option .attrListInt.badValue.a. is not of type .signed integer.. Definition values:' config.attrListIntStrict.badValue ./declare-attrList.nix
checkConfigError 'A definition for option .attrList.badListElem. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListElem ./declare-attrList.nix
checkConfigError 'A definition for option .attrList.badString. is not of type .attribute list of string.. TypeError: Definition values:' config.attrListStrict.badString ./declare-attrList.nix
checkConfigError 'A definition for option .attrList.badListString. is not of type .attribute list of string.. Each list element must be a single-key attribute set.' config.attrListStrict.badListString ./declare-attrList.nix
cat <<EOF
====== module tests ======
$pass Pass

View File

@@ -0,0 +1,730 @@
# Run with:
# cd nixpkgs
# ./lib/tests/modules.sh
{ lib, config, ... }:
let
inherit (lib)
mkOption
mkOrder
mkMerge
mkBefore
mkAfter
mkIf
mkOverride
mkDefault
mkForce
types
;
in
{
options = {
attrList = mkOption {
type = types.lazyAttrsOf (types.attrListOf types.str);
};
attrListInt = mkOption {
type = types.lazyAttrsOf (types.attrListOf types.int);
};
attrListSubmodule = mkOption {
type = types.attrListOf (
types.submodule {
options.port = mkOption {
type = types.int;
description = "Port number";
};
options.host = mkOption {
type = types.str;
default = "localhost";
description = "Hostname";
};
}
);
};
# Strict wrappers that force deep evaluation, for testing error cases
attrListStrict = mkOption {
type = types.lazyAttrsOf types.raw;
};
attrListIntStrict = mkOption {
type = types.lazyAttrsOf types.raw;
};
# either picks attrList when input is list/attrset, int when input is int
eitherAttrListOrInt = mkOption {
type = types.either (types.attrListOf types.str) types.int;
};
eitherAttrListOrIntFallback = mkOption {
type = types.either (types.attrListOf types.str) types.int;
};
eitherIntOrAttrList = mkOption {
type = types.either types.int (types.attrListOf types.str);
};
eitherIntOrAttrListFallback = mkOption {
type = types.either types.int (types.attrListOf types.str);
};
assertions = mkOption { };
};
imports = [
# Second module contributing to multiModule
{
attrListInt.multiModule = [
{ b = 2; }
];
}
];
config = {
# List input: pass-through
attrList.listInput = [
{ a = "alpha"; }
{ b = "beta"; }
];
# Attrset input with explicit ordering
attrList.attrsetOrdered = {
x = mkOrder 200 "x-val";
y = mkOrder 100 "y-val";
};
# Mixed: list elements at default priority, attrset with mkOrder
attrList.mixed = mkMerge [
[
{ m = "from-list"; }
]
{
n = mkOrder 50 "from-attrset";
}
];
# Multiple list definitions from separate modules
attrListInt.multiModule = [
{ a = 1; }
];
# Attrset without mkOrder uses default priority
attrList.attrsetNoOrder = {
foo = "bar";
baz = "qux";
};
# Empty list
attrList.empty = [ ];
# Ordering test: lower priority first
attrList.ordering = mkMerge [
{
last = mkOrder 1500 "last";
}
{
first = mkOrder 500 "first";
}
[
{ middle = "middle"; }
]
];
# List elements support mkOrder/mkBefore/mkAfter
attrList.listOrdering = [
(mkAfter { z = "after"; })
{ m = "default"; }
(mkBefore { a = "before"; })
];
# Plain list entries land at default priority (1000):
# they appear after mkOrder 999 and before mkOrder 1001.
attrList.listDefaultPrio = mkMerge [
{ after = mkOrder 1001 "after"; }
[
{ mid = "list-entry"; }
]
{ before = mkOrder 999 "before"; }
];
# mkBefore and mkAfter
attrList.beforeAfter = mkMerge [
{
z = mkAfter "after";
}
{
a = mkBefore "before";
}
[
{ m = "default"; }
]
];
# mkIf: conditional definition
attrList.withMkIf = mkMerge [
(mkIf true [
{ yes = "included"; }
])
(mkIf false [
{ no = "excluded"; }
])
];
# mkOverride: higher priority override wins
attrList.withOverride = mkMerge [
(mkOverride 100 [
{ replaced = "gone"; }
])
(mkOverride 50 [
{ winner = "wins"; }
])
];
# mkDefault: lower priority than normal
attrList.withDefault = mkMerge [
(mkDefault [
{ default = "overridden"; }
])
[
{ normal = "wins"; }
]
];
# mkForce on the whole option (should discard other defs)
attrList.withForce = mkMerge [
[
{ discarded = "gone"; }
]
(mkForce [
{ forced = "wins"; }
])
];
# mkForce with mkOrder inside
attrList.forceWithOrder = mkForce [
(mkAfter { second = "after"; })
(mkBefore { first = "before"; })
];
# mkForce on individual element values; non-forced entries are discarded.
# Discarded values use a mix of: plain value, mkDefault(abort ...), mkOverride 100 (abort ...).
# The abort variants verify laziness: peelProperties sees the wrapper without forcing the content.
attrListInt.forceElementValue = [
{ a = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ a = mkForce 42; }
{ a = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ b = 2; }
];
# mkForce on attrset format
attrList.forceAttrset = mkMerge [
[
{ discarded = "gone"; }
]
(mkForce {
x = mkOrder 200 "x-val";
y = mkOrder 100 "y-val";
})
];
# mkForce on repeated key: forced entries override non-forced
attrList.forceRepeatedKey = [
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ x = mkForce "wins"; }
{ x = mkForce "wins 2"; }
];
# mkForce on repeated key across mkMerge
attrList.forceRepeatedKeyMerge = mkMerge [
[
{ x = "unused: overridden by mkForce"; }
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
]
[
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
]
[
{ x = mkForce "forced"; }
]
];
# mkForce on repeated key in attrset format across mkMerge
attrList.forceRepeatedKeyAttrs = mkMerge [
{
x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
y = "kept";
}
{ x = mkForce "forced"; }
];
# mkForce only affects the key it's on, other keys survive
attrList.forcePartialAttrs = mkMerge [
{
x = "unused: overridden by mkForce";
y = "normal y";
}
{ x = mkForce "forced x"; }
];
# mkForce in attrset format overrides same key from list format
attrList.forceMixedFormats = mkMerge [
[
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ y = "list y"; }
]
{ x = mkForce "attrset forced x"; }
];
# Nesting: list format, mkOrder on element + mkForce on value
attrList.nestListOrderForce = mkMerge [
[
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
(mkOrder 500 { x = mkForce "forced-early"; })
(mkOrder 1500 { y = "late"; })
]
[
(mkOrder 100 { z = "earliest"; })
]
];
# Nesting: list format, mkOrder(mkForce(val)) on value
attrList.nestListOrderOfForce = mkMerge [
[
{ x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ y = "plain-early"; }
]
[
{ x = mkOrder 1500 (mkForce "forced-late"); }
{ z = mkOrder 500 "earliest"; }
]
[
{ x = "unused: overridden by mkForce"; }
{ w = mkOrder 1200 "mid"; }
]
];
# Nesting: list format, mkForce(mkOrder(val)) on value
attrList.nestListForceOfOrder = mkMerge [
[
{ x = "unused: overridden by mkForce"; }
{ y = "plain-early"; }
]
[
{ x = mkForce (mkOrder 1500 "forced-late"); }
{ z = mkOrder 500 "earliest"; }
]
[
{ x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated"); }
{ w = mkOrder 1200 "mid"; }
]
];
# Nesting: attrset format, mkOrder wrapping mkForce
attrList.nestAttrsOrderOfForce = mkMerge [
{
x = mkOverride 100 (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
y = "plain-early";
}
{
x = mkOrder 1500 (mkForce "forced-late");
z = mkOrder 500 "earliest";
}
{
x = "unused: overridden by mkForce";
w = mkOrder 1200 "mid";
}
];
# Nesting: attrset format, mkForce wrapping mkOrder
attrList.nestAttrsForceOfOrder = mkMerge [
{
x = "unused: overridden by mkForce";
y = "plain-early";
}
{
x = mkForce (mkOrder 1500 "forced-late");
z = mkOrder 500 "earliest";
}
{
x = mkDefault (abort "overridden by mkForce; laziness guarantee: MUST NOT be evaluated");
w = mkOrder 1200 "mid";
}
];
# mkIf false on individual element value filters it out (list format)
attrListInt.optionalValueList = [
{ a = mkIf true 1; }
{ b = mkIf false 2; }
{ c = 3; }
];
# mkIf false on individual element value filters it out (attrset format)
attrListInt.optionalValueAttrs = {
a = mkIf true 1;
b = mkIf false 2;
c = 3;
};
# submodule elemType: produces real valueMeta
attrListSubmodule = [
{
web = {
port = 80;
};
}
{
db = {
port = 5432;
host = "dbhost";
};
}
];
# either: attrList branch matches for list input
eitherAttrListOrInt = [
{ a = "hello"; }
{ b = "world"; }
];
# either: int input falls through to int branch
eitherAttrListOrIntFallback = 42;
# either (swapped): int first, attrList second — int input matches int
eitherIntOrAttrList = 42;
# either (swapped): list input falls through to attrList branch
eitherIntOrAttrListFallback = [
{ a = "hello"; }
];
# Bad: string where int expected
attrListInt.badValue = [
{ a = "not-an-int"; }
];
# Bad: list element with multiple keys
attrList.badListElem = [
{
a = "ok";
b = "extra";
}
];
# Bad: plain string instead of list or attrset
attrList.badString = "not-a-container";
# Bad: list element is a bare string, not a singleton attrset
attrList.badListString = [
"not a singleton attribute"
];
attrListStrict = builtins.mapAttrs (k: v: builtins.deepSeq v v) config.attrList;
attrListIntStrict = builtins.mapAttrs (k: v: builtins.deepSeq v v) config.attrListInt;
assertions =
let
c = lib.evalModules {
modules = [ ./declare-attrList.nix ];
};
cfg = c.config;
in
# List input preserves elements
assert
cfg.attrList.listInput == [
{ a = "alpha"; }
{ b = "beta"; }
];
# Attrset input with mkOrder: lower priority comes first
assert
cfg.attrList.attrsetOrdered == [
{ y = "y-val"; }
{ x = "x-val"; }
];
# Mixed input: mkOrder 50 < default 1000
assert
cfg.attrList.mixed == [
{ n = "from-attrset"; }
{ m = "from-list"; }
];
# Multiple definitions from separate modules concatenate
# (import module's definition comes before this module's)
assert
cfg.attrListInt.multiModule == [
{ b = 2; }
{ a = 1; }
];
# Attrset without mkOrder: all at default priority
assert builtins.length cfg.attrList.attrsetNoOrder == 2;
# Empty list stays empty
assert cfg.attrList.empty == [ ];
# List elements support mkOrder/mkBefore/mkAfter
assert
cfg.attrList.listOrdering == [
{ a = "before"; }
{ m = "default"; }
{ z = "after"; }
];
# Plain list entries are at default priority (1000)
assert
cfg.attrList.listDefaultPrio == [
{ before = "before"; }
{ mid = "list-entry"; }
{ after = "after"; }
];
# Ordering: 500 < 1000 (default) < 1500
assert
cfg.attrList.ordering == [
{ first = "first"; }
{ middle = "middle"; }
{ last = "last"; }
];
# mkBefore (500) < default (1000) < mkAfter (1500)
assert
cfg.attrList.beforeAfter == [
{ a = "before"; }
{ m = "default"; }
{ z = "after"; }
];
# mkIf true includes, mkIf false excludes
assert
cfg.attrList.withMkIf == [
{ yes = "included"; }
];
# mkOverride: only lowest priority override survives
assert
cfg.attrList.withOverride == [
{ winner = "wins"; }
];
# mkDefault is overridden by normal definitions
assert
cfg.attrList.withDefault == [
{ normal = "wins"; }
];
# mkForce discards other definitions
assert
cfg.attrList.withForce == [
{ forced = "wins"; }
];
# mkForce with mkOrder inside: ordering still works
assert
cfg.attrList.forceWithOrder == [
{ first = "before"; }
{ second = "after"; }
];
# mkForce on individual element values passes through
assert
cfg.attrListInt.forceElementValue == [
{ a = 42; }
{ b = 2; }
];
# mkForce on attrset format: discards other defs, ordering preserved
assert
cfg.attrList.forceAttrset == [
{ y = "y-val"; }
{ x = "x-val"; }
];
# mkForce on repeated key: forced entries override non-forced
assert
cfg.attrList.forceRepeatedKey == [
{ x = "wins"; }
{ x = "wins 2"; }
];
# mkForce on repeated key across mkMerge: forced wins
assert
cfg.attrList.forceRepeatedKeyMerge == [
{ x = "forced"; }
];
# mkForce on repeated key in attrset format: discards other x, keeps y
assert
cfg.attrList.forceRepeatedKeyAttrs == [
{ y = "kept"; }
{ x = "forced"; }
];
# mkForce only affects its own key
assert
cfg.attrList.forcePartialAttrs == [
{ y = "normal y"; }
{ x = "forced x"; }
];
# mkForce in attrset format overrides same key from list format
assert
cfg.attrList.forceMixedFormats == [
{ y = "list y"; }
{ x = "attrset forced x"; }
];
# Nesting: list format, mkOrder on element + mkForce on value
# z(100) < x-forced(500) < y(1500); x-discarded filtered by mkForce
assert
cfg.attrList.nestListOrderForce == [
{ z = "earliest"; }
{ x = "forced-early"; }
{ y = "late"; }
];
# Nesting: list format, mkOrder(mkForce(val)) on value
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
assert
cfg.attrList.nestListOrderOfForce == [
{ z = "earliest"; }
{ y = "plain-early"; }
{ w = "mid"; }
{ x = "forced-late"; }
];
# Nesting: list format, mkForce(mkOrder(val)) on value
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
assert
cfg.attrList.nestListForceOfOrder == [
{ z = "earliest"; }
{ y = "plain-early"; }
{ w = "mid"; }
{ x = "forced-late"; }
];
# Nesting: attrset format, mkOrder(mkForce(val))
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
assert
cfg.attrList.nestAttrsOrderOfForce == [
{ z = "earliest"; }
{ y = "plain-early"; }
{ w = "mid"; }
{ x = "forced-late"; }
];
# Nesting: attrset format, mkForce(mkOrder(val))
# z(500) < y(1000) < w(1200) < x-forced(1500); x-discarded entries filtered
assert
cfg.attrList.nestAttrsForceOfOrder == [
{ z = "earliest"; }
{ y = "plain-early"; }
{ w = "mid"; }
{ x = "forced-late"; }
];
# mkIf false on individual element value filters it out (list format)
assert
cfg.attrListInt.optionalValueList == [
{ a = 1; }
{ c = 3; }
];
# mkIf false on individual element value filters it out (attrset format)
assert
cfg.attrListInt.optionalValueAttrs == [
{ a = 1; }
{ c = 3; }
];
# submodule: value, option descriptions, and valueMeta with real configuration metadata
assert
cfg.attrListSubmodule == [
{
web = {
host = "localhost";
port = 80;
};
}
{
db = {
host = "dbhost";
port = 5432;
};
}
];
assert
builtins.map (m: m.configuration.config) c.options.attrListSubmodule.valueMeta.attrList == [
{
host = "localhost";
port = 80;
}
{
host = "dbhost";
port = 5432;
}
];
assert
builtins.map (
m:
builtins.mapAttrs (n: o: o.description) (builtins.removeAttrs m.configuration.options [ "_module" ])
) c.options.attrListSubmodule.valueMeta.attrList == [
{
host = "Hostname";
port = "Port number";
}
{
host = "Hostname";
port = "Port number";
}
];
# valueMeta.attrList has one entry per (non-filtered) element
assert
c.options.attrList.valueMeta.attrs.listInput.attrList == [
{ }
{ }
];
assert
c.options.attrList.valueMeta.attrs.attrsetOrdered.attrList == [
{ }
{ }
];
assert
c.options.attrList.valueMeta.attrs.mixed.attrList == [
{ }
{ }
];
assert c.options.attrList.valueMeta.attrs.empty.attrList == [ ];
assert
c.options.attrListInt.valueMeta.attrs.optionalValueList.attrList == [
{ }
{ }
];
# either: headError is null for valid attrList input, so attrList branch is picked
assert
cfg.eitherAttrListOrInt == [
{ a = "hello"; }
{ b = "world"; }
];
# either: headError is non-null for int input, so int branch is picked
assert cfg.eitherAttrListOrIntFallback == 42;
# either (swapped): int first — int input matches
assert cfg.eitherIntOrAttrList == 42;
# either (swapped): list input falls through to attrList branch
assert
cfg.eitherIntOrAttrListFallback == [
{ a = "hello"; }
];
# Error cases are tested via checkConfigError in modules.sh
"ok";
};
}

View File

@@ -167,6 +167,28 @@ in
elemType = str;
lazy = false;
}).description == "attribute set of string";
assert (attrListOf str).description == "attribute list of string";
assert (attrListOf int).description == "attribute list of signed integer";
assert (attrListOf bool).description == "attribute list of boolean";
assert (attrListOf (either int str)).description == "attribute list of (signed integer or string)";
assert (attrListOf (nullOr str)).description == "attribute list of (null or string)";
assert (attrListOf (listOf str)).description == "attribute list of list of string";
assert
(attrListOf (attrsOf int)).description == "attribute list of attribute set of signed integer";
assert (attrListOf (attrListOf str)).description == "attribute list of attribute list of string";
assert (attrListOf ints.positive).description == "attribute list of (positive integer, meaning >0)";
assert
(attrListOf (enum [
"a"
"b"
])).description == "attribute list of (one of \"a\", \"b\")";
assert
(attrListOf (strMatching "[0-9]+")).description
== "attribute list of string matching the pattern [0-9]+";
assert
(attrListOf (nonEmptyListOf str)).description == "attribute list of non-empty (list of string)";
assert (attrListOf (submodule { })).description == "attribute list of (submodule)";
assert (coercedTo str abort int).description == "signed integer or string convertible to it";
assert (coercedTo int abort str).description == "string or signed integer convertible to it";
assert (coercedTo bool abort str).description == "string or boolean convertible to it";

View File

@@ -20,6 +20,7 @@ let
isStorePath
isString
substring
sort
throwIf
toDerivation
toList
@@ -27,6 +28,7 @@ let
;
inherit (lib.lists)
concatLists
concatMap
elemAt
filter
foldl'
@@ -70,6 +72,9 @@ let
mergeDefinitions
fixupOptionType
mergeOptionDecls
defaultOrderPriority
defaultOverridePriority
mkOverride
;
inherit (lib.fileset)
isFileset
@@ -805,6 +810,145 @@ rec {
substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
};
attrListOf =
elemType:
mkOptionType rec {
name = "attrListOf";
description = "attribute list of ${
optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType
}";
descriptionClass = "composite";
check = {
__functor = _self: x: isList x || isAttrs x;
isV2MergeCoherent = true;
};
merge = {
__functor =
self: loc: defs:
(self.v2 { inherit loc defs; }).value;
v2 =
{ loc, defs }:
let
# Peel order and override properties from a value in any nesting order.
# Returns { value, prio, overridePrio }.
# mkOrder is stripped (we consume it for sorting).
# mkOverride is preserved in value (mergeDefinitions strips it).
peelProperties =
value:
let
type = value._type or null;
in
if type == "order" then
let
inner = peelProperties value.content;
in
{
inherit (inner) value overridePrio;
prio = value.priority;
}
else if type == "override" then
let
inner = peelProperties value.content;
in
{
inherit (inner) prio;
overridePrio = value.priority;
# Re-wrap mkOverride around the inner value (with mkOrder stripped)
value = mkOverride value.priority inner.value;
}
else
{
inherit value;
prio = defaultOrderPriority;
overridePrio = defaultOverridePriority;
};
# Extract { file, key, value, prio, overridePrio } from a single-key attrset,
# optionally wrapped in mkOrder at the element level (list format).
extractItem =
file: raw:
let
hasOrder = raw._type or null == "order";
item = if hasOrder then raw.content else raw;
key = head (attrNames item);
peeled = peelProperties item.${key};
in
if isAttrs item && length (attrNames item) == 1 then
peeled
// {
inherit file key;
prio = if hasOrder then raw.priority else peeled.prio;
}
else
throw "A definition for option `${showOption loc}' is not of type `${description}'. Each list element must be a single-key attribute set.";
# Convert a definition to a flat list of { file, key, value, prio, overridePrio }
defToItems =
def:
if isList def.value then
map (extractItem def.file) def.value
else
# isAttrs: properties are on the values directly
map (
key:
peelProperties def.value.${key}
// {
inherit (def) file;
inherit key;
}
) (attrNames def.value);
allItems = concatMap defToItems defs;
# Per key, find the highest override priority (lowest number)
winningOverridePrio = foldl' (
acc: item:
let
prev = acc.${item.key} or defaultOverridePriority;
in
if item.overridePrio < prev then
acc // { ${item.key} = item.overridePrio; }
else
# minimize `//` operations
acc
) { } allItems;
# Keep only items at the winning override priority for their key
items = sort (a: b: a.prio < b.prio) (
filter (
item: item.overridePrio == winningOverridePrio.${item.key} or defaultOverridePriority
) allItems
);
evals = filter (e: e.eval.optionalValue ? value) (
map (item: {
inherit (item) key;
eval = mergeDefinitions (loc ++ [ item.key ]) elemType [
{
inherit (item) file value;
}
];
}) items
);
in
{
headError = checkDefsForError check loc defs;
value = map (e: { ${e.key} = e.eval.optionalValue.value or e.eval.mergedValue; }) evals;
valueMeta.attrList = map (e: e.eval.checkedAndMerged.valueMeta) evals;
};
};
emptyValue = {
value = [ ];
};
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]);
getSubModules = elemType.getSubModules;
substSubModules = m: attrListOf (elemType.substSubModules m);
functor = elemTypeFunctor name { inherit elemType; } // {
type = payload: types.attrListOf payload.elemType;
};
nestedTypes.elemType = elemType;
};
attrsOf = elemType: attrsWith { inherit elemType; };
# A version of attrsOf that's lazy in its values at the expense of

View File

@@ -494,6 +494,23 @@ Composed types are types that take a type as parameter. `listOf
Displays the option as `foo.<id>` in the manual.
`types.attrListOf` *`t`*
: An ordered list of single-attribute attribute sets, where each value is of *`t`* type.
The output is always `[ { name1 = value1; } { name2 = value2; } ... ]`.
Definitions can be provided in two formats, which may be mixed via `lib.mkMerge`, `imports`, etc:
- **List format**: `[ { a = 1; } { b = 2; } ]` — each element must be a single-attribute attribute set.
Elements may be wrapped in `lib.mkOrder` (or `lib.mkBefore`/`lib.mkAfter`) to control ordering;
unwrapped elements use the default order priority.
- **Attribute set format**: `{ a = lib.mkOrder 100 1; b = 2; }` — each name-value pair becomes a single-attribute attribute set in the output.
Values may be wrapped in `lib.mkOrder` (or `lib.mkBefore`/`lib.mkAfter`) to control ordering.
Values without `lib.mkOrder` use the default priority.
Multiple definitions of the same option are concatenated and then sorted by priority.
Entries at the same priority level preserve their definition order.
`types.uniq` *`t`*