From 06e27c84de5efb56ce2310af2010d7d0d978ebda Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 16 Apr 2025 09:13:19 +0200 Subject: [PATCH 1/4] lib/jsonschema: make `attrs` required Before the change, modules of the form ```nix { lib, ... }: { foo.bar = lib.mkOption { # ... }; } ``` or ```nix { lib, ... }: { foo = lib.mkOption { type = lib.types.subModule { bar = lib.mkOption { # ... }; }; }; } ``` would not render with `foo` as required, which is not faithful to the module system's semantics. This change also tests that fields with defaults are not marked required. Note that submodule options cannot have their defaults rendered to JSON schema, and are therefore always marked required. Architecturally this change is rather unfortunate: So far the checks for defaults happen in the rendering (using `isDefault`) and not in the parsing, but here we're adding a field to `$exportedModuleInfo`. While strictly speaking we probably don't want to consider requiredness as module-level information, it seems more reasonable to me to do it that way since at the JSON schema level we have lost the distinction between `attrs`, `attrsOf`, `submodule`. --- lib/jsonschema/default.nix | 14 ++++++++++---- lib/jsonschema/example-schema.json | 1 + lib/jsonschema/test_parseOptions.nix | 23 ++++++++++++++++++++++- 3 files changed, 33 insertions(+), 5 deletions(-) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 177fef8d5..855739897 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -123,7 +123,7 @@ rec { # parse options to jsonschema properties 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"); + isRequired = prop: !(prop ? default || prop."$exportedModuleInfo".required or false); requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; header' = if addHeader then header else { }; @@ -150,9 +150,9 @@ rec { { }; # Metadata about the module that is made available to the schema via '$propagatedModuleInfo' - exportedModuleInfo = lib.optionalAttrs true (makeModuleInfo { + exportedModuleInfo = makeModuleInfo { inherit path; - }); + }; in # return jsonschema header' @@ -377,7 +377,13 @@ rec { # return jsonschema property definition for attrs then default - // exposedModuleInfo + // (lib.recursiveUpdate exposedModuleInfo ( + lib.optionalAttrs (!default ? default) { + "$exportedModuleInfo" = { + required = true; + }; + } + )) // example // description // { diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index 3944cca62..ca2175472 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -3,6 +3,7 @@ "$exportedModuleInfo": { "path": [] }, "type": "object", "additionalProperties": false, + "required": [ "services" ], "properties": { "name": { "$exportedModuleInfo": { "path": ["name"] }, diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index 137bf52a1..0c2ff8a93 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -13,7 +13,17 @@ testParseNestedOptions = let evaled = lib.evalModules { - modules = [ { options.foo.bar = lib.mkOption { type = lib.types.bool; }; } ]; + modules = [ + { + options.foo.bar = lib.mkOption { + type = lib.types.bool; + }; + options.foo.baz = lib.mkOption { + type = lib.types.bool; + default = false; + }; + } + ]; }; in { @@ -40,12 +50,23 @@ }; type = "boolean"; }; + baz = { + "$exportedModuleInfo" = { + path = [ + "foo" + "baz" + ]; + }; + type = "boolean"; + default = false; + }; }; required = [ "bar" ]; type = "object"; }; }; type = "object"; + required = [ "foo" ]; }; }; From 15fe06fbf5755b86cf722e1c45a55c441a0c07a1 Mon Sep 17 00:00:00 2001 From: Johannes Kirschbauer Date: Wed, 16 Apr 2025 14:48:35 +0200 Subject: [PATCH 2/4] feat(jsonschema): add test for attrsof submodule --- lib/jsonschema/example-interface.nix | 8 ++++++++ lib/jsonschema/example-schema.json | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/lib/jsonschema/example-interface.nix b/lib/jsonschema/example-interface.nix index 9b7c1b9e8..fedd0d12b 100644 --- a/lib/jsonschema/example-interface.nix +++ b/lib/jsonschema/example-interface.nix @@ -40,6 +40,14 @@ albrecht = 3; }; }; + # attrs of submodule + userModules = lib.mkOption { + type = lib.types.attrsOf ( + lib.types.submodule { + options.foo = lib.mkOption { }; + } + ); + }; # list of str kernelModules = lib.mkOption { type = lib.types.listOf lib.types.str; diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index ca2175472..6b95742a4 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -3,7 +3,7 @@ "$exportedModuleInfo": { "path": [] }, "type": "object", "additionalProperties": false, - "required": [ "services" ], + "required": ["services", "userModules"], "properties": { "name": { "$exportedModuleInfo": { "path": ["name"] }, @@ -47,6 +47,32 @@ }, "description": "Some attributes" }, + "userModules": { + "$exportedModuleInfo": { "path": ["userModules"] }, + "type": "object", + "additionalProperties": { + "$exportedModuleInfo": { "path": ["userModules", ""] }, + "additionalProperties": false, + "type": "object", + "properties": { + "foo": { + "$exportedModuleInfo": { + "path": ["userModules", "", "foo"] + }, + "type": [ + "boolean", + "integer", + "number", + "string", + "array", + "object", + "null" + ] + } + }, + "required": ["foo"] + } + }, "colour": { "$exportedModuleInfo": { "path": ["colour"] }, "default": "red", From af7915a5649e8f2c9c356191904749018e77c198 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 16 Apr 2025 16:44:56 +0200 Subject: [PATCH 3/4] lib/jsonschema: render defaults for submodule options this relaxes the constraint that options of type `submodule` are always required, and will render benign default values. --- lib/jsonschema/default.nix | 14 +++++++++++++- lib/jsonschema/example-interface.nix | 13 ++++++++++++- lib/jsonschema/example-schema.json | 14 ++++++++++++++ lib/jsonschema/test_parseOption.nix | 2 ++ 4 files changed, 41 insertions(+), 2 deletions(-) diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 855739897..b0bad30de 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -425,7 +425,19 @@ rec { # return jsonschema property definition for submodule # then (lib.attrNames (option.type.getSubOptions option.loc).opt) then - exposedModuleInfo // example // description // parseSubOptions { inherit option; } + (lib.recursiveUpdate exposedModuleInfo ( + if (default ? default) then + default + else + { + "$exportedModuleInfo" = { + required = true; + }; + } + )) + // example + // description + // parseSubOptions { inherit option; } # throw error if option type is not supported else notSupported option; diff --git a/lib/jsonschema/example-interface.nix b/lib/jsonschema/example-interface.nix index fedd0d12b..3d1e04d57 100644 --- a/lib/jsonschema/example-interface.nix +++ b/lib/jsonschema/example-interface.nix @@ -20,7 +20,7 @@ default = false; description = "Is the user an admin?"; }; - # a submodule option + # a submodule option without default services = lib.mkOption { type = lib.types.submodule { options.opt = lib.mkOption { @@ -30,6 +30,17 @@ }; }; }; + # a submodule option with default + programs = lib.mkOption { + type = lib.types.submodule { + options.opt = lib.mkOption { + type = lib.types.str; + default = "bar"; + description = "Another submodule option"; + }; + }; + default = { }; + }; # attrs of int userIds = lib.mkOption { type = lib.types.attrsOf lib.types.int; diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index 6b95742a4..ecb937fc7 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -92,6 +92,20 @@ } } }, + "programs": { + "$exportedModuleInfo": { "path": ["programs"] }, + "type": "object", + "additionalProperties": false, + "properties": { + "opt": { + "$exportedModuleInfo": { "path": ["programs", "opt"] }, + "type": "string", + "default": "bar", + "description": "Another submodule option" + } + }, + "default": {} + }, "destinations": { "$exportedModuleInfo": { "path": ["destinations"] }, "additionalProperties": { diff --git a/lib/jsonschema/test_parseOption.nix b/lib/jsonschema/test_parseOption.nix index 51f092de1..75ced777c 100644 --- a/lib/jsonschema/test_parseOption.nix +++ b/lib/jsonschema/test_parseOption.nix @@ -324,6 +324,7 @@ in }; }; }; + default = { }; }; }; @@ -354,6 +355,7 @@ in }; }; }; + default = { }; required = [ "opt" ]; }; }; From c8b305c437e930a0cf9bcc08757ceae597137431 Mon Sep 17 00:00:00 2001 From: Valentin Gagarin Date: Wed, 16 Apr 2025 16:54:08 +0200 Subject: [PATCH 4/4] fixup(jsonschema): test attrsOf submodules with valid value --- lib/jsonschema/example-data.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/lib/jsonschema/example-data.json b/lib/jsonschema/example-data.json index 9fb8e0cd7..74b728423 100644 --- a/lib/jsonschema/example-data.json +++ b/lib/jsonschema/example-data.json @@ -16,5 +16,10 @@ "name": "John Doe", "repo": "test-backup" } + }, + "userModules": { + "some-user": { + "foo": {} + } } }