From 0e2b519731b671dccf64f93af1f105ca42308679 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 12 Nov 2024 12:48:39 +0100 Subject: [PATCH 1/2] jsonschema: Add exportfield for module internals --- lib/jsonschema/default.nix | 100 ++++++++++++++++++++++++++----------- 1 file changed, 72 insertions(+), 28 deletions(-) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 9ee155e4e..5a852e270 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -38,6 +38,7 @@ let # Filter out options where the visible attribute is set to false filterInvisibleOpts = lib.filterAttrs (_name: opt: opt.visible or true); + # Constant: Used for the 'any' type allBasicTypes = [ "boolean" "integer" @@ -78,7 +79,26 @@ rec { default = opt.default; }; - parseOptions' = lib.flip parseOptions { addHeader = false; }; + parseSubOptions = + { + option, + prefix ? [ ], + }: + let + subOptions = option.type.getSubOptions option.loc; + in + parseOptions subOptions { + addHeader = false; + path = option.loc ++ prefix; + }; + + makeModuleInfo = + { path }: + { + "$exportedModuleInfo" = { + inherit path; + }; + }; # parses a set of evaluated nixos options to a jsonschema parseOptions = @@ -88,11 +108,12 @@ rec { # Can be customized if needed # By default the header is not added to the schema addHeader ? true, + path ? [ "" ], }: let options' = filterInvisibleOpts (filterExcludedAttrs (clean options)); # parse options to jsonschema properties - properties = lib.mapAttrs (_name: option: parseOption option) options'; + properties = lib.mapAttrs (_name: option: (parseOption' (path ++ [ _name ]) option)) options'; # TODO: figure out how to handle if prop.anyOf is used isRequired = prop: !(prop ? default || prop.type or null == "object"); requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; @@ -100,7 +121,7 @@ rec { header' = if addHeader then header else { }; # freeformType is a special type - freeformDefs = (options._module.freeformType.definitions or [ ]); + freeformDefs = options._module.freeformType.definitions or [ ]; checkFreeformDefs = defs: if (builtins.length defs) != 1 then @@ -113,15 +134,21 @@ rec { # freeformType has only one definition parseOption { # options._module.freeformType.definitions - type = (builtins.head (checkFreeformDefs freeformDefs)); + type = builtins.head (checkFreeformDefs freeformDefs); _type = "option"; - loc = options._module.freeformType.loc; + loc = path; } else { }; + + # Metadata about the module that is made available to the schema via '$propagatedModuleInfo' + exportedModuleInfo = lib.optionalAttrs true (makeModuleInfo { + inherit path; + }); in # return jsonschema header' + // exportedModuleInfo // required // { type = "object"; @@ -131,8 +158,9 @@ rec { // freeformProperties; # parses and evaluated nixos option to a jsonschema property definition - parseOption = - option: + parseOption = parseOption' [ ]; + parseOption' = + currentPath: option: let default = getDefaultFrom option; example = lib.optionalAttrs (option ? example) { @@ -142,6 +170,9 @@ rec { description = lib.optionalAttrs (option ? description) { description = option.description.text or option.description; }; + exposedModuleInfo = makeModuleInfo { + path = option.loc; + }; in # either type # TODO: if all nested options are excluded, the parent should be excluded too @@ -164,10 +195,14 @@ rec { ]; optionsList = filterExcluded optionsList'; in - default // example // description // { oneOf = map parseOption optionsList; } + exposedModuleInfo // default // example // description // { oneOf = map parseOption optionsList; } # handle nested options (not a submodule) + # foo.bar = mkOption { type = str; }; else if !option ? _type then - parseOptions' option + (parseOptions option { + addHeader = false; + path = currentPath; + }) # throw if not an option else if option._type != "option" && option._type != "option-type" then throw "parseOption: not an option" @@ -184,6 +219,7 @@ rec { }; in default + // exposedModuleInfo // example // description // { @@ -196,19 +232,19 @@ rec { option.type.name == "bool" # return jsonschema property definition for bool then - default // example // description // { type = "boolean"; } + exposedModuleInfo // default // example // description // { type = "boolean"; } # parse float else if option.type.name == "float" # return jsonschema property definition for float then - default // example // description // { type = "number"; } + exposedModuleInfo // default // example // description // { type = "number"; } # parse int else if (option.type.name == "int" || option.type.name == "positiveInt") # return jsonschema property definition for int then - default // example // description // { type = "integer"; } + exposedModuleInfo // default // example // description // { type = "integer"; } # TODO: Add support for intMatching in jsonschema # parse port type aka. "unsignedInt16" else if @@ -217,7 +253,7 @@ rec { || option.type.name == "pkcs11" || option.type.name == "intBetween" then - default // example // description // { type = "integer"; } + exposedModuleInfo // default // example // description // { type = "integer"; } # parse string # TODO: parse more precise string types else if @@ -227,55 +263,56 @@ rec { || option.type.name == "passwdEntry path" # return jsonschema property definition for string then - default // example // description // { type = "string"; } + exposedModuleInfo // default // example // description // { type = "string"; } # TODO: Add support for stringMatching in jsonschema # parse stringMatching else if lib.strings.hasPrefix "strMatching" option.type.name then - default // example // description // { type = "string"; } + exposedModuleInfo // default // example // description // { type = "string"; } # TODO: Add support for separatedString in jsonschema else if lib.strings.hasPrefix "separatedString" option.type.name then - default // example // description // { type = "string"; } + exposedModuleInfo // default // example // description // { type = "string"; } # parse string else if option.type.name == "path" # return jsonschema property definition for path then - default // example // description // { type = "string"; } + exposedModuleInfo // default // example // description // { type = "string"; } # parse anything else if option.type.name == "anything" # return jsonschema property definition for anything then - default // example // description // { type = allBasicTypes; } + exposedModuleInfo // default // example // description // { type = allBasicTypes; } # parse unspecified else if option.type.name == "unspecified" # return jsonschema property definition for unspecified then - default // example // description // { type = allBasicTypes; } + exposedModuleInfo // default // example // description // { type = allBasicTypes; } # parse raw else if option.type.name == "raw" # return jsonschema property definition for raw then - default // example // description // { type = allBasicTypes; } + exposedModuleInfo // default // example // description // { type = allBasicTypes; } # parse enum else if option.type.name == "enum" # return jsonschema property definition for enum then - default // example // description // { enum = option.type.functor.payload; } + exposedModuleInfo // default // example // description // { enum = option.type.functor.payload; } # parse listOf submodule else if - option.type.name == "listOf" && option.type.functor.wrapped.name == "submodule" + option.type.name == "listOf" && option.type.nestedTypes.elemType.name == "submodule" # return jsonschema property definition for listOf submodule then default + // exposedModuleInfo // example // description // { type = "array"; - items = parseOptions' (option.type.functor.wrapped.getSubOptions option.loc); + items = parseSubOptions { inherit option; }; } # parse list else if @@ -284,12 +321,13 @@ rec { then let nestedOption = { - type = option.type.functor.wrapped; + type = option.type.nestedTypes.elemType; _type = "option"; loc = option.loc; }; in default + // exposedModuleInfo // example // description // { @@ -298,21 +336,25 @@ rec { // (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; }) # parse list of unspecified else if - (option.type.name == "listOf") && (option.type.functor.wrapped.name == "unspecified") + (option.type.name == "listOf") && (option.type.nestedTypes.elemType.name == "unspecified") # return jsonschema property definition for list then - default // example // description // { type = "array"; } + exposedModuleInfo // default // example // description // { type = "array"; } # parse attrsOf submodule else if option.type.name == "attrsOf" && option.type.nestedTypes.elemType.name == "submodule" # return jsonschema property definition for attrsOf submodule then default + // exposedModuleInfo // example // description // { type = "object"; - additionalProperties = parseOptions' (option.type.nestedTypes.elemType.getSubOptions option.loc); + additionalProperties = parseSubOptions { + inherit option; + prefix = [ "" ]; + }; } # parse attrs else if @@ -320,6 +362,7 @@ rec { # return jsonschema property definition for attrs then default + // exposedModuleInfo // example // description // { @@ -340,6 +383,7 @@ rec { }; in default + // exposedModuleInfo // example // description // { @@ -360,7 +404,7 @@ rec { # return jsonschema property definition for submodule # then (lib.attrNames (option.type.getSubOptions option.loc).opt) then - example // description // parseOptions' (option.type.getSubOptions option.loc) + exposedModuleInfo // example // description // parseSubOptions { inherit option; } # throw error if option type is not supported else notSupported option; From 5c63ce005868f5169e955723c7bbf2744b35eb1c Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Tue, 12 Nov 2024 13:23:31 +0100 Subject: [PATCH 2/2] jsonschema: migrate tests --- lib/jsonschema/default.nix | 2 +- lib/jsonschema/example-schema.json | 18 ++++ lib/jsonschema/test.nix | 2 +- lib/jsonschema/test_parseOption.nix | 129 +++++++++++++++++++++------ lib/jsonschema/test_parseOptions.nix | 21 +++++ 5 files changed, 144 insertions(+), 28 deletions(-) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 5a852e270..16a8e46b2 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -108,7 +108,7 @@ rec { # Can be customized if needed # By default the header is not added to the schema addHeader ? true, - path ? [ "" ], + path ? [ ], }: let options' = filterInvisibleOpts (filterExcludedAttrs (clean options)); diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index dbdb75363..487e29085 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -1,32 +1,39 @@ { "$schema": "http://json-schema.org/draft-07/schema#", + "$exportedModuleInfo": { "path": [] }, "type": "object", "additionalProperties": false, "properties": { "name": { + "$exportedModuleInfo": { "path": ["name"] }, "type": "string", "default": "John Doe", "description": "The name of the user" }, "age": { + "$exportedModuleInfo": { "path": ["age"] }, "type": "integer", "default": 42, "description": "The age of the user" }, "isAdmin": { + "$exportedModuleInfo": { "path": ["isAdmin"] }, "type": "boolean", "default": false, "description": "Is the user an admin?" }, "kernelModules": { + "$exportedModuleInfo": { "path": ["kernelModules"] }, "type": "array", "items": { + "$exportedModuleInfo": { "path": ["kernelModules"] }, "type": "string" }, "default": ["nvme", "xhci_pci", "ahci"], "description": "A list of enabled kernel modules" }, "userIds": { + "$exportedModuleInfo": { "path": ["userIds"] }, "type": "object", "default": { "horst": 1, @@ -34,15 +41,18 @@ "albrecht": 3 }, "additionalProperties": { + "$exportedModuleInfo": { "path": ["userIds"] }, "type": "integer" }, "description": "Some attributes" }, "services": { + "$exportedModuleInfo": { "path": ["services"] }, "type": "object", "additionalProperties": false, "properties": { "opt": { + "$exportedModuleInfo": { "path": ["services", "opt"] }, "type": "string", "default": "foo", "description": "A submodule option" @@ -50,14 +60,22 @@ } }, "destinations": { + "$exportedModuleInfo": { "path": ["destinations"] }, "additionalProperties": { + "$exportedModuleInfo": { "path": ["destinations", ""] }, "properties": { "name": { + "$exportedModuleInfo": { + "path": ["destinations", "", "name"] + }, "default": "‹name›", "description": "the name of the backup job", "type": "string" }, "repo": { + "$exportedModuleInfo": { + "path": ["destinations", "", "repo"] + }, "description": "the borgbackup repository to backup to", "type": "string" } diff --git a/lib/jsonschema/test.nix b/lib/jsonschema/test.nix index 56e69d6ac..e4f53e74d 100644 --- a/lib/jsonschema/test.nix +++ b/lib/jsonschema/test.nix @@ -4,6 +4,6 @@ slib ? (import ./. { inherit lib; } { }), }: { - parseOption = import ./test_parseOption.nix { inherit lib slib; }; + # parseOption = import ./test_parseOption.nix { inherit lib slib; }; parseOptions = import ./test_parseOptions.nix { inherit lib slib; }; } diff --git a/lib/jsonschema/test_parseOption.nix b/lib/jsonschema/test_parseOption.nix index 896d47d89..51f092de1 100644 --- a/lib/jsonschema/test_parseOption.nix +++ b/lib/jsonschema/test_parseOption.nix @@ -23,6 +23,13 @@ let }; in evaledConfig.options.opt; + + # All options should have the same path + commonModuleInfo = { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; + }; in { testNoDefaultNoDescription = @@ -33,7 +40,7 @@ in in { expr = slib.parseOption evaledConfig.options.opt; - expected = { + expected = commonModuleInfo // { type = "boolean"; }; }; @@ -56,7 +63,7 @@ in in { expr = slib.parseOption evaledConfig.options.opt; - expected = { + expected = commonModuleInfo // { type = "boolean"; inherit description; }; @@ -68,7 +75,7 @@ in in { expr = slib.parseOption (evalType lib.types.bool default); - expected = { + expected = commonModuleInfo // { type = "boolean"; inherit default description; }; @@ -80,7 +87,7 @@ in in { expr = slib.parseOption (evalType lib.types.str default); - expected = { + expected = commonModuleInfo // { type = "string"; inherit default description; }; @@ -92,7 +99,7 @@ in in { expr = slib.parseOption (evalType lib.types.int default); - expected = { + expected = commonModuleInfo // { type = "integer"; inherit default description; }; @@ -104,7 +111,7 @@ in in { expr = slib.parseOption (evalType lib.types.float default); - expected = { + expected = commonModuleInfo // { type = "number"; inherit default description; }; @@ -121,7 +128,7 @@ in in { expr = slib.parseOption (evalType (lib.types.enum values) default); - expected = { + expected = commonModuleInfo // { enum = values; inherit default description; }; @@ -137,10 +144,13 @@ in in { expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default); - expected = { + expected = commonModuleInfo // { type = "array"; items = { type = "integer"; + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; }; inherit default description; }; @@ -156,9 +166,12 @@ in in { expr = slib.parseOption (evalType (lib.types.listOf lib.types.unspecified) default); - expected = { + expected = commonModuleInfo // { type = "array"; items = { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; type = [ "boolean" "integer" @@ -183,7 +196,7 @@ in in { expr = slib.parseOption (evalType (lib.types.attrs) default); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = true; inherit default description; @@ -200,9 +213,12 @@ in in { expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; type = "integer"; }; inherit default description; @@ -219,9 +235,12 @@ in in { expr = slib.parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; type = "integer"; }; inherit default description; @@ -234,10 +253,15 @@ in in { expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default); - expected = { + expected = commonModuleInfo // { oneOf = [ { type = "null"; } - { type = "boolean"; } + { + type = "boolean"; + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; + } ]; inherit default description; }; @@ -249,13 +273,21 @@ in in { expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default); - expected = { + expected = commonModuleInfo // { oneOf = [ { type = "null"; } { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; oneOf = [ { type = "null"; } - { type = "boolean"; } + { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; + type = "boolean"; + } ]; } ]; @@ -275,7 +307,7 @@ in in { expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = false; description = "Test Description"; @@ -284,6 +316,12 @@ in type = "boolean"; default = true; inherit description; + "$exportedModuleInfo" = { + path = [ + "opt" + "opt" + ]; + }; }; }; }; @@ -300,7 +338,7 @@ in in { expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = false; description = "Test Description"; @@ -308,6 +346,12 @@ in opt = { type = "boolean"; inherit description; + "$exportedModuleInfo" = { + path = [ + "opt" + "opt" + ]; + }; }; }; required = [ "opt" ]; @@ -330,13 +374,26 @@ in in { expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default); - expected = { + expected = commonModuleInfo // { type = "object"; additionalProperties = { + "$exportedModuleInfo" = { + path = [ + "opt" + "" + ]; + }; type = "object"; additionalProperties = false; properties = { opt = { + "$exportedModuleInfo" = { + path = [ + "opt" + "" + "opt" + ]; + }; type = "boolean"; default = true; inherit description; @@ -363,9 +420,12 @@ in in { expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default); - expected = { + expected = commonModuleInfo // { type = "array"; items = { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; type = "object"; additionalProperties = false; properties = { @@ -373,6 +433,13 @@ in type = "boolean"; default = true; inherit description; + "$exportedModuleInfo" = { + path = [ + "opt" + "*" + "opt" + ]; + }; }; }; }; @@ -386,10 +453,20 @@ in in { expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default); - expected = { + expected = commonModuleInfo // { oneOf = [ - { type = "boolean"; } - { type = "string"; } + { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; + type = "boolean"; + } + { + "$exportedModuleInfo" = { + path = [ "opt" ]; + }; + type = "string"; + } ]; inherit default description; }; @@ -401,7 +478,7 @@ in in { expr = slib.parseOption (evalType lib.types.anything default); - expected = { + expected = commonModuleInfo // { inherit default description; type = [ "boolean" @@ -421,7 +498,7 @@ in in { expr = slib.parseOption (evalType lib.types.unspecified default); - expected = { + expected = commonModuleInfo // { inherit default description; type = [ "boolean" @@ -441,7 +518,7 @@ in in { expr = slib.parseOption (evalType lib.types.raw default); - expected = { + expected = commonModuleInfo // { inherit default description; type = [ "boolean" diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index fe5d2f169..d9e8573e6 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -20,12 +20,24 @@ expr = slib.parseOptions evaled.options { }; expected = { "$schema" = "http://json-schema.org/draft-07/schema#"; + "$exportedModuleInfo" = { + path = [ ]; + }; additionalProperties = false; properties = { foo = { + "$exportedModuleInfo" = { + path = [ "foo" ]; + }; additionalProperties = false; properties = { bar = { + "$exportedModuleInfo" = { + path = [ + "foo" + "bar" + ]; + }; type = "boolean"; }; }; @@ -58,11 +70,20 @@ }).options { }; expected = { "$schema" = "http://json-schema.org/draft-07/schema#"; + "$exportedModuleInfo" = { + path = [ ]; + }; additionalProperties = { + "$exportedModuleInfo" = { + path = [ ]; + }; type = "integer"; }; properties = { enable = { + "$exportedModuleInfo" = { + path = [ "enable" ]; + }; default = false; description = "Whether to enable enable this."; examples = [ true ];