diff --git a/lib/attrsets.nix b/lib/attrsets.nix index ed2fa9770410d..a362e5b00d9ac 100644 --- a/lib/attrsets.nix +++ b/lib/attrsets.nix @@ -2258,4 +2258,223 @@ rec { ) intersection; in (x // y) // mask; + + /** + Determine whether a value is a leaf node when traversing a `nestedAttrsOf elemType` structure. + + `isNestedAttrsLeaf :: ((String -> Bool) -> [String] -> Bool) -> OptionType -> Any -> Bool` + + For scalar element types (e.g. `int`, `str`), uses the type's `.check`. + For submodule types, given their native type check works only after merge, + instead check key presence, controlled by `predicate` (`lib.any` or `lib.all`) + depending on whether the element submodule should allow additional keys. + + # Inputs + + `predicate` + + : 1\. A predicate combinator: `lib.any` treats a value as a leaf if *any* key matches a declared option; `lib.all` requires *all* keys to match + + `elemType` + + : 2\. The element option type of the `nestedAttrsOf` structure + + `v` + + : 3\. The value to test + + # Example + + ```nix + let + elementType = types.submodule { options.result = mkOption { type = types.int; }; }; + in + isNestedAttrsLeaf lib.any elementType { result = 1; } + # => true + isNestedAttrsLeaf lib.any elementType { a.b = { result = 1; }; } + # => false (intermediate attrset, no key matches "result") + ``` + */ + isNestedAttrsLeaf = + predicate: elemType: v: + let + # Top-level option names of the element type, used to detect leaves. + # Mirrors the heuristic in `nestedAttrsOf.merge`. + elemOptAttrs = elemType.getSubOptions [ ]; + in + if elemOptAttrs == { } then + elemType.check v + else + lib.isAttrs v && predicate (k: elemOptAttrs ? ${k}) (lib.attrNames v); + + /** + Map a function over all leaves in a `nestedAttrsOf` structure. + + `mapNestedAttrsWith :: ((String -> Bool) -> [String] -> Bool) -> nestedAttrsOf a -> (a -> b) -> nestedAttrsOf a -> nestedAttrsOf b` + + Traverses the nested attribute set recursively, applying `fn` only at leaf + values as determined by `isNestedAttrsLeaf predicate`. Intermediate attrset + nodes are preserved as-is in the output structure. + + # Inputs + + `predicate` + + : 1\. Leaf-detection combinator: `lib.any` or `lib.all` + + `type` + + : 2\. A `nestedAttrsOf` option type whose element type identifies leaves + + `fn` + + : 3\. Function applied to each leaf value + + `value` + + : 4\. The nested attribute set (or leaf) to traverse + + # Example + + ```nix + mapNestedAttrsWith lib.any (types.nestedAttrsOf types.int) (x: x * 2) { a.b = 1; a.c = 3; d = 5; } + # => { a.b = 2; a.c = 6; d = 10; } + ``` + */ + mapNestedAttrsWith = + predicate: + let + recurse = + type: fn: value: + if isNestedAttrsLeaf predicate type.nestedTypes.elemType value then + fn value + else + mapAttrs (_: recurse type fn) value; + in + recurse; + + /** + Map a function over all leaves in a `nestedAttrsOf` structure, preserving the structure. + + `mapNestedAttrs' :: nestedAttrsOf a -> (a -> b) -> nestedAttrsOf a -> nestedAttrsOf b` + + Traverses the nested attribute set recursively, + applying `fn` only at leaf values. + Intermediate attrset nodes are preserved as-is. + For submodule element types allowing additional keys, + while presuming no key of the nestred attrset structure is contained in the element submodule type. + + # Inputs + + `type` + + : 1\. A `nestedAttrsOf` option type whose element type identifies leaves + + `fn` + + : 2\. Function applied to each leaf value; result replaces the leaf in the output + + `value` + + : 3\. The nested attribute set to traverse + + # Example + + ```nix + mapNestedAttrs' (types.nestedAttrsOf types.int) (x: x * 2) { a.b = 1; a.c = 3; d = 5; } + # => { a.b = 2; a.c = 6; d = 10; } + ``` + */ + mapNestedAttrs' = mapNestedAttrsWith lib.any; + + /** + Like `concatMapNestedAttrs'`, but with a configurable leaf-detection predicate. + + `concatMapNestedAttrsWith :: ((String -> Bool) -> [String] -> Bool) -> nestedAttrsOf elemType -> ([String] -> elemType -> AttrSet b) -> nestedAttrsOf elemType -> AttrSet b` + + The `predicate` argument controls how submodule-typed leaves are detected from + intermediate namespace attrsets: pass `lib.any` (used by `concatMapNestedAttrs'`) + to treat a value as a leaf if *any* of its keys is a declared option of the element + type, or `lib.all` to require *all* keys to match. + + # Inputs + + `predicate` + + : 1\. Leaf-detection combinator: `lib.any` or `lib.all` + + `type` + + : 2\. A `nestedAttrsOf` option type whose element type identifies leaves + + `fn` + + : 3\. Function called at each leaf: receives the path (list of strings) and leaf value, returns an attrset + + `attrs` + + : 4\. The nested attribute set to traverse + + # Example + + ```nix + concatMapNestedAttrsWith lib.all (nestedAttrsOf (submodule { options.result = mkOption { type = int; }; })) + (path: v: { ${concatStringsSep "." path} = v.result; }) + { a.b = { result = 1; }; a.c = { result = 2; }; } + # => { "a.b" = 1; "a.c" = 2; } + ``` + */ + concatMapNestedAttrsWith = + predicate: type: fn: + let + recurse = + path: attrs: + lib.concatMapAttrs ( + name: v: + if isNestedAttrsLeaf predicate type.nestedTypes.elemType v then + fn (path ++ [ name ]) v + else + recurse (path ++ [ name ]) v + ) attrs; + in + recurse [ ]; + + /** + Like `concatMapAttrs`, but recurses into nested attribute sets, calling `fn` + only at leaves as determined by the element type of the given `nestedAttrsOf` type. + + The function `fn` receives the full path (as a list of strings) to the leaf + and the leaf value, and must return an attribute set to be merged into the result. + + Leaf detection mirrors `nestedAttrsOf.merge`: for simple element types (e.g. `int`) + `elemType.check` is used directly; for submodule element types (whose `check = isAttrs` + cannot distinguish leaves from intermediate nodes) a value is treated as a leaf when + all of its keys are declared options of the element type. + + `concatMapNestedAttrs' :: nestedAttrsOf elemType -> ([String] -> elemType -> AttrSet b) -> nestedAttrsOf elemType -> AttrSet b` + + # Inputs + + `type` + + : 1\. A `nestedAttrsOf` option type whose element type identifies leaves + + `fn` + + : 2\. Function called at each leaf: receives the path (list of strings) and leaf value, returns an attrset + + `attrs` + + : 3\. The nested attribute set to traverse + + # Example + + ```nix + concatMapNestedAttrs' (nestedAttrsOf (submodule { options.result = mkOption { type = int; }; })) + (path: v: { ${concatStringsSep "." path} = v.result; }) + { a.b = { result = 1; }; a.c = { result = 2; }; } + # => { "a.b" = 1; "a.c" = 2; } + ``` + */ + concatMapNestedAttrs' = concatMapNestedAttrsWith lib.any; } diff --git a/lib/contracts/default.nix b/lib/contracts/default.nix new file mode 100644 index 0000000000000..6c8b098438bf1 --- /dev/null +++ b/lib/contracts/default.nix @@ -0,0 +1,11 @@ +# set of utilities for using contracts. +# templates of individual contracts are stored in `./templates`. +{ lib, ... }: +let + callLibs = file: import file { inherit lib; }; +in +{ + module = ./module.nix; + definitionType = callLibs ./definition-type.nix; +} +// callLibs ./helpers.nix diff --git a/lib/contracts/definition-type.nix b/lib/contracts/definition-type.nix new file mode 100644 index 0000000000000..79f6284c1da09 --- /dev/null +++ b/lib/contracts/definition-type.nix @@ -0,0 +1,329 @@ +{ lib, ... }: +let + inherit (lib) mkOption types; + inherit (types) + attrs + attrsOf + functionTo + listOf + optionDeclaration + optionType + raw + str + submodule + ; +in +submodule (contract: { + options = { + meta = mkOption { + description = '' + Useful information about the contract and its maintenance. + ''; + type = submodule { + options = { + description = mkOption { + description = '' + Description of the contract. + ''; + type = str; + }; + maintainers = mkOption { + description = '' + Maintainers of the contract. + ''; + type = listOf attrs; + }; + }; + }; + }; + interface = mkOption { + description = '' + Interface describing the types used in the contract. + ''; + default = { }; + type = + let + type = attrsOf optionDeclaration; + default = { }; + in + submodule { + options = { + request = mkOption { + description = "Request type of the contract."; + inherit type default; + }; + result = mkOption { + description = "Result type of the contract."; + inherit type default; + }; + extraImports = mkOption { + description = "Extra imports for the request and result submodules (e.g. rename shims)."; + default = { }; + type = submodule { + options = lib.genAttrs [ "request" "result" ] ( + k: + mkOption { + description = "Extra imports for the ${k} submodule."; + type = listOf raw; + default = [ ]; + } + ); + }; + }; + }; + }; + }; + mkContract = mkOption { + description = '' + Augment the contract interface's type using a set of overrides. + + `contract.mkContract :: attrs -> optionType` + + **Inputs:** + + `overrides` + + : 1\. A (recursive) attrset of fields to add to the contract interface submodule type + + **Example:** + + ```nix + { config, lib, ... }: + let + inherit (lib) mkOption contract types; + in + { + options.foo = mkOption { + default = { }; + type = config.contractType."".mkContract + { + bar = { + default = 10; + defaultText = "10"; + }; + }; + }; + } + ``` + ''; + type = functionTo optionType; + readOnly = true; + default = + overrides: + let + inherit (contract.config) interface; + in + lib.extendSubmodule overrides (submodule { + options = lib.mapAttrs ( + k: options: + mkOption { + description = "The ${k} of the contract instance."; + type = submodule { + imports = interface.extraImports.${k}; + inherit options; + }; + } + ) (lib.getAttrs [ "request" "result" ] interface); + }); + }; + _mkProviderType = mkOption { + description = '' + Create a `nestedAttrsOf` type for provider instances. + + Note that this should not be used directly, as `defaults` specified get lost once any value gets set manually. + Instead, use `config.contracts..mkProviderType`. + + `._mkProviderType :: { providerOptions?, overrides?, fulfill?, fulfill'? } -> optionType` + + **Inputs:** + + `overrides` + + : 1\. Overrides for `{ request, result }` submodule types (to e.g. add defaults) + + `providerOptions` + + : 2\. Additional option declarations of the provider outside of the contract's request/result. + + `fulfill` + + : 3\. Optional function `request -> result` that derives result values + from request values. Applied with `mkDefault` priority so explicit + result settings take precedence. Use `fulfill'` if the result also + needs the instance `name`. + + `fulfill'` + + : 4\. Optional function `{ request, name, instance } -> result`. Lower-level + variant of `fulfill` exposing the instance `name` and the full submodule + `instance` (including provider-specific options). At most one of + `fulfill` / `fulfill'` may be set. + + `_requests` + + : 5\. Internal. Pre-bound by `contracts..mkProviderType`, which is the + recommended call site for providers. Forwards consumer `want` request data + into each leaf at `mkDefault` priority (1000) so provider-specific options + do not silently mask consumer wants via `nestedAttrsOf` leaf-priority filtering. + + **Example:** + + ```nix + { lib, config, options, ... }: + let + inherit (config.contracts) arithmetic; + in + { + imports = [ + # simple dummy contract with request/result both shaped `{ value: int }` + + ]; + options.services.increment.arithmetic = lib.mkOption { + default = arithmetic.providerRequests.increment; + type = arithmetic.mkProviderType { + fulfill = request: { + value = request.value + 1; + }; + }; + }; + config = { + contracts.arithmetic.providers.increment.module = options.services.increment.arithmetic; + }; + } + ``` + ''; + type = functionTo optionType; + readOnly = true; + default = + { + providerOptions ? { }, + overrides ? { }, + fulfill ? null, + fulfill' ? null, + _requests ? null, + }: + assert lib.assertMsg ( + fulfill == null || fulfill' == null + ) "mkProviderType: at most one of `fulfill` and `fulfill'` may be set."; + let + fulfill'' = if fulfill != null then ({ request, ... }: fulfill request) else fulfill'; + inherit (contract.config) interface; + mkExtended = + k: + lib.extendSubmodule (overrides.${k} or { }) (submodule { + imports = interface.extraImports.${k}; + options = interface.${k}; + }); + in + types.nestedAttrsOf ( + types.submodule ( + [ + { + options = { + request = mkOption { + description = "Request of the contract instance."; + type = mkExtended "request"; + }; + result = mkOption { + description = "Result of the contract instance."; + default = { }; + type = mkExtended "result"; + }; + } + // providerOptions; + } + # Forward consumer `want` request data at `mkDefault` (priority 1000) + # so it beats `overrides.request..default` (`mkOptionDefault`, + # priority 1500) but loses to explicit deployer writes (normal, 100). + # Without this, writing any provider-specific option causes + # `nestedAttrsOf`'s leaf-level priority filtering to drop the outer + # `mkOptionDefault` default that carried the consumer's want, + # leaving `overrides.request..default` as the only surviving + # value -- silently masking the consumer's declared overrides. + # + # The path-split search strips the provider option path prefix from + # `options.request.loc` to recover the leaf's position within + # `_requests` (the want-derived request tree pre-bound by the caller). + ( + { options, ... }: + if _requests == null then + { } + else + { + config.request = + let + leafPath = lib.init options.request.loc; + matchN = lib.findFirst ( + n: + let + v = lib.attrByPath (lib.drop n leafPath) null _requests; + in + v != null && v ? request + ) null (lib.range 0 (lib.length leafPath)); + wantRequest = + if matchN != null then (lib.attrByPath (lib.drop matchN leafPath) null _requests).request else null; + in + lib.mkIf (wantRequest != null) (lib.mkDefault wantRequest); + } + ) + ] + ++ lib.optional (fulfill'' != null) ( + { config, name, ... }: + { + config.result = lib.mkDefault (fulfill'' { + inherit (config) request; + inherit name; + instance = config; + }); + } + ) + ) + ); + }; + behaviorTest = mkOption { + description = '' + Test used to ensure all `providers` of the contract behave the same way. + + For an example of how to write a test for a contract, + see the `behaviorTest` in `lib/contracts/file-secrets.nix`. + ''; + # The type should be more precise of course. + # There should actually be a NixOSTest type. + # And we can probably do something fancy with the `request` and `result` modules. + type = functionTo attrs; + default = + { + name, + extraModules ? [ ], + }: + { + name = "contracts__${name}"; + containers.machine = + { ... }: + { + imports = extraModules; + }; + testScript = + { ... }: + '' + machine.succeed("echo 'please define a test!' >&2; exit 1") + ''; + }; + defaultText = lib.literalExpression '' + { + name, + extraModules ? [ ], + }: + { + name = "contracts__''${name}"; + containers.machine = + { ... }: + { + imports = extraModules; + }; + testScript = { ... }: ""; + } + ''; + }; + }; +}) diff --git a/lib/contracts/definitions/default.nix b/lib/contracts/definitions/default.nix new file mode 100644 index 0000000000000..19507fcd26a72 --- /dev/null +++ b/lib/contracts/definitions/default.nix @@ -0,0 +1,9 @@ +# collection of contract templates, defined in `lib` so we can still build the manual. +# declarations of individual contracts follow the type in `./definition-type.nix`. +{ lib, ... }: +lib.mapAttrs ( + _name: path: + lib.evalOption (lib.mkOption { + type = lib.contract.definitionType; + }) (import path { inherit lib; }) +) { fileSecrets = ./file-secrets.nix; } diff --git a/lib/contracts/definitions/file-secrets.nix b/lib/contracts/definitions/file-secrets.nix new file mode 100644 index 0000000000000..d3898b0d0c61e --- /dev/null +++ b/lib/contracts/definitions/file-secrets.nix @@ -0,0 +1,129 @@ +{ + lib, + ... +}: +let + inherit (lib) mkOption types; + inherit (types) str; +in +{ + meta = { + description = '' + Contract for secrets handling where a consumer requests a secret + and a provider provides it at runtime at a given file path. + ''; + maintainers = with lib.maintainers; [ + ibizaman + kiara + ]; + }; + interface = { + request = { + mode = mkOption { + description = '' + Mode the secret file must have. + ''; + type = str; + default = "0400"; + }; + + owner = mkOption { + description = '' + Linux user that must own the secret file. + ''; + type = str; + }; + + group = mkOption { + description = '' + Linux group that must own the secret file. + ''; + type = str; + }; + }; + result = { + path = mkOption { + type = str; + description = '' + Path to the file containing the secret generated out of band. + + This path will exist after deploying to a target host, + it is not available through the nix store. + ''; + }; + }; + }; + behaviorTest = + { + name, + wantPath, + extraModules ? [ ], + }: + lib.contract.mkBehaviorTest { + contractName = "fileSecrets"; + testName = name; + inherit wantPath extraModules; + nodeModule = + { config, ... }: + { + options.test = { + owner = mkOption { + type = str; + default = "root"; + }; + + group = mkOption { + type = str; + default = "root"; + }; + + mode = mkOption { + type = str; + default = "0400"; + }; + + content = mkOption { + type = str; + default = "a super secret secret!"; + }; + }; + + config = lib.mkMerge [ + (lib.mkIf (config.test.owner != "root") { + users.users.${config.test.owner}.isNormalUser = true; + }) + (lib.mkIf (config.test.group != "root") { + users.groups.${config.test.group} = { }; + }) + ]; + }; + requestOf = config: { inherit (config.test) owner group mode; }; + testScript = + { result, nodes }: + let + cfg = nodes.machine; + in + '' + owner = machine.succeed("stat -c '%U' ${result.path}").strip() + print(f"Got owner {owner}") + if owner != "${cfg.test.owner}": + raise Exception(f"Owner should be '${cfg.test.owner}' but got '{owner}'") + + group = machine.succeed("stat -c '%G' ${result.path}").strip() + print(f"Got group {group}") + if group != "${cfg.test.group}": + raise Exception(f"Group should be '${cfg.test.group}' but got '{group}'") + + mode = str(int(machine.succeed("stat -c '%a' ${result.path}").strip())) + print(f"Got mode {mode}") + wantedMode = str(int("${cfg.test.mode}")) + if mode != wantedMode: + raise Exception(f"Mode should be '{wantedMode}' but got '{mode}'") + + content = machine.succeed("cat ${result.path}").strip() + print(f"Got content {content}") + if content != "${cfg.test.content}": + raise Exception(f"Content should be '${cfg.test.content}' but got '{content}'") + ''; + }; +} diff --git a/lib/contracts/helpers.nix b/lib/contracts/helpers.nix new file mode 100644 index 0000000000000..d12b728c0bc21 --- /dev/null +++ b/lib/contracts/helpers.nix @@ -0,0 +1,126 @@ +{ lib, ... }: +{ + /** + Whether a value is a contract instance (an attrset with `request` and `result`). + + Useful when an option accepts both a contract type and a plain value + (e.g. `types.oneOf [ types.path contractType ]` or `types.nullOr contractType`) + and the consumer needs to branch on which was provided. + + lib.contract.isInstance :: a -> bool + + # Example + + ```nix + if lib.contract.isInstance cfg.passwordFile + then cfg.passwordFile.result.path + else cfg.passwordFile + ``` + */ + isInstance = v: lib.isAttrs v && v ? result; + + /** + Rebind every contract's `mkProviderType` against a NixOS module's `config`, + returning an attrset shaped like `lib.contracts` extended with any + contracts defined inline on `config.contractDefinitions`. + + This makes for the idiomatic way to access `mkProviderType` for providers subject + to the sandboxing used in builds of the NixOS manual. + In other cases, one may instead use `config.contracts..mkProviderType`. + + For each contract, prefers the bridge (`config.contracts..mkProviderType`) + when available - the bridge pre-binds `_requests` so consumer `want` request + data is forwarded into each leaf (see `_mkProviderType`'s `_requests` + parameter in `./definition-type.nix`). Falls back to the pure lib function + when `config.contracts` is absent (e.g. inside the manual docs build's + per-module sandbox, where the contracts module is not imported); the lib + version produces an identical option type shape, only without runtime want + forwarding, which is irrelevant to rendered docs. + + Use this in provider modules to bind one or more contracts at once, + including contracts defined inline on `config.contractDefinitions`: + + ```nix + inherit (lib.contract.forModule config) fileSecrets databaseConnection; + # then: type = fileSecrets.mkProviderType { ... }; + # type = databaseConnection.mkProviderType { ... }; + ``` + + lib.contract.forModule :: moduleConfig -> { = contract; ... } + */ + forModule = + moduleConfig: + let + contracts = lib.contracts // (moduleConfig.contractDefinitions or { }); + in + lib.mapAttrs ( + name: contract: + contract + // { + mkProviderType = moduleConfig.contracts.${name}.mkProviderType or contract._mkProviderType; + } + ) contracts; + + /** + Generic skeleton for a contract `behaviorTest`. + + Abstracts the contract-name prefix, want-path wiring, and result extraction, + leaving only per-contract pieces (test options, request shape, nodeConfig, + testScript body) to the caller. + + lib.contract.mkBehaviorTest :: { contractName, testName, wantPath, ... } -> NixOSTest + + # Arguments + + - `contractName`: attrset key under `config.contracts` (e.g. `"fileSecrets"`) + - `testName`: test-name suffix; produces `contracts__` + - `wantPath`: attr-path list into `contracts..want` + - `extraModules`: extra NixOS modules for the test machine (optional, default `[]`) + - `nodeModule`: NixOS module that defines per-contract `options` and their config + (e.g. `options.test.*`, user/group autocreation) + - `requestOf`: `config -> request` - derive the contract request attrset from + the evaluated node config + - `testScript`: `{ result, nodes } -> string` - return the Python test script; + `result` is the already-resolved result attrset for `wantPath` + */ + mkBehaviorTest = + { + contractName, + testName, + wantPath, + extraModules ? [ ], + nodeModule, + requestOf, + testScript, + }: + { + name = "contracts_${contractName}_${testName}"; + nodes.machine = + { config, ... }: + { + imports = extraModules ++ [ nodeModule ]; + config = lib.setAttrByPath ( + [ + "contracts" + contractName + "want" + ] + ++ wantPath + ++ [ "request" ] + ) (requestOf config); + }; + testScript = + { nodes, ... }: + testScript { + result = lib.getAttrFromPath ( + [ + "contracts" + contractName + "results" + ] + ++ wantPath + ) nodes.machine; + inherit nodes; + }; + }; +} diff --git a/lib/contracts/module.nix b/lib/contracts/module.nix new file mode 100644 index 0000000000000..ecebbfc807793 --- /dev/null +++ b/lib/contracts/module.nix @@ -0,0 +1,741 @@ +{ lib, config, ... }: +let + inherit (lib) mkOption types; + inherit (types) + attrsOf + enum + nestedAttrsOf + nullOr + raw + submodule + ; + # `or` fallback allows the docs build sandbox to evaluate this module: + # the sandbox passes a fake `config` via `specialArgs` that lacks this attribute. + contractDefinitions = config.contractDefinitions or lib.contracts; + + # A provider *reference* (`{ module, contract? }`) like `raw`, but mergeable: + # multiple definitions are allowed as long as they all agree on + # `module.loc` (the only field routing reads -- `routedName` matches a + # reference to a provider by its `module.loc`). `raw`'s `mergeOneOption` + # throws on any second definition; `mergeEqualOption` cannot help because two + # references to the same provider differ in `module.value` (a thunk/function, + # never `==`). Comparing only `module.loc` lets the legitimate duplicates a + # node can accumulate -- e.g. a host that co-locates several apps, each of + # whose routing modules sets the same node-wide `defaultProvider` -- collapse + # to one, while still rejecting a genuine conflict (two different providers). + # Used for the single-reference `defaultProvider` option; the nested + # `instances` tree uses `mergeableRaw` (see below) at its leaves instead. + providerRefType = lib.mkOptionType { + name = "providerRef"; + description = "contract provider reference"; + # Same permissive check as `raw`: a reference is an opaque `{ module; ... }`. + check = lib.types.raw.check; + merge = + loc: defs: + let + locOf = def: def.value.module.loc or null; + first = lib.head defs; + firstLoc = locOf first; + allAgree = lib.all (def: locOf def == firstLoc) defs; + in + if allAgree then + first.value + else + throw "The option `${lib.showOption loc}' has conflicting provider references (different `module.loc`) from: ${ + lib.concatMapStringsSep ", " (def: def.file) defs + }."; + }; + + # `raw` for a provider-reference tree, but with a proper leaf merge: agreeing + # duplicate definitions collapse, genuine conflicts throw. `raw`'s own merge + # is `mergeOneOption`, which rejects any second definition -- even an identical + # one -- so a combined node whose co-located apps each route a shared instance + # to the same provider would spuriously fail. `mergeEqualOption` is the + # standard nixpkgs "merge properly" for such opaque leaves. `instances` leaves + # are provider references that `nestedAttrsOf` recurses into (their sub-tree + # keys `module`/`contract` are opaque to `raw`), so this merge only fires at + # the genuine scalar leaves, where identical references are byte-equal and a + # real conflict differs. + mergeableRaw = raw // { + merge = lib.mergeEqualOption; + }; +in +{ + options = { + contractDefinitions = mkOption { + description = '' + Types of contracts. + For info on how to instantiate these, see `config.contracts`. + + To create a new contract type, add an instance of `config.contractDefinitions.""` + defining `meta` and `interface` options, or when adding to nixpkgs, + preferably adding one in `lib/contracts`. + + Nixpkgs-shipped contract types (`lib.contracts`) are available automatically + via a fallback in `contracts` option generation. Additional (user-defined) + contract types added here are merged alongside the lib-shipped ones. + + **Integrating into a new module system** (e.g. home-manager, nix-darwin): + + 1\. Import this module (`lib.contract.module`). + + 2\. Seed `config.contractDefinitions` with `lib.contracts` so that nixpkgs-shipped + contract definitions are available alongside any user-defined types. + + Both steps are combined in a thin wrapper module; see + `nixos/modules/contracts/default.nix` for the reference implementation. + ''; + # types are in `lib` as the docs build's sandbox has no `config`. + type = attrsOf lib.contract.definitionType; + }; + contracts = mkOption { + description = '' + Contract instances, keyed by contract type. + + This option is system-agnostic - it works identically in NixOS + and any module system that imports `lib.contract.module`. + + Consumers set `contracts..want`, providers set `contracts..providers`, + and results are read from `contracts..results`. + ''; + type = submodule { + options = lib.mapAttrs ( + contractName: contractType: + let + inherit (contractType) meta interface; + wantType = nestedAttrsOf (submodule { + options = { + request = mkOption { + description = '' + The request parameters. + Must match the `${contractName}` contract interface's request type. + ''; + type = submodule { + imports = interface.extraImports.request; + options = interface.request; + }; + }; + result = mkOption { + description = '' + Result returned to the request by the provider's side of the `${contractName}` contract. + Must match the `${contractName}` contract interface's result type. + ''; + type = submodule { + imports = interface.extraImports.result; + options = interface.result; + }; + }; + }; + }); + in + mkOption { + description = '' + ${meta.description} + + Providers for the contract may be implemented by defining an option as follows: + + ```nix + { lib, config, ... }: + let + inherit (lib.contract.forModule config) ${contractName}; + # or, outside of modules NixOS wants to build sandboxed for the manual: + # inherit (config.contracts) ${contractName}; + in + { + options = { + ${contractName} = lib.mkOption { + description = ${"'"}' + Instances of contract `${contractName}`, including contract request/result and provider-specific options. + + Option `config.contracts.${contractName}.instances` refers to providers' options like this one. + ${"'"}'; + example = lib.literalExpression ${"'"}' + { + ""."" = { + request = { + # options shared between any provider of the `${contractName}` contract + # "" = ...; + }; + # provider-specific options: + # "" = ...; + }; + } + ${"'"}'; + type = ${contractName}.mkProviderType { + # overrides.request = { "".default = ...; }; + # overrides.result = { "".default = ...; }; + # providerOptions = { + # "" = lib.mkOption { + # type = lib.types.""; + # description = "A provider-specific option."; + # }; + # }; + fulfill = request: { + # "" = ...; + }; + # fulfill' = { request, name }: { ... }; # variant exposing instance name + }; + }; + }; + } + ``` + ''; + type = submodule ( + contract: + let + # Resolve a provider reference (`{ module, contract? }`) to the + # provider's contract instances - the `instances` apply, lifted + # out so `resolvedInstances` and `providerRequests` share it. + resolveRef = + v: + if v.contract or null != null then + lib.getAttrFromPath v.contract v.module.value + else if v.module.value ? ${contractName} then + v.module.value.${contractName} + else + v.module.value; + # The `providers` key whose registration references the same + # option as `ref` (matched on `module.loc`, a list of strings + # cheap to force - unlike the provider's value, which may not + # even evaluate outside its own NixOS context). `null` when no + # provider matches (e.g. a hand-written resolved instance). + routedName = + ref: + lib.findFirst ( + name: contract.config.providers.${name}.module.loc or null == ref.module.loc or null + ) null (lib.attrNames contract.config.providers); + # Walk the `instances` ref tree, applying `f path ref` at each + # leaf (`v ? module`). Path-aware so a ref nested above several + # leaves resolves per leaf. Non-ref attrsets recurse; non-attrs + # (already-resolved leaves) pass through. + resolveInstanceTree = + f: + let + walk = + path: v: + if !lib.isAttrs v then + v + else if v ? module then + f path v + else + lib.mapAttrs (n: walk (path ++ [ n ])) v; + in + walk [ ] contract.config.instances; + # Path-aware filter over a `nestedAttrsOf` tree, preserving + # structure: keep each leaf (per `wantType`'s leaf detection) + # only when `keep path leaf`, and prune now-empty namespaces. + filterNestedAttrs = + keep: + let + recurse = + path: v: + if lib.isNestedAttrsLeaf lib.any wantType.nestedTypes.elemType v then + lib.optionalAttrs (keep path v) v + else + lib.filterAttrs (_: sub: sub != { }) (lib.mapAttrs (n: recurse (path ++ [ n ])) v); + in + recurse [ ]; + in + { + options = { + want = mkOption { + description = '' + Requests declared by consumers of the `${contractName}` contract, consisting of + request inputs and (once fulfilled) the provider's returned results. + + `want` uses `nestedAttrsOf`, so consumers can organize entries at any depth: + + ```nix + contracts.${contractName}.want."" = { + secret = cfg.secret; # flat + db.primary = cfg.primaryDb; # grouped + caches.region-a.fast = cfg.fastCache; # deeply nested + }; + ``` + + Results mirror the same structure: + `config.contracts.${contractName}.results."".db.primary`, etc. + + **NixOS module consumer:** + + ```nix + { lib, config, ... }: + let + cfg = config.services.""; + inherit (lib.contracts) ${contractName}; + in + { + options.services.""."