-
-
Notifications
You must be signed in to change notification settings - Fork 18.4k
lib/types: add record (simplified submodule)
#334680
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
fef8b11
39d7c8f
c21e79a
8dad01e
514b6b0
0692c8b
6edec33
27ed4ac
a925162
694ea38
1a372a1
4432827
5af8e09
598fdec
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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, | ||||||
| # Option type, providing type-checking and value merging. | ||||||
| type ? null, | ||||||
| # Whether the field is for NixOS developers only. | ||||||
| internal ? null, | ||||||
|
||||||
| # 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".
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -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? | ||||
|
||||
|
|
||||
|
|
||||
| if false; then | ||||
|
||||
| if false; then |
| 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; }; | ||
| } |
| 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; }; | ||
| } |
| 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; }; | ||
| } |
| 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; }; | ||
| } |
| 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; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| people.alice = { | ||
| nixerSince = 2016; | ||
| name = "Alice"; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| people.bob = { | ||
| nixerSince = 2019; | ||
| name = "Bob"; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| { | ||
| people.mallory = { | ||
| nixerSince = "beginning of time"; | ||
| name = "Bobby"; | ||
| }; | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| { | ||
| people.mike = { | ||
| nixerSince = 2020; | ||
| name = "Mike"; | ||
| age = 27; | ||
| }; | ||
| } |
| 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, | ||
|
||
| }@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; | ||
| } | ||
There was a problem hiding this comment.
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
metaattribute, 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
mkFieldannoying to use, typing more and being dissimilar frommkOption.Also the performance improvement could be inverted when any of the attrs inside of
metawere to somehow become part of the main evaluation later (unlikely?).