Merge pull request 'Json-schema: extend interface by header to allow schema spec and arbitrary extensions' (#1740) from hsjobeki/clan-core:hsjobeki-main into main
All checks were successful
deploy / deploy-docs (push) Successful in 19s
checks / checks-impure (push) Successful in 3m10s
buildbot/nix-build .#checks.aarch64-darwin.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.aarch64-darwin.nixos-flash-installer Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.aarch64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-no-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-apk Build done.
buildbot/nix-build .#checks.x86_64-linux.check-for-breakpoints Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-deb Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-rpm Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-installer-archlinux Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-age Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-bash Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-e2fsprogs Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-git Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-mypy Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-nix Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-openssh Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-qemu Build done.
buildbot/nix-build .#checks.x86_64-linux.borgbackup Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-inventory-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sshpass Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-tor Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-zbar Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-with-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-pytest-without-core Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-app-pytest Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-rsync Build done.
buildbot/nix-build .#checks.x86_64-linux.clan-dep-sops Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-default Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.module-clan-vars-eval Build done.
buildbot/nix-build .#checks.x86_64-linux.deltachat Build done.
buildbot/nix-build .#checks.x86_64-linux.matrix-synapse Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-inventory-examples-cue Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-example-valid Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.container Build done.
buildbot/nix-build .#checks.x86_64-linux.module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-app Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.flash Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-cli-full Build done.
buildbot/nix-build .#checks.x86_64-linux.package-clan-ts-api Build done.
buildbot/nix-build .#checks.x86_64-linux.package-default Build done.
buildbot/nix-build .#checks.x86_64-linux.lib-jsonschema-nix-unit-tests Build done.
buildbot/nix-build .#checks.x86_64-linux.package-editor Build done.
buildbot/nix-build .#checks.x86_64-linux.devShell-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-impure-checks Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test-backup Build done.
buildbot/nix-build .#checks.x86_64-linux.package-merge-after-ci Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-minimal-inventory-machine Build done.
buildbot/nix-build .#checks.x86_64-linux.package-moonlight-sunshine-accept Build done.
buildbot/nix-build .#checks.x86_64-linux.package-pending-reviews Build done.
buildbot/nix-build .#checks.x86_64-linux.package-tea-create-pr Build done.
buildbot/nix-build .#checks.x86_64-linux.package-webview-ui Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotier-members Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zerotierone Build done.
buildbot/nix-build .#checks.x86_64-linux.package-zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-test_install_machine Build done.
buildbot/nix-build .#checks.x86_64-linux.package-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.package-inventory-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.secrets Build done.
buildbot/nix-build .#checks.x86_64-linux.package-deploy-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.nixos-flash-installer Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.treefmt Build done.
buildbot/nix-build .#checks.x86_64-linux.renderClanOptions Build done.
buildbot/nix-build .#checks.x86_64-linux.postgresql Build done.
buildbot/nix-build .#checks.x86_64-linux.package-function-schema Build done.
buildbot/nix-build .#checks.x86_64-linux.package-module-docs Build done.
buildbot/nix-build .#checks.x86_64-linux.test-backups Build done.
buildbot/nix-build .#checks.x86_64-linux.zt-tcp-relay Build done.
buildbot/nix-build .#checks.x86_64-linux.template-minimal Build done.
buildbot/nix-build .#checks.x86_64-linux.wayland-proxy-virtwl Build done.
buildbot/nix-build .#checks.x86_64-linux.syncthing Build done.
buildbot/nix-build .#checks.x86_64-linux.test-installation Build done.
buildbot/nix-build .#checks.x86_64-linux.package-gui-install-test-ubuntu-22-04 Build done.
buildbot/nix-eval Build done.

This commit is contained in:
clan-bot 2024-07-14 13:06:08 +00:00
commit a1c74c4a10
14 changed files with 236 additions and 287 deletions

View File

@ -15,7 +15,7 @@ let
}; };
machineRef = lib.mkOptionType { machineRef = lib.mkOptionType {
name = "machineRef"; name = "str";
description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]"; description = "Machine :: [${builtins.concatStringsSep " | " (builtins.attrNames config.machines)}]";
check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines); check = v: lib.isString v && builtins.elem v (builtins.attrNames config.machines);
merge = lib.mergeEqualOption; merge = lib.mergeEqualOption;
@ -29,20 +29,85 @@ let
); );
tagRef = lib.mkOptionType { tagRef = lib.mkOptionType {
name = "tagRef"; name = "str";
description = "Tags :: [${builtins.concatStringsSep " | " allTags}]"; description = "Tags :: [${builtins.concatStringsSep " | " allTags}]";
check = v: lib.isString v && builtins.elem v allTags; check = v: lib.isString v && builtins.elem v allTags;
merge = lib.mergeEqualOption; merge = lib.mergeEqualOption;
}; };
moduleConfig = lib.mkOption {
default = { };
type = t.attrsOf t.anything;
};
in in
{ {
options.assertions = lib.mkOption { options = {
type = t.listOf t.unspecified; assertions = lib.mkOption {
internal = true; type = t.listOf t.unspecified;
default = [ ]; internal = true;
visible = false;
default = [ ];
};
meta = metaOptions;
machines = lib.mkOption {
default = { };
type = t.attrsOf (
t.submodule {
options = {
inherit (metaOptions) name description icon;
tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = t.listOf t.str;
};
system = lib.mkOption {
default = null;
type = t.nullOr t.str;
};
};
}
);
};
services = lib.mkOption {
default = { };
type = t.attrsOf (
t.attrsOf (
t.submodule {
options.meta = metaOptions;
options.config = moduleConfig;
options.machines = lib.mkOption {
default = { };
type = t.attrsOf (t.submodule { options.config = moduleConfig; });
};
options.roles = lib.mkOption {
default = { };
type = t.attrsOf (
t.submodule {
options.machines = lib.mkOption {
default = [ ];
type = t.listOf machineRef;
};
options.tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = t.listOf tagRef;
};
}
);
};
}
)
);
};
}; };
# Smoke validation of the inventory
config.assertions = config.assertions =
let let
# Inventory assertions
# - All referenced machines must exist in the top-level machines
serviceAssertions = lib.foldlAttrs ( serviceAssertions = lib.foldlAttrs (
ass1: serviceName: c: ass1: serviceName: c:
ass1 ass1
@ -60,8 +125,11 @@ in
ass2 ++ assertions ass2 ++ assertions
) [ ] c ) [ ] c
) [ ] config.services; ) [ ] config.services;
# Machine assertions
# - A machine must define their host system
machineAssertions = map ( machineAssertions = map (
{ name, value }: { name, ... }:
{ {
assertion = true; assertion = true;
message = "Machine ${name} should define its host system in the inventory. ()"; message = "Machine ${name} should define its host system in the inventory. ()";
@ -69,68 +137,4 @@ in
) (lib.attrsToList (lib.filterAttrs (_n: v: v.system or null == null) config.machines)); ) (lib.attrsToList (lib.filterAttrs (_n: v: v.system or null == null) config.machines));
in in
machineAssertions ++ serviceAssertions; machineAssertions ++ serviceAssertions;
options.meta = metaOptions;
options.machines = lib.mkOption {
default = { };
type = t.attrsOf (
t.submodule {
options = {
inherit (metaOptions) name description icon;
tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = t.listOf t.str;
};
system = lib.mkOption {
default = null;
type = t.nullOr t.str;
};
};
}
);
};
options.services = lib.mkOption {
default = { };
type = t.attrsOf (
t.attrsOf (
t.submodule {
options.meta = metaOptions;
options.config = lib.mkOption {
default = { };
type = t.anything;
};
options.machines = lib.mkOption {
default = { };
type = t.attrsOf (
t.submodule {
options.config = lib.mkOption {
default = { };
type = t.anything;
};
}
);
};
options.roles = lib.mkOption {
default = { };
type = t.attrsOf (
t.submodule {
options.machines = lib.mkOption {
default = [ ];
type = t.listOf machineRef;
};
options.tags = lib.mkOption {
default = [ ];
apply = lib.unique;
type = t.listOf tagRef;
};
}
);
};
}
)
);
};
} }

View File

@ -1,47 +0,0 @@
{
"machines": {
"camina_machine": {
"name": "camina"
},
"vyr_machine": {
"name": "vyr"
},
"vi_machine": {
"name": "vi"
}
},
"meta": {
"name": "kenjis clan"
},
"services": {
"syncthing": {
"instance_1": {
"meta": {
"name": "My sync"
},
"roles": {
"peer": {
"machines": ["vyr_machine", "vi_machine", "camina_machine"]
}
},
"machines": {},
"config": {
"folders": {
"test": {
"path": "~/data/docs",
"devices": ["camina_machine", "vyr_machine", "vi_machine"]
},
"videos": {
"path": "~/data/videos",
"devices": ["camina_machine", "vyr_machine"]
},
"playlist": {
"path": "~/data/playlist",
"devices": ["camina_machine", "vi_machine"]
}
}
}
}
}
}
}

View File

@ -1,36 +0,0 @@
{
"machines": {
"camina_machine": {
"name": "camina"
},
"vyr_machine": {
"name": "vyr"
},
"vi_machine": {
"name": "vi"
}
},
"meta": {
"name": "kenjis clan"
},
"services": {
"zerotier": {
"instance_1": {
"meta": {
"name": "My Network"
},
"roles": {
"controller": { "machines": ["vyr_machine"] },
"moon": { "machines": ["vyr_machine"] },
"peer": { "machines": ["vi_machine", "camina_machine"] }
},
"machines": {
"vyr_machine": {
"config": {}
}
},
"config": {}
}
}
}
}

View File

@ -21,16 +21,112 @@ in
clan-core = self; clan-core = self;
inherit lib; inherit lib;
}; };
optionsFromModule =
mName:
let
eval = self.lib.evalClanModules [ mName ];
in
if (eval.options.clan ? "${mName}") then eval.options.clan.${mName} else { };
modulesSchema = lib.mapAttrs (
moduleName: _: jsonLib'.parseOptions (optionsFromModule moduleName) { }
) self.clanModules;
jsonLib = self.lib.jsonschema {
# includeDefaults = false;
};
jsonLib' = self.lib.jsonschema {
# includeDefaults = false;
header = { };
};
inventorySchema = jsonLib.parseModule (import ./build-inventory/interface.nix);
getRoles =
modulePath:
let
rolesDir = "${modulePath}/roles";
in
if builtins.pathExists rolesDir then
lib.pipe rolesDir [
builtins.readDir
(lib.filterAttrs (_n: v: v == "regular"))
lib.attrNames
(map (fileName: lib.removeSuffix ".nix" fileName))
]
else
null;
schema = inventorySchema // {
properties = inventorySchema.properties // {
services = {
type = "object";
additionalProperties = false;
properties = lib.mapAttrs (moduleName: moduleSchema: {
type = "object";
additionalProperties = {
type = "object";
additionalProperties = false;
properties = {
meta =
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.meta;
config = moduleSchema;
roles = {
type = "object";
additionalProperties = false;
required = [ ];
properties = lib.listToAttrs (
map
(role: {
name = role;
value =
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.roles.additionalProperties;
})
(
let
roles = getRoles self.clanModules.${moduleName};
in
if roles == null then [ ] else roles
)
);
};
machines =
lib.recursiveUpdate
inventorySchema.properties.services.additionalProperties.additionalProperties.properties.machines
{ additionalProperties.properties.config = moduleSchema; };
};
};
}) modulesSchema;
};
};
};
in in
{ {
legacyPackages.inventorySchema = schema;
devShells.inventory-schema = pkgs.mkShell { devShells.inventory-schema = pkgs.mkShell {
inputsFrom = with config.checks; [ inputsFrom = with config.checks; [
lib-inventory-schema lib-inventory-examples-cue
lib-inventory-eval lib-inventory-eval
self'.devShells.default self'.devShells.default
]; ];
}; };
# Inventory schema with concrete module implementations
packages.inventory-schema = pkgs.stdenv.mkDerivation {
name = "inventory-schema";
buildInputs = [ pkgs.cue ];
src = ./.;
buildPhase = ''
export SCHEMA=${builtins.toFile "inventory-schema.json" (builtins.toJSON self'.legacyPackages.inventorySchema)}
cp $SCHEMA schema.json
cue import -f -p compose -l '#Root:' schema.json
mkdir $out
cp schema.cue $out
cp schema.json $out
'';
};
# Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests # Run: nix-unit --extra-experimental-features flakes --flake .#legacyPackages.x86_64-linux.evalTests
legacyPackages.evalTests-inventory = import ./tests { legacyPackages.evalTests-inventory = import ./tests {
inherit buildInventory; inherit buildInventory;
@ -38,32 +134,21 @@ in
}; };
checks = { checks = {
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } '' lib-inventory-examples-cue = pkgs.stdenv.mkDerivation {
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
${inputOverrides} \
--flake ${self}#legacyPackages.${system}.evalTests-inventory
touch $out
'';
lib-inventory-schema = pkgs.stdenv.mkDerivation {
name = "inventory-schema-checks"; name = "inventory-schema-checks";
src = ./.; src = ./.;
buildInputs = [ pkgs.cue ]; buildInputs = [ pkgs.cue ];
buildPhase = '' buildPhase = ''
echo "Running inventory tests..." echo "Running inventory tests..."
# Cue is easier to run in the same directory as the schema # Cue is easier to run in the same directory as the schema
cd spec cp ${self'.packages.inventory-schema}/schema.cue root.cue
echo "Export cue as json-schema..." ls -la .
cue export --out openapi root.cue
echo "Validate test/*.json against inventory-schema..." echo "Validate test/*.json against inventory-schema..."
cat root.cue
test_dir="../examples" test_dir="./examples"
for file in "$test_dir"/*; do for file in "$test_dir"/*; do
# Check if the item is a file # Check if the item is a file
if [ -f "$file" ]; then if [ -f "$file" ]; then
@ -78,6 +163,16 @@ in
touch $out touch $out
''; '';
}; };
lib-inventory-eval = pkgs.runCommand "tests" { nativeBuildInputs = [ pkgs.nix-unit ]; } ''
export HOME="$(realpath .)"
nix-unit --eval-store "$HOME" \
--extra-experimental-features flakes \
${inputOverrides} \
--flake ${self}#legacyPackages.${system}.evalTests-inventory
touch $out
'';
}; };
}; };
} }

View File

@ -1,2 +0,0 @@
module: "clan.lol/inventory"
language: version: "v0.8.2"

View File

@ -1,23 +0,0 @@
package inventory
import (
"clan.lol/inventory/schema"
)
@jsonschema(schema="http://json-schema.org/schema#")
#Root: {
meta: {
// A name of the clan (primarily shown by the UI)
name: string
// A description of the clan
description?: string
// The icon path
icon?: string
}
// // A map of services
schema.#service
// // A map of machines
schema.#machine
}

View File

@ -1,40 +0,0 @@
package schema
#machine: machines: [string]: {
name: string,
description?: string,
icon?: string
tags: [...string]
system?: string
}
#role: string
#service: services: [string]: [string]: {
// Required meta fields
meta: {
name: string,
icon?: string
description?: string,
},
// We moved the machine sepcific config to "machines".
// It may be moved back depending on what makes more sense in the future.
roles: [#role]: {
machines: [...string],
tags: [...string],
}
machines?: {
[string]: {
config?: {
...
}
}
},
// Global Configuration for the service
config?: {
// Schema depends on the module.
// It declares the interface how the service can be configured.
...
}
}

View File

@ -1,9 +1,16 @@
{ {
lib ? import <nixpkgs/lib>, lib ? import <nixpkgs/lib>,
}:
{
excludedTypes ? [ excludedTypes ? [
"functionTo" "functionTo"
"package" "package"
], ],
includeDefaults ? true,
header ? {
"$schema" = "http://json-schema.org/draft-07/schema#";
},
specialArgs ? { },
}: }:
let let
# remove _module attribute from options # remove _module attribute from options
@ -40,29 +47,41 @@ let
]; ];
in in
rec { rec {
# parses a nixos module to a jsonschema # parses a nixos module to a jsonschema
parseModule = parseModule =
module: module:
let let
evaled = lib.evalModules { modules = [ module ]; }; evaled = lib.evalModules {
modules = [ module ];
inherit specialArgs;
};
in in
{ "$schema" = "http://json-schema.org/draft-07/schema#"; } // parseOptions evaled.options; parseOptions evaled.options { };
parseOptions' = lib.flip parseOptions { addHeader = false; };
# parses a set of evaluated nixos options to a jsonschema # parses a set of evaluated nixos options to a jsonschema
parseOptions = parseOptions =
options': options:
{
# The top-level header object should specify at least the schema version
# Can be customized if needed
# By default the header is not added to the schema
addHeader ? true,
}:
let let
options = filterInvisibleOpts (filterExcludedAttrs (clean options')); options' = filterInvisibleOpts (filterExcludedAttrs (clean options));
# parse options to jsonschema properties # parse options to jsonschema properties
properties = lib.mapAttrs (_name: option: parseOption option) options; properties = lib.mapAttrs (_name: option: parseOption option) options';
# TODO: figure out how to handle if prop.anyOf is used # TODO: figure out how to handle if prop.anyOf is used
isRequired = prop: !(prop ? default || prop.type or null == "object"); isRequired = prop: !(prop ? default || prop.type or null == "object");
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; };
header' = if addHeader then header else { };
in in
# return jsonschema # return jsonschema
required header'
// required
// { // {
type = "object"; type = "object";
inherit properties; inherit properties;
@ -73,7 +92,7 @@ rec {
parseOption = parseOption =
option: option:
let let
default = lib.optionalAttrs (option ? default) { inherit (option) default; }; default = lib.optionalAttrs (option ? default && includeDefaults) { inherit (option) default; };
example = lib.optionalAttrs (option ? example) { example = lib.optionalAttrs (option ? example) {
examples = examples =
if (builtins.typeOf option.example) == "list" then option.example else [ option.example ]; if (builtins.typeOf option.example) == "list" then option.example else [ option.example ];
@ -82,7 +101,6 @@ rec {
description = option.description.text or option.description; description = option.description.text or option.description;
}; };
in in
# either type # either type
# TODO: if all nested options are excluded, the parent should be excluded too # TODO: if all nested options are excluded, the parent should be excluded too
if if
@ -104,16 +122,13 @@ rec {
]; ];
optionsList = filterExcluded optionsList'; optionsList = filterExcluded optionsList';
in in
default // example // description // { anyOf = map parseOption optionsList; } default // example // description // { oneOf = map parseOption optionsList; }
# handle nested options (not a submodule) # handle nested options (not a submodule)
else if !option ? _type then else if !option ? _type then
parseOptions option parseOptions' option
# throw if not an option # throw if not an option
else if option._type != "option" && option._type != "option-type" then else if option._type != "option" && option._type != "option-type" then
throw "parseOption: not an option" throw "parseOption: not an option"
# parse nullOr # parse nullOr
else if else if
option.type.name == "nullOr" option.type.name == "nullOr"
@ -130,32 +145,28 @@ rec {
// example // example
// description // description
// { // {
anyOf = [ oneOf = [
{ type = "null"; } { type = "null"; }
] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption)); ] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption));
} }
# parse bool # parse bool
else if else if
option.type.name == "bool" option.type.name == "bool"
# return jsonschema property definition for bool # return jsonschema property definition for bool
then then
default // example // description // { type = "boolean"; } default // example // description // { type = "boolean"; }
# parse float # parse float
else if else if
option.type.name == "float" option.type.name == "float"
# return jsonschema property definition for float # return jsonschema property definition for float
then then
default // example // description // { type = "number"; } default // example // description // { type = "number"; }
# parse int # parse int
else if else if
(option.type.name == "int" || option.type.name == "positiveInt") (option.type.name == "int" || option.type.name == "positiveInt")
# return jsonschema property definition for int # return jsonschema property definition for int
then then
default // example // description // { type = "integer"; } default // example // description // { type = "integer"; }
# TODO: Add support for intMatching in jsonschema # TODO: Add support for intMatching in jsonschema
# parse port type aka. "unsignedInt16" # parse port type aka. "unsignedInt16"
else if else if
@ -165,7 +176,6 @@ rec {
|| option.type.name == "intBetween" || option.type.name == "intBetween"
then then
default // example // description // { type = "integer"; } default // example // description // { type = "integer"; }
# parse string # parse string
# TODO: parse more precise string types # TODO: parse more precise string types
else if else if
@ -176,51 +186,43 @@ rec {
# return jsonschema property definition for string # return jsonschema property definition for string
then then
default // example // description // { type = "string"; } default // example // description // { type = "string"; }
# TODO: Add support for stringMatching in jsonschema # TODO: Add support for stringMatching in jsonschema
# parse stringMatching # parse stringMatching
else if lib.strings.hasPrefix "strMatching" option.type.name then else if lib.strings.hasPrefix "strMatching" option.type.name then
default // example // description // { type = "string"; } default // example // description // { type = "string"; }
# TODO: Add support for separatedString in jsonschema # TODO: Add support for separatedString in jsonschema
else if lib.strings.hasPrefix "separatedString" option.type.name then else if lib.strings.hasPrefix "separatedString" option.type.name then
default // example // description // { type = "string"; } default // example // description // { type = "string"; }
# parse string # parse string
else if else if
option.type.name == "path" option.type.name == "path"
# return jsonschema property definition for path # return jsonschema property definition for path
then then
default // example // description // { type = "string"; } default // example // description // { type = "string"; }
# parse anything # parse anything
else if else if
option.type.name == "anything" option.type.name == "anything"
# return jsonschema property definition for anything # return jsonschema property definition for anything
then then
default // example // description // { type = allBasicTypes; } default // example // description // { type = allBasicTypes; }
# parse unspecified # parse unspecified
else if else if
option.type.name == "unspecified" option.type.name == "unspecified"
# return jsonschema property definition for unspecified # return jsonschema property definition for unspecified
then then
default // example // description // { type = allBasicTypes; } default // example // description // { type = allBasicTypes; }
# parse raw # parse raw
else if else if
option.type.name == "raw" option.type.name == "raw"
# return jsonschema property definition for raw # return jsonschema property definition for raw
then then
default // example // description // { type = allBasicTypes; } default // example // description // { type = allBasicTypes; }
# parse enum # parse enum
else if else if
option.type.name == "enum" option.type.name == "enum"
# return jsonschema property definition for enum # return jsonschema property definition for enum
then then
default // example // description // { enum = option.type.functor.payload; } default // example // description // { enum = option.type.functor.payload; }
# parse listOf submodule # parse listOf submodule
else if else if
option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule" option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule"
@ -231,9 +233,8 @@ rec {
// description // description
// { // {
type = "array"; type = "array";
items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc); items = parseOptions' (option.type.functor.wrapped.getSubOptions option.loc);
} }
# parse list # parse list
else if else if
(option.type.name == "listOf") (option.type.name == "listOf")
@ -253,14 +254,12 @@ rec {
type = "array"; type = "array";
} }
// (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; }) // (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; })
# parse list of unspecified # parse list of unspecified
else if else if
(option.type.name == "listOf") && (option.type.functor.wrapped.name == "unspecified") (option.type.name == "listOf") && (option.type.functor.wrapped.name == "unspecified")
# return jsonschema property definition for list # return jsonschema property definition for list
then then
default // example // description // { type = "array"; } default // example // description // { type = "array"; }
# parse attrsOf submodule # parse attrsOf submodule
else if else if
option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule" option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule"
@ -271,9 +270,8 @@ rec {
// description // description
// { // {
type = "object"; type = "object";
additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc); additionalProperties = parseOptions' (option.type.nestedTypes.elemType.getSubOptions option.loc);
} }
# parse attrs # parse attrs
else if else if
option.type.name == "attrs" option.type.name == "attrs"
@ -286,7 +284,6 @@ rec {
type = "object"; type = "object";
additionalProperties = true; additionalProperties = true;
} }
# parse attrsOf # parse attrsOf
# TODO: if nested option is excluded, the parent sould be excluded too # TODO: if nested option is excluded, the parent sould be excluded too
else if else if
@ -315,15 +312,13 @@ rec {
else else
false; false;
} }
# parse submodule # parse submodule
else if else if
option.type.name == "submodule" option.type.name == "submodule"
# return jsonschema property definition for submodule # return jsonschema property definition for submodule
# then (lib.attrNames (option.type.getSubOptions option.loc).opt) # then (lib.attrNames (option.type.getSubOptions option.loc).opt)
then then
parseOptions (option.type.getSubOptions option.loc) parseOptions' (option.type.getSubOptions option.loc)
# throw error if option type is not supported # throw error if option type is not supported
else else
notSupported option; notSupported option;

View File

@ -1,7 +1,7 @@
# run these tests via `nix-unit ./test.nix` # run these tests via `nix-unit ./test.nix`
{ {
lib ? (import <nixpkgs> { }).lib, lib ? (import <nixpkgs> { }).lib,
slib ? import ./. { inherit lib; }, slib ? (import ./. { inherit lib; } { }),
}: }:
{ {
parseOption = import ./test_parseOption.nix { inherit lib slib; }; parseOption = import ./test_parseOption.nix { inherit lib slib; };

View File

@ -2,7 +2,7 @@
# run these tests via `nix-unit ./test.nix` # run these tests via `nix-unit ./test.nix`
{ {
lib ? (import <nixpkgs> { }).lib, lib ? (import <nixpkgs> { }).lib,
slib ? import ./. { inherit lib; }, slib ? (import ./. { inherit lib; } { }),
}: }:
let let
description = "Test Description"; description = "Test Description";
@ -236,7 +236,7 @@ in
{ {
expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default); expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default);
expected = { expected = {
anyOf = [ oneOf = [
{ type = "null"; } { type = "null"; }
{ type = "boolean"; } { type = "boolean"; }
]; ];
@ -251,10 +251,10 @@ in
{ {
expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default); expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default);
expected = { expected = {
anyOf = [ oneOf = [
{ type = "null"; } { type = "null"; }
{ {
anyOf = [ oneOf = [
{ type = "null"; } { type = "null"; }
{ type = "boolean"; } { type = "boolean"; }
]; ];
@ -386,7 +386,7 @@ in
{ {
expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default); expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default);
expected = { expected = {
anyOf = [ oneOf = [
{ type = "boolean"; } { type = "boolean"; }
{ type = "string"; } { type = "string"; }
]; ];

View File

@ -2,7 +2,7 @@
# run these tests via `nix-unit ./test.nix` # run these tests via `nix-unit ./test.nix`
{ {
lib ? (import <nixpkgs> { }).lib, lib ? (import <nixpkgs> { }).lib,
slib ? import ./. { inherit lib; }, slib ? (import ./. { inherit lib; } { }),
}: }:
{ {
testParseOptions = { testParseOptions = {
@ -17,8 +17,9 @@
}; };
in in
{ {
expr = slib.parseOptions evaled.options; expr = slib.parseOptions evaled.options { };
expected = { expected = {
"$schema" = "http://json-schema.org/draft-07/schema#";
additionalProperties = false; additionalProperties = false;
properties = { properties = {
foo = { foo = {

View File

@ -1,6 +1,6 @@
{ options, lib, ... }: { options, lib, ... }:
let let
jsonschema = import ../../lib/jsonschema { inherit lib; }; jsonschema = import ../../lib/jsonschema { inherit lib; } { };
in in
{ {
options.clanSchema = lib.mkOption { options.clanSchema = lib.mkOption {

View File

@ -94,8 +94,8 @@ def machine_schema(
++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []); ++ (map (name: clan-core.clanModules.${{name}}) config.clanImports or []);
}}; }};
options = fakeMachine.options{"." + ".".join(option_path) if option_path else ""}; options = fakeMachine.options{"." + ".".join(option_path) if option_path else ""};
jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }}; jsonschemaLib = import {Path(__file__).parent / "jsonschema"} {{ inherit lib; }} {{}};
jsonschema = jsonschemaLib.parseOptions options; jsonschema = jsonschemaLib.parseOptions options {{}};
in in
jsonschema jsonschema
""", """,

View File

@ -5,6 +5,8 @@
let let
clanModules = self.clanModules; clanModules = self.clanModules;
jsonLib = self.lib.jsonschema { };
# Uncomment if you only want one module to be available # Uncomment if you only want one module to be available
# clanModules = { # clanModules = {
# borgbackup = self.clanModules.borgbackup; # borgbackup = self.clanModules.borgbackup;
@ -18,13 +20,13 @@
if (eval.options.clan ? "${mName}") then eval.options.clan.${mName} else { }; if (eval.options.clan ? "${mName}") then eval.options.clan.${mName} else { };
clanModuleSchemas = lib.mapAttrs ( clanModuleSchemas = lib.mapAttrs (
modulename: _: self.lib.jsonschema.parseOptions (optionsFromModule modulename) modulename: _: jsonLib.parseOptions (optionsFromModule modulename) { }
) clanModules; ) clanModules;
clanModuleFunctionSchemas = lib.mapAttrsFlatten (modulename: _: { clanModuleFunctionSchemas = lib.mapAttrsFlatten (modulename: _: {
name = modulename; name = modulename;
description = self.lib.modules.getShortDescription modulename; description = self.lib.modules.getShortDescription modulename;
parameters = self.lib.jsonschema.parseOptions (optionsFromModule modulename); parameters = jsonLib.parseOptions (optionsFromModule modulename) { };
}) clanModules; }) clanModules;
in in
rec { rec {