clanLib.jsonschema: move tests from pkgs/clan-cli

This commit is contained in:
DavHau
2023-08-09 16:05:16 +02:00
parent 9438a9eb5f
commit 7262208a4c
12 changed files with 38 additions and 28 deletions

153
lib/jsonschema/default.nix Normal file
View File

@@ -0,0 +1,153 @@
{ lib ? (import <nixpkgs> { }).lib }:
let
# from nixos type to jsonschema type
typeMap = {
bool = "boolean";
float = "number";
int = "integer";
str = "string";
};
# remove _module attribute from options
clean = opts: builtins.removeAttrs opts [ "_module" ];
# throw error if option type is not supported
notSupported = option: throw
"option type '${option.type.description}' not supported by jsonschema converter";
in
rec {
# parses a nixos module to a jsonschema
parseModule = module:
let
evaled = lib.evalModules {
modules = [ module ];
};
in
parseOptions evaled.options;
# parses a set of evaluated nixos options to a jsonschema
parseOptions = options':
let
options = clean options';
# parse options to jsonschema properties
properties = lib.mapAttrs (_name: option: parseOption option) options;
isRequired = prop: ! (prop ? default || prop.type == "object");
requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties;
required = lib.optionalAttrs (requiredProps != { }) {
required = lib.attrNames requiredProps;
};
in
# return jsonschema
required // {
type = "object";
inherit properties;
};
# parses and evaluated nixos option to a jsonschema property definition
parseOption = option:
let
default = lib.optionalAttrs (option ? default) {
inherit (option) default;
};
description = lib.optionalAttrs (option ? description) {
inherit (option) description;
};
in
if option._type != "option"
then throw "parseOption: not an option"
# parse nullOr
else if option.type.name == "nullOr"
# return jsonschema property definition for nullOr
then default // description // {
type = [
"null"
(typeMap.${option.type.functor.wrapped.name} or (notSupported option))
];
}
# parse bool
else if option.type.name == "bool"
# return jsonschema property definition for bool
then default // description // {
type = "boolean";
}
# parse float
else if option.type.name == "float"
# return jsonschema property definition for float
then default // description // {
type = "number";
}
# parse int
else if option.type.name == "int"
# return jsonschema property definition for int
then default // description // {
type = "integer";
}
# parse string
else if option.type.name == "str"
# return jsonschema property definition for string
then default // description // {
type = "string";
}
# parse enum
else if option.type.name == "enum"
# return jsonschema property definition for enum
then default // description // {
enum = option.type.functor.payload;
}
# parse listOf submodule
else if option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule"
# return jsonschema property definition for listOf submodule
then default // description // {
type = "array";
items = parseOptions (option.type.functor.wrapped.getSubOptions option.loc);
}
# parse list
else if
(option.type.name == "listOf")
&& (typeMap ? "${option.type.functor.wrapped.name}")
# return jsonschema property definition for list
then default // description // {
type = "array";
items = {
type = typeMap.${option.type.functor.wrapped.name};
};
}
# parse attrsOf submodule
else if option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule"
# return jsonschema property definition for attrsOf submodule
then default // description // {
type = "object";
additionalProperties = parseOptions (option.type.nestedTypes.elemType.getSubOptions option.loc);
}
# parse attrs
else if option.type.name == "attrsOf"
# return jsonschema property definition for attrs
then default // description // {
type = "object";
additionalProperties = {
type = typeMap.${option.type.nestedTypes.elemType.name} or (notSupported option);
};
}
# parse submodule
else if option.type.name == "submodule"
# return jsonschema property definition for submodule
# then (lib.attrNames (option.type.getSubOptions option.loc).opt)
then parseOptions (option.type.getSubOptions option.loc)
# throw error if option type is not supported
else notSupported option;
}

View File

@@ -0,0 +1,17 @@
{
"name": "John Doe",
"age": 42,
"isAdmin": false,
"kernelModules": [
"usbhid",
"usb_storage"
],
"userIds": {
"mic92": 1,
"lassulus": 2,
"davhau": 3
},
"services": {
"opt": "this option doesn't make sense"
}
}

View File

@@ -0,0 +1,46 @@
/*
An example nixos module declaring an interface.
*/
{ lib, ... }: {
options = {
name = lib.mkOption {
type = lib.types.str;
default = "John Doe";
description = "The name of the user";
};
age = lib.mkOption {
type = lib.types.int;
default = 42;
description = "The age of the user";
};
isAdmin = lib.mkOption {
type = lib.types.bool;
default = false;
description = "Is the user an admin?";
};
# a submodule option
services = lib.mkOption {
type = lib.types.submodule {
options.opt = lib.mkOption {
type = lib.types.str;
default = "foo";
description = "A submodule option";
};
};
};
userIds = lib.mkOption {
type = lib.types.attrsOf lib.types.int;
description = "Some attributes";
default = {
horst = 1;
peter = 2;
albrecht = 3;
};
};
kernelModules = lib.mkOption {
type = lib.types.listOf lib.types.str;
default = [ "nvme" "xhci_pci" "ahci" ];
description = "A list of enabled kernel modules";
};
};
}

View File

@@ -0,0 +1,54 @@
{
"type": "object",
"properties": {
"name": {
"type": "string",
"default": "John Doe",
"description": "The name of the user"
},
"age": {
"type": "integer",
"default": 42,
"description": "The age of the user"
},
"isAdmin": {
"type": "boolean",
"default": false,
"description": "Is the user an admin?"
},
"kernelModules": {
"type": "array",
"items": {
"type": "string"
},
"default": [
"nvme",
"xhci_pci",
"ahci"
],
"description": "A list of enabled kernel modules"
},
"userIds": {
"type": "object",
"default": {
"horst": 1,
"peter": 2,
"albrecht": 3
},
"additionalProperties": {
"type": "integer"
},
"description": "Some attributes"
},
"services": {
"type": "object",
"properties": {
"opt": {
"type": "string",
"default": "foo",
"description": "A submodule option"
}
}
}
}
}

View File

@@ -0,0 +1,29 @@
{
perSystem = { pkgs, self', ... }: {
checks = {
# check if the `clan config` example jsonschema and data is valid
lib-jsonschema-example-valid = pkgs.runCommand "lib-jsonschema-example-valid" { } ''
echo "Checking that example-schema.json is valid"
${pkgs.check-jsonschema}/bin/check-jsonschema \
--check-metaschema ${./.}/example-schema.json
echo "Checking that example-data.json is valid according to example-schema.json"
${pkgs.check-jsonschema}/bin/check-jsonschema \
--schemafile ${./.}/example-schema.json \
${./.}/example-data.json
touch $out
'';
# check if the `clan config` nix jsonschema converter unit tests succeed
lib-jsonschema-nix-unit-tests = pkgs.runCommand "lib-jsonschema-nix-unit-tests" { } ''
export NIX_PATH=nixpkgs=${pkgs.path}
${self'.packages.nix-unit}/bin/nix-unit \
${./.}/test.nix \
--eval-store $(realpath .)
touch $out
'';
};
};
}

8
lib/jsonschema/test.nix Normal file
View File

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

View File

@@ -0,0 +1,249 @@
# tests for the nixos options to jsonschema converter
# run these tests via `nix-unit ./test.nix`
{ lib ? (import <nixpkgs> { }).lib
, slib ? import ./. { inherit lib; }
}:
let
description = "Test Description";
evalType = type: default:
let
evaledConfig = lib.evalModules {
modules = [{
options.opt = lib.mkOption {
inherit type;
inherit default;
inherit description;
};
}];
};
in
evaledConfig.options.opt;
in
{
testNoDefaultNoDescription =
let
evaledConfig = lib.evalModules {
modules = [{
options.opt = lib.mkOption {
type = lib.types.bool;
};
}];
};
in
{
expr = slib.parseOption evaledConfig.options.opt;
expected = {
type = "boolean";
};
};
testBool =
let
default = false;
in
{
expr = slib.parseOption (evalType lib.types.bool default);
expected = {
type = "boolean";
inherit default description;
};
};
testString =
let
default = "hello";
in
{
expr = slib.parseOption (evalType lib.types.str default);
expected = {
type = "string";
inherit default description;
};
};
testInteger =
let
default = 42;
in
{
expr = slib.parseOption (evalType lib.types.int default);
expected = {
type = "integer";
inherit default description;
};
};
testFloat =
let
default = 42.42;
in
{
expr = slib.parseOption (evalType lib.types.float default);
expected = {
type = "number";
inherit default description;
};
};
testEnum =
let
default = "foo";
values = [ "foo" "bar" "baz" ];
in
{
expr = slib.parseOption (evalType (lib.types.enum values) default);
expected = {
enum = values;
inherit default description;
};
};
testListOfInt =
let
default = [ 1 2 3 ];
in
{
expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default);
expected = {
type = "array";
items = {
type = "integer";
};
inherit default description;
};
};
testAttrsOfInt =
let
default = { foo = 1; bar = 2; baz = 3; };
in
{
expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default);
expected = {
type = "object";
additionalProperties = {
type = "integer";
};
inherit default description;
};
};
testNullOrBool =
let
default = null; # null is a valid value for this type
in
{
expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default);
expected = {
type = [ "null" "boolean" ];
inherit default description;
};
};
testSubmoduleOption =
let
subModule = {
options.opt = lib.mkOption {
type = lib.types.bool;
default = true;
inherit description;
};
};
in
{
expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
expected = {
type = "object";
properties = {
opt = {
type = "boolean";
default = true;
inherit description;
};
};
};
};
testSubmoduleOptionWithoutDefault =
let
subModule = {
options.opt = lib.mkOption {
type = lib.types.bool;
inherit description;
};
};
in
{
expr = slib.parseOption (evalType (lib.types.submodule subModule) { });
expected = {
type = "object";
properties = {
opt = {
type = "boolean";
inherit description;
};
};
required = [ "opt" ];
};
};
testAttrsOfSubmodule =
let
subModule = {
options.opt = lib.mkOption {
type = lib.types.bool;
default = true;
inherit description;
};
};
default = { foo.opt = false; bar.opt = true; };
in
{
expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default);
expected = {
type = "object";
additionalProperties = {
type = "object";
properties = {
opt = {
type = "boolean";
default = true;
inherit description;
};
};
};
inherit default description;
};
};
testListOfSubmodule =
let
subModule = {
options.opt = lib.mkOption {
type = lib.types.bool;
default = true;
inherit description;
};
};
default = [{ opt = false; } { opt = true; }];
in
{
expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default);
expected = {
type = "array";
items = {
type = "object";
properties = {
opt = {
type = "boolean";
default = true;
inherit description;
};
};
};
inherit default description;
};
};
}

View File

@@ -0,0 +1,20 @@
# tests for the nixos options to jsonschema converter
# run these tests via `nix-unit ./test.nix`
{ lib ? (import <nixpkgs> { }).lib
, slib ? import ./. { inherit lib; }
}:
let
evaledOptions =
let
evaledConfig = lib.evalModules {
modules = [ ./example-interface.nix ];
};
in
evaledConfig.options;
in
{
testParseOptions = {
expr = slib.parseOptions evaledOptions;
expected = builtins.fromJSON (builtins.readFile ./example-schema.json);
};
}