diff --git a/lib/jsonschema/default.nix b/lib/jsonschema/default.nix index 094407869..f51d97218 100644 --- a/lib/jsonschema/default.nix +++ b/lib/jsonschema/default.nix @@ -96,12 +96,15 @@ rec { makeModuleInfo = { path, + required, defaultText ? null, - }: + ... + }@attrs: { "$exportedModuleInfo" = - { - inherit path; + attrs + // { + inherit path required; } // lib.optionalAttrs (defaultText != null) { inherit defaultText; @@ -119,13 +122,20 @@ rec { path ? [ ], }: let - options' = filterInvisibleOpts (filterExcludedAttrs (clean options)); + options' = (filterInvisibleOpts (filterExcludedAttrs (clean options))); # parse options to jsonschema properties - properties = lib.mapAttrs (_name: option: (parseOption' (path ++ [ _name ]) 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."$exportedModuleInfo".required or false); - requiredProps = lib.filterAttrs (_: prop: isRequired prop) properties; - required = lib.optionalAttrs (requiredProps != { }) { required = lib.attrNames requiredProps; }; + isRequired = prop: prop."$exportedModuleInfo".required or true; + requiredProps = lib.filterAttrs (_: prop: isRequired prop) (properties); + + # json schema spec 6.5.3: required + # The value of this keyword MUST be an array. Elements of this array, if any, MUST be strings, and MUST be unique. + # ... + # Omitting this keyword has the same behavior as an empty array. + required = { + required = lib.attrNames requiredProps; + }; header' = if addHeader then header else { }; # freeformType is a special type @@ -148,15 +158,9 @@ rec { } else { }; - - # Metadata about the module that is made available to the schema via '$propagatedModuleInfo' - exportedModuleInfo = makeModuleInfo { - inherit path; - }; in # return jsonschema header' - // exportedModuleInfo // required // { type = "object"; @@ -169,287 +173,277 @@ rec { parseOption = parseOption' [ ]; parseOption' = currentPath: option: - let - default = getDefaultFrom option; - example = lib.optionalAttrs (option ? example) { - examples = - if (builtins.typeOf option.example) == "list" then option.example else [ option.example ]; - }; - description = lib.optionalAttrs (option ? description) { - description = option.description.text or option.description; - }; - exposedModuleInfo = makeModuleInfo { - path = option.loc; - defaultText = option.defaultText or null; - }; - in - # either type - # TODO: if all nested options are excluded, the parent should be excluded too - if - option.type.name or null == "either" || option.type.name or null == "coercedTo" - # return jsonschema property definition for either - then + # lib.trace + ( let - optionsList' = [ - { - type = option.type.nestedTypes.left or option.type.nestedTypes.coercedType; - _type = "option"; - loc = option.loc; + default = (getDefaultFrom (option)); + example = lib.optionalAttrs (option ? example) { + examples = + if (builtins.typeOf option.example) == "list" then option.example else [ option.example ]; + }; + description = lib.optionalAttrs (option ? description) { + description = option.description.text or option.description; + }; + exposedModuleInfo = ( + makeModuleInfo { + path = option.loc; + defaultText = option.defaultText or null; + required = !(option.defaultText or null != null || option ? default); + default = option.default or null; } - { - type = option.type.nestedTypes.right or option.type.nestedTypes.finalType; - _type = "option"; - loc = option.loc; - } - ]; - optionsList = filterExcluded optionsList'; + ); + # default = in - 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 { - addHeader = false; - path = currentPath; - }) - # throw if not an option - else if option._type != "option" && option._type != "option-type" then - throw "parseOption: not an option" - # parse nullOr - else if - option.type.name == "nullOr" - # return jsonschema property definition for nullOr - then - let - nestedOption = { - type = option.type.nestedTypes.elemType; - _type = "option"; - loc = option.loc; - }; - in - default - // exposedModuleInfo - // example - // description - // { - oneOf = [ - { type = "null"; } - ] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption)); - } - # parse bool - else if - option.type.name == "bool" - # return jsonschema property definition for bool - then - exposedModuleInfo // default // example // description // { type = "boolean"; } - # parse float - else if - option.type.name == "float" - # return jsonschema property definition for float - then - 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 - exposedModuleInfo // default // example // description // { type = "integer"; } - # TODO: Add support for intMatching in jsonschema - # parse port type aka. "unsignedInt16" - else if - option.type.name == "unsignedInt16" - || option.type.name == "unsignedInt" - || option.type.name == "pkcs11" - || option.type.name == "intBetween" - then - exposedModuleInfo // default // example // description // { type = "integer"; } - # parse string - # TODO: parse more precise string types - else if - option.type.name == "str" - || option.type.name == "singleLineStr" - || option.type.name == "passwdEntry str" - || option.type.name == "passwdEntry path" - # return jsonschema property definition for string - then - 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 - exposedModuleInfo // default // example // description // { type = "string"; } - # TODO: Add support for separatedString in jsonschema - else if lib.strings.hasPrefix "separatedString" option.type.name then - exposedModuleInfo // default // example // description // { type = "string"; } - # parse string - else if - option.type.name == "path" - # return jsonschema property definition for path - then - exposedModuleInfo // default // example // description // { type = "string"; } - # parse anything - else if - option.type.name == "anything" - # return jsonschema property definition for anything - then - exposedModuleInfo // default // example // description // { type = allBasicTypes; } - # parse unspecified - else if - option.type.name == "unspecified" - # return jsonschema property definition for unspecified - then - exposedModuleInfo // default // example // description // { type = allBasicTypes; } - # parse raw - else if - option.type.name == "raw" - # return jsonschema property definition for raw - then - exposedModuleInfo // default // example // description // { type = allBasicTypes; } - else if - # This is a special case for the deferred clan.service 'settings', we assume it is JSON serializable - # To get the type of a Deferred modules we need to know the interface of the place where it is evaluated. - # i.e. in case of a clan.service this is the interface of the service which dynamically changes depending on the service - # We assign "type" = [] - # This means any value is valid — or like TypeScript’s unknown. - # We can assign the type later, when we know the exact interface. - # tsType = "unknown" is a type that we preload for json2ts, such that it gets the correct type in typescript - (option.type.name == "deferredModule") - then - exposedModuleInfo // default // example // description // { tsType = "unknown"; } - # parse enum - else if - option.type.name == "enum" - # return jsonschema property definition for enum - then - exposedModuleInfo - // default - // example - // description - // { - enum = option.type.functor.payload.values; - } - # parse listOf submodule - else if - 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 = parseSubOptions { inherit option; }; - } - # parse list - else if - (option.type.name == "listOf") - # return jsonschema property definition for list - then - let - nestedOption = { - type = option.type.nestedTypes.elemType; - _type = "option"; - loc = option.loc; - }; - in - default - // exposedModuleInfo - // example - // description - // { - type = "array"; - } - // (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; }) - # parse list of unspecified - else if - (option.type.name == "listOf") && (option.type.nestedTypes.elemType.name == "unspecified") - # return jsonschema property definition for list - then - 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 = parseSubOptions { - inherit option; - prefix = [ "" ]; - }; - } - # parse attrs - else if - option.type.name == "attrs" - # return jsonschema property definition for attrs - then - default - // (lib.recursiveUpdate exposedModuleInfo ( - lib.optionalAttrs (!default ? default) { - "$exportedModuleInfo" = { - required = true; - }; - } - )) - // example - // description - // { - type = "object"; - additionalProperties = true; - } - # parse attrsOf - # TODO: if nested option is excluded, the parent should be excluded too - else if - option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf" - # return jsonschema property definition for attrs - then - let - nestedOption = { - type = option.type.nestedTypes.elemType; - _type = "option"; - loc = option.loc; - }; - in - default - // exposedModuleInfo - // example - // description - // { - type = "object"; - additionalProperties = - if !isExcludedOption nestedOption then - parseOption { - type = option.type.nestedTypes.elemType; + # either type + # TODO: if all nested options are excluded, the parent should be excluded too + if + option.type.name or null == "either" || option.type.name or null == "coercedTo" + # return jsonschema property definition for either + then + let + optionsList' = [ + { + type = option.type.nestedTypes.left or option.type.nestedTypes.coercedType; _type = "option"; loc = option.loc; } - else - false; - } - # parse submodule - else if - option.type.name == "submodule" - # return jsonschema property definition for submodule - # then (lib.attrNames (option.type.getSubOptions option.loc).opt) - then - (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; + { + type = option.type.nestedTypes.right or option.type.nestedTypes.finalType; + _type = "option"; + loc = option.loc; + } + ]; + optionsList = filterExcluded optionsList'; + in + 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 { + addHeader = false; + path = currentPath; + }) + # throw if not an option + else if option._type != "option" && option._type != "option-type" then + throw "parseOption: not an option" + # parse nullOr + else if + option.type.name == "nullOr" + # return jsonschema property definition for nullOr + then + let + nestedOption = { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + }; + in + default + // exposedModuleInfo + // example + // description + // { + oneOf = [ + { type = "null"; } + ] ++ (lib.optional (!isExcludedOption nestedOption) (parseOption nestedOption)); + } + # parse bool + else if + option.type.name == "bool" + # return jsonschema property definition for bool + then + exposedModuleInfo // default // example // description // { type = "boolean"; } + # parse float + else if + option.type.name == "float" + # return jsonschema property definition for float + then + 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 + exposedModuleInfo // default // example // description // { type = "integer"; } + # TODO: Add support for intMatching in jsonschema + # parse port type aka. "unsignedInt16" + else if + option.type.name == "unsignedInt16" + || option.type.name == "unsignedInt" + || option.type.name == "pkcs11" + || option.type.name == "intBetween" + then + exposedModuleInfo // default // example // description // { type = "integer"; } + # parse string + # TODO: parse more precise string types + else if + option.type.name == "str" + || option.type.name == "singleLineStr" + || option.type.name == "passwdEntry str" + || option.type.name == "passwdEntry path" + # return jsonschema property definition for string + then + 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 + exposedModuleInfo // default // example // description // { type = "string"; } + # TODO: Add support for separatedString in jsonschema + else if lib.strings.hasPrefix "separatedString" option.type.name then + exposedModuleInfo // default // example // description // { type = "string"; } + # parse string + else if + option.type.name == "path" + # return jsonschema property definition for path + then + exposedModuleInfo // default // example // description // { type = "string"; } + # parse anything + else if + option.type.name == "anything" + # return jsonschema property definition for anything + then + exposedModuleInfo // default // example // description // { type = allBasicTypes; } + # parse unspecified + else if + option.type.name == "unspecified" + # return jsonschema property definition for unspecified + then + exposedModuleInfo // default // example // description // { type = allBasicTypes; } + # parse raw + else if + option.type.name == "raw" + # return jsonschema property definition for raw + then + exposedModuleInfo // default // example // description // { type = allBasicTypes; } + else if + # This is a special case for the deferred clan.service 'settings', we assume it is JSON serializable + # To get the type of a Deferred modules we need to know the interface of the place where it is evaluated. + # i.e. in case of a clan.service this is the interface of the service which dynamically changes depending on the service + # We assign "type" = [] + # This means any value is valid — or like TypeScript’s unknown. + # We can assign the type later, when we know the exact interface. + # tsType = "unknown" is a type that we preload for json2ts, such that it gets the correct type in typescript + (option.type.name == "deferredModule") + then + exposedModuleInfo // default // example // description // { tsType = "unknown"; } + # parse enum + else if + option.type.name == "enum" + # return jsonschema property definition for enum + then + exposedModuleInfo + // default + // example + // description + // { + enum = option.type.functor.payload.values; + } + # parse listOf submodule + else if + 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 = parseSubOptions { inherit option; }; + } + # parse list + else if + (option.type.name == "listOf") + # return jsonschema property definition for list + then + let + nestedOption = { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + }; + in + default + // exposedModuleInfo + // example + // description + // { + type = "array"; + } + // (lib.optionalAttrs (!isExcludedOption nestedOption) { items = parseOption nestedOption; }) + # parse list of unspecified + else if + (option.type.name == "listOf") && (option.type.nestedTypes.elemType.name == "unspecified") + # return jsonschema property definition for list + then + 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 = parseSubOptions { + inherit option; + prefix = [ "" ]; + }; + } + # parse attrs + else if + option.type.name == "attrs" + # return jsonschema property definition for attrs + then + default + // exposedModuleInfo + // example + // description + // { + type = "object"; + additionalProperties = true; + } + # parse attrsOf + # TODO: if nested option is excluded, the parent should be excluded too + else if + option.type.name == "attrsOf" || option.type.name == "lazyAttrsOf" + # return jsonschema property definition for attrs + then + let + nestedOption = { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + }; + in + default + // exposedModuleInfo + // example + // description + // { + type = "object"; + additionalProperties = + if !isExcludedOption nestedOption then + parseOption { + type = option.type.nestedTypes.elemType; + _type = "option"; + loc = option.loc; + } + else + false; + } + # parse submodule + else if + option.type.name == "submodule" + # return jsonschema property definition for submodule + # then (lib.attrNames (option.type.getSubOptions option.loc).opt) + then + default // exposedModuleInfo // example // description // parseSubOptions ({ inherit option; }) + # throw error if option type is not supported + else + notSupported option + ); } diff --git a/lib/jsonschema/example-schema.json b/lib/jsonschema/example-schema.json index ecb937fc7..e2b6e165c 100644 --- a/lib/jsonschema/example-schema.json +++ b/lib/jsonschema/example-schema.json @@ -1,40 +1,33 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$exportedModuleInfo": { "path": [] }, "type": "object", "additionalProperties": false, "required": ["services", "userModules"], "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, @@ -42,23 +35,17 @@ "albrecht": 3 }, "additionalProperties": { - "$exportedModuleInfo": { "path": ["userIds"] }, "type": "integer" }, "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", @@ -74,55 +61,44 @@ } }, "colour": { - "$exportedModuleInfo": { "path": ["colour"] }, "default": "red", "description": "The colour of the user", "enum": ["red", "blue", "green"] }, "services": { - "$exportedModuleInfo": { "path": ["services"] }, "type": "object", "additionalProperties": false, "properties": { "opt": { - "$exportedModuleInfo": { "path": ["services", "opt"] }, "type": "string", "default": "foo", "description": "A submodule option" } - } + }, + "required": [] }, "programs": { - "$exportedModuleInfo": { "path": ["programs"] }, "type": "object", "additionalProperties": false, "properties": { "opt": { - "$exportedModuleInfo": { "path": ["programs", "opt"] }, "type": "string", "default": "bar", "description": "Another submodule option" } }, + "required": [], "default": {} }, "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_parseOption.nix b/lib/jsonschema/test_parseOption.nix index 75ced777c..3cdf32da0 100644 --- a/lib/jsonschema/test_parseOption.nix +++ b/lib/jsonschema/test_parseOption.nix @@ -7,29 +7,31 @@ let description = "Test Description"; + # Wrap the parseOption function to reduce the surface that needs to be migrated, when '$exportedModuleInfo' changes + parseOption = opt: filterSchema (slib.parseOption opt); + evalType = type: default: + (evalModuleOptions { + options.opt = lib.mkOption { + inherit type description; + default = default; + }; + }).opt; + + evalModuleOptions = + module: let evaledConfig = lib.evalModules { modules = [ - { - options.opt = lib.mkOption { - inherit type; - inherit default; - inherit description; - }; - } + module ]; }; in - evaledConfig.options.opt; + evaledConfig.options; - # All options should have the same path - commonModuleInfo = { - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; - }; + filterSchema = + schema: lib.filterAttrsRecursive (name: _value: name != "$exportedModuleInfo") schema; in { testNoDefaultNoDescription = @@ -39,8 +41,8 @@ in }; in { - expr = slib.parseOption evaledConfig.options.opt; - expected = commonModuleInfo // { + expr = parseOption evaledConfig.options.opt; + expected = { type = "boolean"; }; }; @@ -62,8 +64,8 @@ in }; in { - expr = slib.parseOption evaledConfig.options.opt; - expected = commonModuleInfo // { + expr = parseOption evaledConfig.options.opt; + expected = { type = "boolean"; inherit description; }; @@ -74,8 +76,8 @@ in default = false; in { - expr = slib.parseOption (evalType lib.types.bool default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.bool default); + expected = { type = "boolean"; inherit default description; }; @@ -86,8 +88,8 @@ in default = "hello"; in { - expr = slib.parseOption (evalType lib.types.str default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.str default); + expected = { type = "string"; inherit default description; }; @@ -98,8 +100,8 @@ in default = 42; in { - expr = slib.parseOption (evalType lib.types.int default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.int default); + expected = { type = "integer"; inherit default description; }; @@ -110,8 +112,8 @@ in default = 42.42; in { - expr = slib.parseOption (evalType lib.types.float default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.float default); + expected = { type = "number"; inherit default description; }; @@ -127,8 +129,8 @@ in ]; in { - expr = slib.parseOption (evalType (lib.types.enum values) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.enum values) default); + expected = { enum = values; inherit default description; }; @@ -143,14 +145,11 @@ in ]; in { - expr = slib.parseOption (evalType (lib.types.listOf lib.types.int) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.listOf lib.types.int) default); + expected = { type = "array"; items = { type = "integer"; - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; }; inherit default description; }; @@ -165,13 +164,10 @@ in ]; in { - expr = slib.parseOption (evalType (lib.types.listOf lib.types.unspecified) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.listOf lib.types.unspecified) default); + expected = { type = "array"; items = { - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; type = [ "boolean" "integer" @@ -195,8 +191,8 @@ in }; in { - expr = slib.parseOption (evalType (lib.types.attrs) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.attrs) default); + expected = { type = "object"; additionalProperties = true; inherit default description; @@ -212,13 +208,11 @@ in }; in { - expr = slib.parseOption (evalType (lib.types.attrsOf lib.types.int) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.attrsOf lib.types.int) default); + expected = { type = "object"; additionalProperties = { - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; + type = "integer"; }; inherit default description; @@ -234,13 +228,11 @@ in }; in { - expr = slib.parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.lazyAttrsOf lib.types.int) default); + expected = { type = "object"; additionalProperties = { - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; + type = "integer"; }; inherit default description; @@ -252,14 +244,17 @@ in default = null; # null is a valid value for this type in { - expr = slib.parseOption (evalType (lib.types.nullOr lib.types.bool) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.nullOr lib.types.bool) default); + expected = { oneOf = [ { type = "null"; } { type = "boolean"; "$exportedModuleInfo" = { path = [ "opt" ]; + default = null; + defaultText = null; + required = true; }; } ]; @@ -272,19 +267,25 @@ in default = null; # null is a valid value for this type in { - expr = slib.parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.nullOr (lib.types.nullOr lib.types.bool)) default); + expected = { oneOf = [ { type = "null"; } { "$exportedModuleInfo" = { + default = null; + defaultText = null; path = [ "opt" ]; + required = true; }; oneOf = [ { type = "null"; } { "$exportedModuleInfo" = { + default = null; + defaultText = null; path = [ "opt" ]; + required = true; }; type = "boolean"; } @@ -306,8 +307,8 @@ in }; in { - expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.submodule subModule) { }); + expected = { type = "object"; additionalProperties = false; description = "Test Description"; @@ -316,14 +317,9 @@ in type = "boolean"; default = true; inherit description; - "$exportedModuleInfo" = { - path = [ - "opt" - "opt" - ]; - }; }; }; + required = [ ]; default = { }; }; }; @@ -331,32 +327,28 @@ in testSubmoduleOptionWithoutDefault = let subModule = { - options.opt = lib.mkOption { + options.foo = lib.mkOption { type = lib.types.bool; inherit description; }; }; + opt = evalType (lib.types.submodule subModule) { }; in { - expr = slib.parseOption (evalType (lib.types.submodule subModule) { }); - expected = commonModuleInfo // { + inherit opt; + expr = parseOption (opt); + expected = { type = "object"; additionalProperties = false; description = "Test Description"; properties = { - opt = { + foo = { type = "boolean"; inherit description; - "$exportedModuleInfo" = { - path = [ - "opt" - "opt" - ]; - }; }; }; default = { }; - required = [ "opt" ]; + required = [ "foo" ]; }; }; @@ -375,32 +367,21 @@ in }; in { - expr = slib.parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.attrsOf (lib.types.submodule subModule)) default); + expected = { type = "object"; additionalProperties = { - "$exportedModuleInfo" = { - path = [ - "opt" - "" - ]; - }; + type = "object"; additionalProperties = false; properties = { opt = { - "$exportedModuleInfo" = { - path = [ - "opt" - "" - "opt" - ]; - }; type = "boolean"; default = true; inherit description; }; }; + required = [ ]; }; inherit default description; }; @@ -421,13 +402,10 @@ in ]; in { - expr = slib.parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.listOf (lib.types.submodule subModule)) default); + expected = { type = "array"; items = { - "$exportedModuleInfo" = { - path = [ "opt" ]; - }; type = "object"; additionalProperties = false; properties = { @@ -435,15 +413,9 @@ in type = "boolean"; default = true; inherit description; - "$exportedModuleInfo" = { - path = [ - "opt" - "*" - "opt" - ]; - }; }; }; + required = [ ]; }; inherit default description; }; @@ -454,18 +426,24 @@ in default = "foo"; in { - expr = slib.parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default); - expected = commonModuleInfo // { + expr = parseOption (evalType (lib.types.either lib.types.bool lib.types.str) default); + expected = { oneOf = [ { "$exportedModuleInfo" = { path = [ "opt" ]; + default = null; + defaultText = null; + required = true; }; type = "boolean"; } { "$exportedModuleInfo" = { path = [ "opt" ]; + default = null; + defaultText = null; + required = true; }; type = "string"; } @@ -479,8 +457,8 @@ in default = "foo"; in { - expr = slib.parseOption (evalType lib.types.anything default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.anything default); + expected = { inherit default description; type = [ "boolean" @@ -499,8 +477,8 @@ in default = "foo"; in { - expr = slib.parseOption (evalType lib.types.unspecified default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.unspecified default); + expected = { inherit default description; type = [ "boolean" @@ -519,8 +497,8 @@ in default = "foo"; in { - expr = slib.parseOption (evalType lib.types.raw default); - expected = commonModuleInfo // { + expr = parseOption (evalType lib.types.raw default); + expected = { inherit default description; type = [ "boolean" @@ -533,4 +511,61 @@ in ]; }; }; + + test_option_with_default_text = { + expr = ( + parseOption (evalModuleOptions { + options.opt = lib.mkOption { + type = lib.types.bool; + defaultText = "This option is a optional, but we cannot assign a default value to it yet."; + }; + }) + ); + + expected = { + additionalProperties = false; + properties = { + opt = { + type = "boolean"; + }; + }; + # opt is not required, because it has a defaultText + required = [ ]; + type = "object"; + }; + }; + test_nested_option_with_default_text = { + expr = ( + parseOption (evalModuleOptions { + options.opt = lib.mkOption { + type = lib.types.submodule { + options = { + foo = lib.mkOption { + type = lib.types.bool; + defaultText = "Not required"; + }; + }; + }; + defaultText = "Not required"; + }; + }) + ); + expected = { + additionalProperties = false; + properties = { + opt = { + additionalProperties = false; + properties = { + foo = { + type = "boolean"; + }; + }; + required = [ ]; + type = "object"; + }; + }; + required = [ ]; + type = "object"; + }; + }; } diff --git a/lib/jsonschema/test_parseOptions.nix b/lib/jsonschema/test_parseOptions.nix index 0c2ff8a93..739f2f058 100644 --- a/lib/jsonschema/test_parseOptions.nix +++ b/lib/jsonschema/test_parseOptions.nix @@ -4,9 +4,13 @@ lib ? (import { }).lib, slib ? (import ./. { inherit lib; } { }), }: +let + filterSchema = + schema: lib.filterAttrsRecursive (name: _value: name != "$exportedModuleInfo") schema; +in { testParseOptions = { - expr = slib.parseModule ./example-interface.nix; + expr = filterSchema (slib.parseModule ./example-interface.nix); expected = builtins.fromJSON (builtins.readFile ./example-schema.json); }; @@ -27,36 +31,18 @@ }; in { - expr = slib.parseOptions evaled.options { }; + expr = filterSchema (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"; }; baz = { - "$exportedModuleInfo" = { - path = [ - "foo" - "baz" - ]; - }; type = "boolean"; default = false; }; @@ -78,7 +64,7 @@ }; in { - expr = + expr = filterSchema ( slib.parseOptions (lib.evalModules { modules = [ @@ -91,29 +77,22 @@ default ]; }).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 ]; type = "boolean"; }; }; + required = [ ]; type = "object"; }; };