From 43d998e6c09494aa66ebd677a2aa63eb7d1b3f3d Mon Sep 17 00:00:00 2001 From: Robert Hensing Date: Thu, 16 Apr 2026 20:43:32 +0100 Subject: [PATCH] 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. --- lib/tests/modules.sh | 7 + lib/tests/modules/declare-attrList.nix | 730 ++++++++++++++++++ lib/tests/modules/types.nix | 22 + lib/types.nix | 144 ++++ .../development/option-types.section.md | 17 + 5 files changed, 920 insertions(+) create mode 100644 lib/tests/modules/declare-attrList.nix diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 515ab169fe43..ac9cc83030d5 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -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 <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"; diff --git a/lib/types.nix b/lib/types.nix index 5a42ada60d84..34d5b600322e 100644 --- a/lib/types.nix +++ b/lib/types.nix @@ -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 diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index cc195abcc37b..24b966fbf3b3 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -494,6 +494,23 @@ Composed types are types that take a type as parameter. `listOf Displays the option as `foo.` 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`*