clan-config: init

- nixos-modules to jsonschema converter
- nix unit testing via adisbladis/nix-unit
- clan config: configuration CLI for nixos-modules
This commit is contained in:
DavHau
2023-08-02 20:04:16 +02:00
parent 5268ecb595
commit b88ac7a2bf
12 changed files with 668 additions and 26 deletions

View File

@@ -0,0 +1,99 @@
# !/usr/bin/env python3
import argparse
import json
from pathlib import Path
from typing import Any, Optional, Union
class Kwargs:
def __init__(self):
self.type = None
self.default: Any = None
self.required: bool = False
self.help: Optional[str] = None
self.action: Optional[str] = None
self.choices: Optional[list] = None
# takes a (sub)parser and configures it
def register_parser(
parser: Optional[argparse.ArgumentParser] = None,
schema: Union[dict, str, Path] = "./tests/config/example-schema.json",
) -> dict:
if not isinstance(schema, dict):
with open(str(schema)) as f:
schema: dict = json.load(f)
assert "type" in schema and schema["type"] == "object"
required_set = set(schema.get("required", []))
if parser is None:
parser = argparse.ArgumentParser(description=schema.get("description"))
type_map = {
"array": list,
"boolean": bool,
"integer": int,
"number": float,
"string": str,
}
subparsers = parser.add_subparsers(
title="more options",
description="Other options to configure",
help="the option to configure",
required=True,
)
for name, value in schema.get("properties", {}).items():
assert isinstance(value, dict)
# TODO: add support for nested objects
if value.get("type") == "object":
subparser = subparsers.add_parser(name, help=value.get("description"))
register_parser(parser=subparser, schema=value)
continue
# elif value.get("type") == "array":
# subparser = parser.add_subparsers(dest=name)
# register_parser(subparser, value)
# continue
kwargs = Kwargs()
kwargs.default = value.get("default")
kwargs.help = value.get("description")
kwargs.required = name in required_set
if kwargs.default is not None:
kwargs.help = f"{kwargs.help}, [{kwargs.default}] in default"
if "enum" in value:
enum_list = value["enum"]
assert len(enum_list) > 0, "Enum List is Empty"
arg_type = type(enum_list[0])
assert all(
arg_type is type(item) for item in enum_list
), f"Items in [{enum_list}] with Different Types"
kwargs.type = arg_type
kwargs.choices = enum_list
else:
kwargs.type = type_map[value.get("type")]
del kwargs.choices
name = f"--{name}"
if kwargs.type is bool:
assert not kwargs.default, "boolean have to be False in default"
kwargs.default = False
kwargs.action = "store_true"
del kwargs.type
else:
del kwargs.action
parser.add_argument(name, **vars(kwargs))
if __name__ == "__main__":
parser = argparse.ArgumentParser()
register_parser(parser)
args = parser.parse_args()
print(args)

View File

@@ -0,0 +1,144 @@
{ 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 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;
}