diff --git a/lib/tests/modules.sh b/lib/tests/modules.sh index 3a23766a17f53..e31621ec44411 100755 --- a/lib/tests/modules.sh +++ b/lib/tests/modules.sh @@ -107,6 +107,43 @@ checkConfigError() { fi } +# record field +checkConfigOutput '^"Alice"$' config.people.alice.name ./declare-record.nix ./define-record-alice.nix ./define-record-bob.nix +checkConfigOutput '^2019$' config.people.bob.nixerSince ./declare-record.nix ./define-record-alice.nix ./define-record-bob.nix + +# record field type error +checkConfigError 'A definition for option .people.mallory.nixerSince. is not of type .signed integer.. Definition values' config.people.mallory.nixerSince ./declare-record.nix ./define-record-mallory.nix +checkConfigError 'define-record-mallory.nix.: "beginning of time"' config.people.mallory.nixerSince ./declare-record.nix ./define-record-mallory.nix + +# record field default +checkConfigOutput '^true$' config.people.bob.isCool ./declare-record.nix ./define-record-alice.nix ./define-record-bob.nix + +# record field bad default definition +checkConfigError 'In .the default value of option people.mallory.: "yeah"' config.people.mallory.isCool ./declare-record-bad-default.nix ./define-record-mallory.nix +checkConfigError 'A definition for option .people.mallory.isCool. is not of type .boolean.. Definition values:' config.people.mallory.isCool ./declare-record-bad-default.nix ./define-record-mallory.nix + +# record field works in presence of wildcard +checkConfigOutput '^2016$' config.people.alice.nixerSince ./declare-record-wildcard.nix ./define-record-alice-prefs.nix + +# record wildcard field +checkConfigOutput '^true$' config.people.alice.mechKeyboard ./declare-record-wildcard.nix ./define-record-alice-prefs.nix + +# record definition without corresponding field +checkConfigError 'A definition for option .people.mike. has an unknown field' config.people.mike.age ./declare-record.nix ./define-record-mike.nix +# record optional field without definition +checkConfigError "attribute 'age' in selection path 'config.people.alice.age' not found" config.people.alice.age ./declare-record-optional-field.nix ./define-record-alice.nix +# record optional field with definition +checkConfigOutput '^27$' config.people.mike.age ./declare-record-optional-field.nix ./define-record-mike.nix + +#TODO: +# - test empty definitions +# - test neseted records +# - test nested optional records +# - etc? + + +if false; then + # Shorthand meta attribute does not duplicate the config checkConfigOutput '^"one two"$' config.result ./shorthand-meta.nix @@ -558,6 +595,8 @@ checkConfigOutput '^34|23$' options.submoduleLine34.declarationPositions.1.line # nested options work checkConfigOutput '^30$' options.nested.nestedLine30.declarationPositions.0.line ./declaration-positions.nix +fi + cat < + Freeform modules allow you to define values for option paths that have not been declared explicitly. This can be used to add attribute-specific types to what would otherwise have to be `attrsOf` options in order to @@ -12,6 +14,47 @@ into the resulting `config` set. Since this feature nullifies name checking for entire option trees, it is only recommended for use in submodules. + + +Wildcard records can also be used to achieve the same thing, and may be +preferred in many scenarios due to their improved performance when +compared to submodules and also the ability to declare "optional" fields. + +Wildcards records are used by simply passing `wildcard = types.anything` +to a record type definition. + +::: {#ex-wildcard-record .example} +### Wildcard record + +Most freeform submodules can also be represented as wildcard records. + +The following example is equivalent to the first submodule example: + +```nix +{ lib, config, ... }: { + + options.settings = lib.mkOption { + type = lib.types.record { + + wildcard = lib.types.str; + + # We want this attribute to be checked for the correct type + fields.port = lib.mkOption { + type = lib.types.port; + # Declaring the option also allows defining a default value + default = 8080; + }; + + # We could use an "optional" field, instead of supplying a default + optionalFields.port = lib.mkOption { + type = lib.types.port; + }; + + }; + }; +} +``` + ::: {#ex-freeform-module .example} ### Freeform submodule diff --git a/nixos/doc/manual/development/option-types.section.md b/nixos/doc/manual/development/option-types.section.md index b44a84553b37b..be5c1791178e8 100644 --- a/nixos/doc/manual/development/option-types.section.md +++ b/nixos/doc/manual/development/option-types.section.md @@ -211,9 +211,27 @@ merging is handled. definitions cannot be merged. The regular expression is processed using `builtins.match`. -## Submodule types {#sec-option-types-submodule} +## Record and module types {#sec-option-types-submodule} -Submodules are detailed in [Submodule](#section-option-types-submodule). +Records and submodules are detailed in [Record](#section-option-types-record) and [Submodule](#section-option-types-submodule) respectively. +While submodules provide the full power of the module system, records are usually faster to evaluate and may be preferred in many scenarios. + +`types.record` { `fields` ? {}, `optionalFields` ? {}, `wildcard` ? null }. + +: A set of sub options, represented as fields. A field can be almost + any option, including record or submodule type options. + It has parameters: + + - *`fields`* An attribute set of options that will be merged into + the final value. + + - *`optionalFields`* An attribute set of options that will only be + merged into the final value _if_ they are defined. + + - *`wildcard`* An option-type used to merge unknown field + definitions into the final value. + + If null, definitions for unknown fields will throw an error. `types.submodule` *`o`* @@ -417,6 +435,48 @@ Composed types are types that take a type as parameter. `listOf value of type *`to`*. Can be used to preserve backwards compatibility of an option if its type was changed. +## Record {#section-option-types-record} + + + +Records are a simpler alternative to [submodules](#section-option-types-submodule). +Rather than merging fully-fledged modules, a record consists only of "field" sub-options and an optional "wildcard" type, similar to a submodule's `freeformType`. + + ::: {.note} + Because records are not implemented using the full module system, they can usually be evaluated significantly faster than submodules. + ::: + +### Record fields + +A record's sub-options are declared as `fields`. A field can be almost any option (restrictions listed below). +You can also use other record or submodule options in a record's fields to implement "nested" options. + +#### Restrictions + +- Fields must be options (e.g. created using `lib.mkOption`) +- Fields cannot have an `apply` function +- Fields cannot be `readOnly` + +### Record optional fields + +In addition to _required_ fields, a record can contain "optional" fields. Optional fields are similar to wildcard or freeform types, in that they are only merged into the final value when defined. +This contrasts "required" fields and submodule sub-options, which will still be merged into the final value when undefined as a stub value that throws a "used but not defined" error when read. + +Optional fields are a unique feature, not currently supported by submodules. + +### Restrictions +- All the same restrictions for required fields apply +- Additionally, optional fields cannot have a `default` + +### Record wildcard type + +Records can optionally be declared with a "wildcard" type, which can be used to allow definitions that do not match the explicitly declared "field" options. +Any definition that does not match a field option will be checked against the "wildcard" type, for example you could set `wildcard = types.anything`. + + ::: {.info} + A wildcard record with no fields can be thought of as equivialent to `types.attrsOf`! + ::: + ## Submodule {#section-option-types-submodule} `submodule` is a very powerful type that defines a set of sub-options