Skip to content

Instantly share code, notes, and snippets.

@KiaraGrouwstra
Last active April 28, 2026 15:36
Show Gist options
  • Select an option

  • Save KiaraGrouwstra/93e819314c5902b7403530428dd74164 to your computer and use it in GitHub Desktop.

Select an option

Save KiaraGrouwstra/93e819314c5902b7403530428dd74164 to your computer and use it in GitHub Desktop.
contracts web form
# 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);
};
}
# 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";
};
});
};
}
{
"$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