Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions lib/default.nix
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ let
modules = callLibs ./modules.nix;
options = callLibs ./options.nix;
types = callLibs ./types.nix;
fields = callLibs ./fields.nix;

# constants
licenses = callLibs ./licenses.nix;
Expand Down Expand Up @@ -438,6 +439,7 @@ let
revOrTag
repoRevToName
;
inherit (self.fields) mkField;
inherit (self.modules)
evalModules
setDefaultModuleLocation
Expand Down
33 changes: 33 additions & 0 deletions lib/fields.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
{ lib }:
{
/**
Creates a Field attribute set. mkField accepts an attribute set with the following keys:

Example:
mkField { } // => { _type = "field"; }
mkField { default = "foo"; } // => { _type = "field"; default = "foo"; }
mkField { optional = true; } // => { _type = "field"; optional = true; }
*/
mkField =
{
# Default value used when no definition is given in the configuration.
default ? null,
# Textual representation of the default, for the manual.
defaultText ? null,
# Example value used in the manual.
example ? null,
# String describing the field.
description ? null,
# Related packages used in the manual (see `genRelatedPackages` in ../nixos/lib/make-fields-doc/default.nix).
relatedPackages ? null,
Comment on lines 15 to 22
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

thought:
These could be bundled into a single meta attribute, and that might further improve performance by virtue of not being turned into values during non-docs evaluation.
I don't know how significant that is, and it may make mkField annoying to use, typing more and being dissimilar from mkOption.
Also the performance improvement could be inverted when any of the attrs inside of meta were to somehow become part of the main evaluation later (unlikely?).

# Option type, providing type-checking and value merging.
type ? null,
# Whether the field is for NixOS developers only.
internal ? null,
Comment on lines 25 to 26
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Whether the field is for NixOS developers only.
internal ? null,

I don't think we've ever made us of the difference between internal and visible, and visible is the more capable one, supporting "shallow".

# Whether the field shows up in the manual. Default: true. Use false to hide the field and any sub-options from submodules. Use "shallow" to hide only sub-options.
visible ? null,
# Whether the field is omitted from the final record when undefined. Default false.
optional ? null,
}@attrs:
attrs // { _type = "field"; };
}
39 changes: 39 additions & 0 deletions lib/tests/modules.sh
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,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 field 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 fields' 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?
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Docs generation (in tests/misc.nix?)



if false; then
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if false; then


# Shorthand meta attribute does not duplicate the config
checkConfigOutput '^"one two"$' config.result ./shorthand-meta.nix

Expand Down Expand Up @@ -782,6 +819,8 @@ checkConfigOutput '^true$' config.v2checkedPass ./add-check.nix
checkConfigError 'A definition for option .* is not of type .attribute set of signed integer.*' config.v2checkedFail ./add-check.nix


fi

cat <<EOF
====== module tests ======
$pass Pass
Expand Down
20 changes: 20 additions & 0 deletions lib/tests/modules/declare-record-bad-default.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{ lib, ... }:

let
inherit (lib) mkField mkOption types;

person = types.record {
fields = {
nixerSince = mkField { type = types.int; };
name = mkField { type = types.str; };
isCool = mkField {
type = types.bool;
default = "yeah";
};
};
};

in
{
options.people = mkOption { type = types.attrsOf person; };
}
21 changes: 21 additions & 0 deletions lib/tests/modules/declare-record-optional-field.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{ lib, ... }:

let
inherit (lib) mkField mkOption types;

person = types.record {
fields = {
nixerSince = mkField { type = types.int; };
name = mkField { type = types.str; };
age = mkField {
type = types.ints.unsigned;
optional = true;
};
};
freeformType = types.bool;
};

in
{
options.people = mkOption { type = types.attrsOf person; };
}
17 changes: 17 additions & 0 deletions lib/tests/modules/declare-record-wildcard.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{ lib, ... }:

let
inherit (lib) mkField mkOption types;

person = types.record {
fields = {
nixerSince = mkField { type = types.int; };
name = mkField { type = types.str; };
};
freeformType = types.bool;
};

in
{
options.people = mkOption { type = types.attrsOf person; };
}
20 changes: 20 additions & 0 deletions lib/tests/modules/declare-record.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
{ lib, ... }:

let
inherit (lib) mkField mkOption types;

person = types.record {
fields = {
nixerSince = mkField { type = types.int; };
name = mkField { type = types.str; };
isCool = mkField {
type = types.bool;
default = true;
};
};
};

in
{
options.people = mkOption { type = types.attrsOf person; };
}
10 changes: 10 additions & 0 deletions lib/tests/modules/define-record-alice-prefs.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{ lib, ... }:
{
people.alice = {
nixerSince = 2016;
name = "Alice";
hiRes = true;
mechKeyboard = true;
spiders = false;
};
}
6 changes: 6 additions & 0 deletions lib/tests/modules/define-record-alice.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
people.alice = {
nixerSince = 2016;
name = "Alice";
};
}
6 changes: 6 additions & 0 deletions lib/tests/modules/define-record-bob.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
people.bob = {
nixerSince = 2019;
name = "Bob";
};
}
6 changes: 6 additions & 0 deletions lib/tests/modules/define-record-mallory.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
people.mallory = {
nixerSince = "beginning of time";
name = "Bobby";
};
}
7 changes: 7 additions & 0 deletions lib/tests/modules/define-record-mike.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
people.mike = {
nixerSince = 2020;
name = "Mike";
age = 27;
};
}
2 changes: 1 addition & 1 deletion lib/types.nix
Original file line number Diff line number Diff line change
Expand Up @@ -1632,7 +1632,7 @@ let
merged = a.typeMerge b.functor;
in
if merged == null then setType "merge-error" { error = "Cannot merge types"; } else merged;
};
} // import ./types/record.nix { inherit lib; };

in
outer_types // outer_types.types
116 changes: 116 additions & 0 deletions lib/types/record.nix
Original file line number Diff line number Diff line change
@@ -0,0 +1,116 @@
{ lib }:

let
inherit (lib)
mapAttrs
concatMapAttrs
zipAttrs
removeAttrs
attrNames
showOption
optional
optionalAttrs
mergeDefinitions
mkOptionType
isAttrs
;

inherit (lib.options)
showDefs
;

inherit (lib.strings)
escapeNixIdentifier
;

record =
{
fields ? { },
freeformType ? null,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The freeform type applies at the root of a submodule / configuration, whereas this one applies to individual field values; not the attribute set containing them. I think that's a significant difference that should be reflected in the name. I think it was wildcardType.

}@args:
let
checkField =
name: field:
if field._type or null != "field" then
throw "Record field `${escapeNixIdentifier name}` must be declared with `mkField`."
else if (field.optional or false) && (field ? default) then
throw "Record field `${escapeNixIdentifier name}` is optional, but a `default` is provided."
else
field;

checkFreeformType =
type:
if type._type or null == "option-type" then
type
else
throw "Record freeformType must be declared with `mkOptionType`.";

checkedFields = mapAttrs checkField fields;
freeformType = if args ? freeformType then checkFreeformType args.freeformType else null;
in
mkOptionType {
name = "record";
description =
if freeformType == null then "record" else "open record of ${freeformType.description}";
descriptionClass = if freeformType == null then "noun" else "composite";
check = isAttrs;
merge =
loc: defs:
let
data = zipAttrs (
map (
def:
mapAttrs (_: value: {
inherit (def) file;
inherit value;
}) def.value
) defs
);
fieldValues = concatMapAttrs (
fieldName: field:
let
mergedOption = mergeDefinitions (loc ++ [ fieldName ]) field.type (
data.${fieldName} or [ ]
++ optional (field ? default) {
value = lib.mkOptionDefault field.default;
file = "the default value of field ${showOption loc}";
}
);
isRequired = !field.optional or false;
in
builtins.addErrorContext "while evaluating the field `${fieldName}' of option `${showOption loc}'" (
optionalAttrs (isRequired || mergedOption.isDefined) {
${fieldName} = mergedOption.mergedValue;
}
)
) checkedFields;
extraData = removeAttrs data (attrNames checkedFields);
extraValues = mapAttrs (
name: defs:
builtins.addErrorContext "while evaluating freeform value `${name}' of option `${showOption loc}'" (
(mergeDefinitions (loc ++ [ name ]) freeformType defs).mergedValue
)
) extraData;
checkedExtraDefs =
if extraData == { } then
fieldValues
else
throw ''
A definition for option `${showOption loc}' has an unknown fields:
${lib.concatMapAttrsStringSep "\n" (name: defs: "`${name}'${showDefs defs}") extraData}'';
in
if freeformType == null then checkedExtraDefs else fieldValues // extraValues;
nestedTypes = lib.optionalAttrs (freeformType != null) {
inherit freeformType;
};
# TODO: include `_freeformOptions`
getSubOptions = prefix: lib.mapAttrs (name: field:
mergeDefinitions (prefix ++ [ name ]) field.type [ ]
) checkedFields;
};

in
# public
{
inherit record;
}
Loading
Loading