Last active
April 28, 2026 15:36
-
-
Save KiaraGrouwstra/93e819314c5902b7403530428dd74164 to your computer and use it in GitHub Desktop.
contracts web form
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Demo: derive a JSON schema for a contract type's chosen-per-instance | |
| # provider configuration. | |
| # | |
| # `contracts.<x>.providers.<name>` stores `{ module, contract? }` where | |
| # `module` is the provider's option set; downstream tooling (GUIs, schema | |
| # generators) introspects `.loc`/`.type` to render forms. Per-instance | |
| # overrides go through the same `instances.<consumer>.<...>` option, with | |
| # `apply` resolving any `{ module, contract? }` leaf path-aware. | |
| # | |
| # Driven by `flake.nix` (`nix run .`); both `nixpkgs` and `clanJsonschema` | |
| # are required arguments. | |
| { | |
| nixpkgs, | |
| clanJsonschema, | |
| }: | |
| let | |
| lib = import (nixpkgs + "/lib"); | |
| # Minimal clanLib stub - jsonschema only uses these two helpers. | |
| clanLib = { | |
| concatMapListToAttrs = | |
| f: list: | |
| lib.zipAttrsWith (_name: values: lib.last values) (map f list); | |
| toUpperFirst = | |
| str: | |
| let | |
| inherit (lib.strings) toUpper substring; | |
| in | |
| (toUpper (substring 0 1 str)) + substring 1 (-1) str; | |
| }; | |
| jsonschema = import clanJsonschema { inherit lib clanLib; }; | |
| # Evaluate the contracts wrapper + the hardcoded-secret provider module, | |
| # with two demo consumer requests so the `instances` schema has leaves | |
| # to mirror. | |
| evaled = lib.evalModules { | |
| class = "nixos"; | |
| modules = [ | |
| (nixpkgs + "/nixos/modules/contracts/default.nix") | |
| (nixpkgs + "/nixos/modules/testing/hardcoded-secret.nix") | |
| ( | |
| { config, ... }: | |
| { | |
| _module.check = false; | |
| _module.args.pkgs = import nixpkgs { }; | |
| contracts.fileSecrets.want.demoConsumer = { | |
| primary.request = { | |
| owner = "alice"; | |
| group = "users"; | |
| mode = "0400"; | |
| }; | |
| backup.request = { | |
| owner = "bob"; | |
| group = "users"; | |
| mode = "0400"; | |
| }; | |
| }; | |
| testing.hardcoded-secret.fileSecrets.demoConsumer = { | |
| primary.content = "primary-content"; | |
| backup.content = "backup-content"; | |
| }; | |
| contracts.fileSecrets.defaultProviderName = "hardcoded-secret"; | |
| # Per-instance override exercising the unified `instances` | |
| # mechanism: `lib.recursiveUpdate` places a providerRef leaf at | |
| # `demoConsumer.backup`; the option's `apply` resolves it to | |
| # the same provider's instance at that path. | |
| contracts.fileSecrets.instances = lib.recursiveUpdate | |
| config.testing.hardcoded-secret.fileSecrets | |
| { | |
| demoConsumer.backup = config.contracts.fileSecrets.providers.hardcoded-secret; | |
| }; | |
| } | |
| ) | |
| ]; | |
| }; | |
| # Per-provider helpers shared by `default` and `instances` schema gen. | |
| # `contractName` is needed for the `[ contractName ]` inferred default | |
| # when `provider.contract` isn't explicit. | |
| providerSchema = contractName: providerName: provider: | |
| let | |
| moduleOption = provider.module; | |
| moduleSubOptions = moduleOption.type.getSubOptions moduleOption.loc; | |
| contractPath = | |
| if provider.contract or null != null then | |
| provider.contract | |
| else if moduleOption.value ? ${contractName} then | |
| [ contractName ] | |
| else | |
| [ ]; | |
| nonContractOptions = | |
| if contractPath == [ ] then | |
| { } | |
| else | |
| lib.removeAttrs moduleSubOptions [ (lib.head contractPath) ]; | |
| typePrefix = (clanLib.toUpperFirst contractName) + (clanLib.toUpperFirst providerName); | |
| in | |
| { | |
| inherit typePrefix; | |
| schema = | |
| jsonschema.fromOptions | |
| { | |
| inherit typePrefix; | |
| output = false; | |
| readOnly = { | |
| input = false; | |
| output = false; | |
| }; | |
| } | |
| nonContractOptions; | |
| }; | |
| # For each contract type, build: | |
| # default = config schema for the chosen `defaultProvider` (if any). | |
| # instances = a tree mirroring `want`, with each leaf being the chosen | |
| # provider's config schema (single, not a `oneOf`: the | |
| # contract is fixed per `contracts.<type>` and the | |
| # provider is pinned by Nix). Here the chosen provider is | |
| # the `defaultProvider` for every leaf; for instances | |
| # explicitly overridden to a different provider, swap in | |
| # that provider's `$ref` (left as a TODO requiring a | |
| # pre-apply walk of `instances` to detect leaves of shape | |
| # `{ module, contract? }`). | |
| perContract = lib.mapAttrs ( | |
| contractName: contract: | |
| let | |
| providerSchemas = lib.mapAttrs (providerSchema contractName) contract.providers; | |
| defaultProviderEntry = | |
| lib.findFirst | |
| (e: contract.defaultProvider != null && e.value.module == contract.defaultProvider.module) | |
| null | |
| (lib.mapAttrsToList (n: v: { name = n; value = v; }) contract.providers); | |
| defaultSchema = | |
| if defaultProviderEntry == null | |
| then null | |
| else providerSchemas.${defaultProviderEntry.name}; | |
| mergedDefs = lib.foldl' (acc: ps: acc // ps.schema."$defs") { } (lib.attrValues providerSchemas); | |
| typePrefix = clanLib.toUpperFirst contractName; | |
| defaultRef = | |
| if defaultSchema != null | |
| then { "$ref" = "#/$defs/${defaultSchema.typePrefix}Input"; } | |
| else null; | |
| buildInstances = wantTree: | |
| if wantTree ? request then | |
| if defaultRef != null then defaultRef else { type = "null"; } | |
| else | |
| { | |
| type = "object"; | |
| additionalProperties = false; | |
| properties = lib.mapAttrs (_: child: buildInstances child) wantTree; | |
| }; | |
| in | |
| { | |
| inherit typePrefix mergedDefs; | |
| schema = { | |
| type = "object"; | |
| additionalProperties = false; | |
| properties = { | |
| default = if defaultRef != null then defaultRef else { type = "null"; }; | |
| instances = buildInstances contract.want; | |
| }; | |
| }; | |
| } | |
| ) (lib.filterAttrs (_: c: c.providers != { }) evaled.config.contracts); | |
| in | |
| { | |
| schema = { | |
| "$schema" = "https://json-schema.org/draft/2020-12/schema"; | |
| type = "object"; | |
| additionalProperties = false; | |
| properties = lib.mapAttrs (_: pc: pc.schema) perContract; | |
| "$defs" = lib.foldl' (acc: pc: acc // pc.mergedDefs) { } (lib.attrValues perContract); | |
| }; | |
| } | |
| in | |
| (toUpper (substring 0 1 str)) + substring 1 (-1) str; | |
| }; | |
| jsonschema = import clanJsonschema { inherit lib clanLib; }; | |
| # Evaluate the contracts wrapper + the hardcoded-secret provider module. | |
| evaled = lib.evalModules { | |
| class = "nixos"; | |
| modules = [ | |
| (nixpkgs + "/nixos/modules/contracts/default.nix") | |
| (nixpkgs + "/nixos/modules/testing/hardcoded-secret.nix") | |
| { | |
| _module.check = false; | |
| _module.args.pkgs = import nixpkgs { }; | |
| # Pick hardcoded-secret as the default fileSecrets provider so the | |
| # iteration below has something to render. | |
| contracts.fileSecrets.defaultProviderName = "hardcoded-secret"; | |
| } | |
| ]; | |
| }; | |
| # For each contract type with a default provider, derive the schema for | |
| # the provider's *non-contract* options (e.g. `directory` for hardcoded- | |
| # secret). The contract sub-tree is stripped because contract instances | |
| # are rendered separately from request-side data. | |
| perContract = lib.mapAttrs ( | |
| contractName: contract: | |
| let | |
| provider = contract.defaultProvider; | |
| moduleOption = provider.module; | |
| moduleSubOptions = moduleOption.type.getSubOptions moduleOption.loc; | |
| contractPath = provider.contract or [ contractName ]; | |
| nonContractOptions = | |
| if contractPath == [ ] then | |
| { } | |
| else | |
| lib.removeAttrs moduleSubOptions [ (lib.head contractPath) ]; | |
| typePrefix = clanLib.toUpperFirst contractName; | |
| schema = | |
| jsonschema.fromOptions | |
| { | |
| inherit typePrefix; | |
| output = false; | |
| readOnly = { | |
| input = false; | |
| output = false; | |
| }; | |
| } | |
| nonContractOptions; | |
| in | |
| { | |
| inherit typePrefix schema; | |
| } | |
| ) (lib.filterAttrs (_: c: c.defaultProvider != null) evaled.config.contracts); | |
| in | |
| { | |
| schema = { | |
| "$schema" = "https://json-schema.org/draft/2020-12/schema"; | |
| type = "object"; | |
| additionalProperties = false; | |
| properties = lib.mapAttrs (_: pc: { | |
| "$ref" = "#/$defs/${pc.typePrefix}Input"; | |
| }) perContract; | |
| "$defs" = lib.foldl' (acc: pc: acc // pc.schema."$defs") { } (lib.attrValues perContract); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # Demo flake for: deriving a JSON schema for a contract type's default | |
| # provider's non-contract-instance options, by way of the | |
| # `providers.<name>.module` option set introduced on the `contracts-automated` | |
| # branch of nixpkgs. | |
| # | |
| # Usage: | |
| # nix run . # JSON schema covering, per contract type, the | |
| # # `default` and the per-instance provider's config. | |
| { | |
| description = "Contract-provider JSON schema demo"; | |
| inputs = { | |
| nixpkgs.url = "github:kiaragrouwstra/nixpkgs/contracts-automated"; | |
| # `nested-attrs-of` branch adds `nestedAttrsOf` -> `attrsOf` mapping and | |
| # `default` annotation passthrough to clan-core's lib/jsonschema. | |
| clan-core = { | |
| url = "git+https://git.clan.lol/kiara/clan-core.git?ref=nested-attrs-of"; | |
| flake = false; | |
| }; | |
| }; | |
| outputs = | |
| { self, nixpkgs, clan-core }: | |
| let | |
| systems = [ | |
| "x86_64-linux" | |
| "aarch64-linux" | |
| "x86_64-darwin" | |
| "aarch64-darwin" | |
| ]; | |
| forAllSystems = nixpkgs.lib.genAttrs systems; | |
| result = forAllSystems ( | |
| system: | |
| import ./demo.nix { | |
| nixpkgs = nixpkgs.outPath; | |
| clanJsonschema = "${clan-core}/lib/jsonschema"; | |
| } | |
| ); | |
| in | |
| { | |
| packages = forAllSystems ( | |
| system: | |
| let | |
| pkgs = nixpkgs.legacyPackages.${system}; | |
| in | |
| { | |
| default = pkgs.writeShellApplication { | |
| name = "schema"; | |
| runtimeInputs = [ pkgs.jq ]; | |
| text = '' | |
| jq . <<'EOF' | |
| ${builtins.toJSON result.${system}.schema} | |
| EOF | |
| ''; | |
| }; | |
| } | |
| ); | |
| apps = forAllSystems (system: { | |
| default = { | |
| type = "app"; | |
| program = "${self.packages.${system}.default}/bin/schema"; | |
| }; | |
| }); | |
| }; | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| { | |
| "$defs": { | |
| "FileSecretsInput": { | |
| "additionalProperties": false, | |
| "properties": { | |
| "directory": { | |
| "default": "/run/hardcodedsecrets", | |
| "description": "The directory to store the secrets at.", | |
| "type": "string" | |
| } | |
| }, | |
| "type": "object" | |
| } | |
| }, | |
| "$schema": "https://json-schema.org/draft/2020-12/schema", | |
| "additionalProperties": false, | |
| "properties": { | |
| "fileSecrets": { | |
| "$ref": "#/$defs/FileSecretsInput" | |
| } | |
| }, | |
| "type": "object" | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
