Merge pull request #284551 from hercules-ci/types-attrTag
Add `types.attrTag`
This commit is contained in:
commit
4f1d724b82
8 changed files with 387 additions and 19 deletions
|
@ -128,7 +128,7 @@ let
|
|||
canCleanSource pathIsGitRepo;
|
||||
inherit (self.modules) evalModules setDefaultModuleLocation
|
||||
unifyModuleSyntax applyModuleArgsIfFunction mergeModules
|
||||
mergeModules' mergeOptionDecls evalOptionValue mergeDefinitions
|
||||
mergeModules' mergeOptionDecls mergeDefinitions
|
||||
pushDownProperties dischargeProperties filterOverrides
|
||||
sortProperties fixupOptionType mkIf mkAssert mkMerge mkOverride
|
||||
mkOptionDefault mkDefault mkImageMediaOverride mkForce mkVMOverride
|
||||
|
@ -138,6 +138,7 @@ let
|
|||
mkMergedOptionModule mkChangedOptionModule
|
||||
mkAliasOptionModule mkDerivedConfig doRename
|
||||
mkAliasOptionModuleMD;
|
||||
evalOptionValue = lib.warn "External use of `lib.evalOptionValue` is deprecated. If your use case isn't covered by non-deprecated functions, we'd like to know more and perhaps support your use case well, instead of providing access to these low level functions. In this case please open an issue in https://github.com/nixos/nixpkgs/issues/." self.modules.evalOptionValue;
|
||||
inherit (self.options) isOption mkEnableOption mkSinkUndeclaredOptions
|
||||
mergeDefaultOption mergeOneOption mergeEqualOption mergeUniqueOption
|
||||
getValues getFiles
|
||||
|
|
|
@ -1378,7 +1378,6 @@ let
|
|||
inherit
|
||||
applyModuleArgsIfFunction
|
||||
dischargeProperties
|
||||
evalOptionValue
|
||||
mergeModules
|
||||
mergeModules'
|
||||
pushDownProperties
|
||||
|
@ -1399,6 +1398,7 @@ private //
|
|||
defaultPriority
|
||||
doRename
|
||||
evalModules
|
||||
evalOptionValue # for use by lib.types
|
||||
filterOverrides
|
||||
filterOverrides'
|
||||
fixMergeModules
|
||||
|
|
|
@ -103,6 +103,18 @@ checkConfigError 'The option .sub.wrong2. does not exist. Definition values:' co
|
|||
checkConfigError '.*This can happen if you e.g. declared your options in .types.submodule.' config.sub ./error-mkOption-in-submodule-config.nix
|
||||
checkConfigError '.*A definition for option .bad. is not of type .non-empty .list of .submodule...\.' config.bad ./error-nonEmptyListOf-submodule.nix
|
||||
|
||||
# types.attrTag
|
||||
checkConfigOutput '^true$' config.okChecks ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.syntaxError. is not of type .attribute-tagged union' config.intStrings.syntaxError ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.syntaxError2. is not of type .attribute-tagged union' config.intStrings.syntaxError2 ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.syntaxError3. is not of type .attribute-tagged union' config.intStrings.syntaxError3 ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.syntaxError4. is not of type .attribute-tagged union' config.intStrings.syntaxError4 ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.mergeError. is not of type .attribute-tagged union' config.intStrings.mergeError ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.badTagError. is not of type .attribute-tagged union' config.intStrings.badTagError ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .intStrings\.badTagTypeError\.left. is not of type .signed integer.' config.intStrings.badTagTypeError.left ./types-attrTag.nix
|
||||
checkConfigError 'A definition for option .nested\.right\.left. is not of type .signed integer.' config.nested.right.left ./types-attrTag.nix
|
||||
checkConfigError 'In attrTag, each tag value must be an option, but tag int was a bare type, not wrapped in mkOption.' config.opt.int ./types-attrTag-wrong-decl.nix
|
||||
|
||||
# types.pathInStore
|
||||
checkConfigOutput '".*/store/0lz9p8xhf89kb1c1kk6jxrzskaiygnlh-bash-5.2-p15.drv"' config.pathInStore.ok1 ./types.nix
|
||||
checkConfigOutput '".*/store/0fb3ykw9r5hpayd05sr0cizwadzq1d8q-bash-5.2-p15"' config.pathInStore.ok2 ./types.nix
|
||||
|
|
41
lib/tests/modules/docs.nix
Normal file
41
lib/tests/modules/docs.nix
Normal file
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
A basic documentation generating module.
|
||||
Declares and defines a `docs` option, suitable for making assertions about
|
||||
the extraction "phase" of documentation generation.
|
||||
*/
|
||||
{ lib, options, ... }:
|
||||
|
||||
let
|
||||
inherit (lib)
|
||||
head
|
||||
length
|
||||
mkOption
|
||||
types
|
||||
;
|
||||
|
||||
traceListSeq = l: v: lib.foldl' (a: b: lib.traceSeq b a) v l;
|
||||
|
||||
in
|
||||
|
||||
{
|
||||
options.docs = mkOption {
|
||||
type = types.lazyAttrsOf types.raw;
|
||||
description = ''
|
||||
All options to be rendered, without any visibility filtering applied.
|
||||
'';
|
||||
};
|
||||
config.docs =
|
||||
lib.zipAttrsWith
|
||||
(name: values:
|
||||
if length values > 1 then
|
||||
traceListSeq values
|
||||
abort "Multiple options with the same name: ${name}"
|
||||
else
|
||||
assert length values == 1;
|
||||
head values
|
||||
)
|
||||
(map
|
||||
(opt: { ${opt.name} = opt; })
|
||||
(lib.optionAttrSetToDocList options)
|
||||
);
|
||||
}
|
14
lib/tests/modules/types-attrTag-wrong-decl.nix
Normal file
14
lib/tests/modules/types-attrTag-wrong-decl.nix
Normal file
|
@ -0,0 +1,14 @@
|
|||
{ lib, ... }:
|
||||
let
|
||||
inherit (lib) types mkOption;
|
||||
in
|
||||
{
|
||||
options = {
|
||||
opt = mkOption {
|
||||
type = types.attrTag {
|
||||
int = types.int;
|
||||
};
|
||||
default = { int = 1; };
|
||||
};
|
||||
};
|
||||
}
|
135
lib/tests/modules/types-attrTag.nix
Normal file
135
lib/tests/modules/types-attrTag.nix
Normal file
|
@ -0,0 +1,135 @@
|
|||
{ lib, config, options, ... }:
|
||||
let
|
||||
inherit (lib) mkOption types;
|
||||
forceDeep = x: builtins.deepSeq x x;
|
||||
mergedSubOption = (options.merged.type.getSubOptions options.merged.loc).extensible."merged.<name>";
|
||||
in
|
||||
{
|
||||
options = {
|
||||
intStrings = mkOption {
|
||||
type = types.attrsOf
|
||||
(types.attrTag {
|
||||
left = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
right = mkOption {
|
||||
type = types.str;
|
||||
};
|
||||
});
|
||||
};
|
||||
nested = mkOption {
|
||||
type = types.attrTag {
|
||||
left = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
right = mkOption {
|
||||
type = types.attrTag {
|
||||
left = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
right = mkOption {
|
||||
type = types.str;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
merged = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.attrTag {
|
||||
yay = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
extensible = mkOption {
|
||||
type = types.enum [ "foo" ];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
submodules = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.attrTag {
|
||||
foo = mkOption {
|
||||
type = types.submodule {
|
||||
options = {
|
||||
bar = mkOption {
|
||||
type = types.int;
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
qux = mkOption {
|
||||
type = types.str;
|
||||
description = "A qux for when you don't want a foo";
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
okChecks = mkOption {};
|
||||
};
|
||||
imports = [
|
||||
./docs.nix
|
||||
{
|
||||
options.merged = mkOption {
|
||||
type = types.attrsOf (
|
||||
types.attrTag {
|
||||
nay = mkOption {
|
||||
type = types.bool;
|
||||
};
|
||||
extensible = mkOption {
|
||||
type = types.enum [ "bar" ];
|
||||
};
|
||||
}
|
||||
);
|
||||
};
|
||||
}
|
||||
];
|
||||
config = {
|
||||
intStrings.syntaxError = 1;
|
||||
intStrings.syntaxError2 = {};
|
||||
intStrings.syntaxError3 = { a = true; b = true; };
|
||||
intStrings.syntaxError4 = lib.mkMerge [ { a = true; } { b = true; } ];
|
||||
intStrings.mergeError = lib.mkMerge [ { int = throw "do not eval"; } { string = throw "do not eval"; } ];
|
||||
intStrings.badTagError.rite = throw "do not eval";
|
||||
intStrings.badTagTypeError.left = "bad";
|
||||
intStrings.numberOne.left = 1;
|
||||
intStrings.hello.right = "hello world";
|
||||
nested.right.left = "not a number";
|
||||
merged.negative.nay = false;
|
||||
merged.positive.yay = 100;
|
||||
merged.extensi-foo.extensible = "foo";
|
||||
merged.extensi-bar.extensible = "bar";
|
||||
okChecks = builtins.addErrorContext "while evaluating the assertions" (
|
||||
assert config.intStrings.hello == { right = "hello world"; };
|
||||
assert config.intStrings.numberOne == { left = 1; };
|
||||
assert config.merged.negative == { nay = false; };
|
||||
assert config.merged.positive == { yay = 100; };
|
||||
assert config.merged.extensi-foo == { extensible = "foo"; };
|
||||
assert config.merged.extensi-bar == { extensible = "bar"; };
|
||||
assert config.docs."submodules.<name>.foo.bar".type == "signed integer";
|
||||
assert config.docs."submodules.<name>.qux".type == "string";
|
||||
assert config.docs."submodules.<name>.qux".declarations == [ __curPos.file ];
|
||||
assert config.docs."submodules.<name>.qux".loc == [ "submodules" "<name>" "qux" ];
|
||||
assert config.docs."submodules.<name>.qux".name == "submodules.<name>.qux";
|
||||
assert config.docs."submodules.<name>.qux".description == "A qux for when you don't want a foo";
|
||||
assert config.docs."submodules.<name>.qux".readOnly == false;
|
||||
assert config.docs."submodules.<name>.qux".visible == true;
|
||||
# Not available (yet?)
|
||||
# assert config.docs."submodules.<name>.qux".declarationsWithPositions == [ ... ];
|
||||
assert options.submodules.declarations == [ __curPos.file ];
|
||||
assert lib.length options.submodules.declarationPositions == 1;
|
||||
assert (lib.head options.submodules.declarationPositions).file == __curPos.file;
|
||||
assert options.merged.declarations == [ __curPos.file __curPos.file ];
|
||||
assert lib.length options.merged.declarationPositions == 2;
|
||||
assert (lib.elemAt options.merged.declarationPositions 0).file == __curPos.file;
|
||||
assert (lib.elemAt options.merged.declarationPositions 1).file == __curPos.file;
|
||||
assert (lib.elemAt options.merged.declarationPositions 0).line != (lib.elemAt options.merged.declarationPositions 1).line;
|
||||
assert mergedSubOption.declarations == [ __curPos.file __curPos.file ];
|
||||
assert lib.length mergedSubOption.declarationPositions == 2;
|
||||
assert (lib.elemAt mergedSubOption.declarationPositions 0).file == __curPos.file;
|
||||
assert (lib.elemAt mergedSubOption.declarationPositions 1).file == __curPos.file;
|
||||
assert (lib.elemAt mergedSubOption.declarationPositions 0).line != (lib.elemAt mergedSubOption.declarationPositions 1).line;
|
||||
assert lib.length config.docs."merged.<name>.extensible".declarations == 2;
|
||||
true);
|
||||
};
|
||||
}
|
102
lib/types.nix
102
lib/types.nix
|
@ -15,6 +15,7 @@ let
|
|||
isList
|
||||
isString
|
||||
isStorePath
|
||||
throwIf
|
||||
toDerivation
|
||||
toList
|
||||
;
|
||||
|
@ -65,6 +66,11 @@ let
|
|||
fixupOptionType
|
||||
mergeOptionDecls
|
||||
;
|
||||
|
||||
inAttrPosSuffix = v: name:
|
||||
let pos = builtins.unsafeGetAttrPos name v; in
|
||||
if pos == null then "" else " at ${pos.file}:${toString pos.line}:${toString pos.column}";
|
||||
|
||||
outer_types =
|
||||
rec {
|
||||
__attrsFailEvaluation = true;
|
||||
|
@ -152,7 +158,7 @@ rec {
|
|||
# If it doesn't, this should be {}
|
||||
# This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
|
||||
emptyValue ? {}
|
||||
, # Return a flat list of sub-options. Used to generate
|
||||
, # Return a flat attrset of sub-options. Used to generate
|
||||
# documentation.
|
||||
getSubOptions ? prefix: {}
|
||||
, # List of modules if any, or null if none.
|
||||
|
@ -623,6 +629,100 @@ rec {
|
|||
nestedTypes.elemType = elemType;
|
||||
};
|
||||
|
||||
attrTag = tags:
|
||||
let tags_ = tags; in
|
||||
let
|
||||
tags =
|
||||
mapAttrs
|
||||
(n: opt:
|
||||
builtins.addErrorContext "while checking that attrTag tag ${lib.strings.escapeNixIdentifier n} is an option with a type${inAttrPosSuffix tags_ n}" (
|
||||
throwIf (opt._type or null != "option")
|
||||
"In attrTag, each tag value must be an option, but tag ${lib.strings.escapeNixIdentifier n} ${
|
||||
if opt?_type then
|
||||
if opt._type == "option-type"
|
||||
then "was a bare type, not wrapped in mkOption."
|
||||
else "was of type ${lib.strings.escapeNixString opt._type}."
|
||||
else "was not."}"
|
||||
opt // {
|
||||
declarations = opt.declarations or (
|
||||
let pos = builtins.unsafeGetAttrPos n tags_;
|
||||
in if pos == null then [] else [ pos.file ]
|
||||
);
|
||||
declarationPositions = opt.declarationPositions or (
|
||||
let pos = builtins.unsafeGetAttrPos n tags_;
|
||||
in if pos == null then [] else [ pos ]
|
||||
);
|
||||
}
|
||||
))
|
||||
tags_;
|
||||
choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags);
|
||||
in
|
||||
mkOptionType {
|
||||
name = "attrTag";
|
||||
description = "attribute-tagged union";
|
||||
descriptionClass = "noun";
|
||||
getSubOptions = prefix:
|
||||
mapAttrs
|
||||
(tagName: tagOption: {
|
||||
"${lib.showOption prefix}" =
|
||||
tagOption // {
|
||||
loc = prefix ++ [ tagName ];
|
||||
};
|
||||
})
|
||||
tags;
|
||||
check = v: isAttrs v && length (attrNames v) == 1 && tags?${head (attrNames v)};
|
||||
merge = loc: defs:
|
||||
let
|
||||
choice = head (attrNames (head defs).value);
|
||||
checkedValueDefs = map
|
||||
(def:
|
||||
assert (length (attrNames def.value)) == 1;
|
||||
if (head (attrNames def.value)) != choice
|
||||
then throw "The option `${showOption loc}` is defined both as `${choice}` and `${head (attrNames def.value)}`, in ${showFiles (getFiles defs)}."
|
||||
else { inherit (def) file; value = def.value.${choice}; })
|
||||
defs;
|
||||
in
|
||||
if tags?${choice}
|
||||
then
|
||||
{ ${choice} =
|
||||
(lib.modules.evalOptionValue
|
||||
(loc ++ [choice])
|
||||
tags.${choice}
|
||||
checkedValueDefs
|
||||
).value;
|
||||
}
|
||||
else throw "The option `${showOption loc}` is defined as ${lib.strings.escapeNixIdentifier choice}, but ${lib.strings.escapeNixIdentifier choice} is not among the valid choices (${choicesStr}). Value ${choice} was defined in ${showFiles (getFiles defs)}.";
|
||||
nestedTypes = tags;
|
||||
functor = defaultFunctor "attrTag" // {
|
||||
type = { tags, ... }: types.attrTag tags;
|
||||
payload = { inherit tags; };
|
||||
binOp =
|
||||
let
|
||||
# Add metadata in the format that submodules work with
|
||||
wrapOptionDecl =
|
||||
option: { options = option; _file = "<attrTag {...}>"; pos = null; };
|
||||
in
|
||||
a: b: {
|
||||
tags = a.tags // b.tags //
|
||||
mapAttrs
|
||||
(tagName: bOpt:
|
||||
lib.mergeOptionDecls
|
||||
# FIXME: loc is not accurate; should include prefix
|
||||
# Fortunately, it's only used for error messages, where a "relative" location is kinda ok.
|
||||
# It is also returned though, but use of the attribute seems rare?
|
||||
[tagName]
|
||||
[ (wrapOptionDecl a.tags.${tagName}) (wrapOptionDecl bOpt) ]
|
||||
// {
|
||||
# mergeOptionDecls is not idempotent in these attrs:
|
||||
declarations = a.tags.${tagName}.declarations ++ bOpt.declarations;
|
||||
declarationPositions = a.tags.${tagName}.declarationPositions ++ bOpt.declarationPositions;
|
||||
}
|
||||
)
|
||||
(builtins.intersectAttrs a.tags b.tags);
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
uniq = unique { message = ""; };
|
||||
|
||||
unique = { message }: type: mkOptionType rec {
|
||||
|
|
|
@ -42,6 +42,9 @@ merging is handled.
|
|||
: One element of the list *`l`*, e.g. `types.enum [ "left" "right" ]`.
|
||||
Multiple definitions cannot be merged.
|
||||
|
||||
If you want to pair these values with more information, possibly of
|
||||
distinct types, consider using a [sum type](#sec-option-types-sums).
|
||||
|
||||
`types.anything`
|
||||
|
||||
: A type that accepts any value and recursively merges attribute sets
|
||||
|
@ -279,6 +282,84 @@ Submodules are detailed in [Submodule](#section-option-types-submodule).
|
|||
more convenient and discoverable than expecting the module user to
|
||||
type-merge with the `attrsOf submodule` option.
|
||||
|
||||
## Union types {#sec-option-types-unions}
|
||||
|
||||
A union of types is a type such that a value is valid when it is valid for at least one of those types.
|
||||
|
||||
If some values are instances of more than one of the types, it is not possible to distinguish which type they are meant to be instances of. If that's needed, consider using a [sum type](#sec-option-types-sums).
|
||||
|
||||
`types.either` *`t1 t2`*
|
||||
|
||||
: Type *`t1`* or type *`t2`*, e.g. `with types; either int str`.
|
||||
Multiple definitions cannot be merged.
|
||||
|
||||
`types.oneOf` \[ *`t1 t2`* ... \]
|
||||
|
||||
: Type *`t1`* or type *`t2`* and so forth, e.g.
|
||||
`with types; oneOf [ int str bool ]`. Multiple definitions cannot be
|
||||
merged.
|
||||
|
||||
`types.nullOr` *`t`*
|
||||
|
||||
: `null` or type *`t`*. Multiple definitions are merged according to
|
||||
type *`t`*.
|
||||
|
||||
|
||||
## Sum types {#sec-option-types-sums}
|
||||
|
||||
A sum type can be thought of, conceptually, as a *`types.enum`* where each valid item is paired with at least a type, through some value syntax.
|
||||
Nix does not have a built-in syntax for this pairing of a label and a type or value, so sum types may be represented in multiple ways.
|
||||
|
||||
If the you're interested in can be distinguished without a label, you may simplify your value syntax with a [union type](#sec-option-types-unions) instead.
|
||||
|
||||
`types.attrTag` *`{ attr1 = option1; attr2 = option2; ... }`*
|
||||
|
||||
: An attribute set containing one attribute, whose name must be picked from
|
||||
the attribute set (`attr1`, etc) and whose value consists of definitions that are valid for the corresponding option (`option1`, etc).
|
||||
|
||||
This type appears in the documentation as _attribute-tagged union_.
|
||||
|
||||
Example:
|
||||
|
||||
```nix
|
||||
{ lib, ... }:
|
||||
let inherit (lib) type mkOption;
|
||||
in {
|
||||
options.toyRouter.rules = mkOption {
|
||||
description = ''
|
||||
Rules for a fictional packet routing service.
|
||||
'';
|
||||
type = types.attrsOf (
|
||||
types.attrTag {
|
||||
bounce = mkOption {
|
||||
description = "Send back a packet explaining why it wasn't forwarded.";
|
||||
type = types.submodule {
|
||||
options.errorMessage = mkOption { … };
|
||||
};
|
||||
};
|
||||
forward = mkOption {
|
||||
description = "Forward the packet.";
|
||||
type = types.submodule {
|
||||
options.destination = mkOption { … };
|
||||
};
|
||||
};
|
||||
ignore = types.mkOption {
|
||||
description = "Drop the packet without sending anything back.";
|
||||
type = types.submodule {};
|
||||
};
|
||||
});
|
||||
};
|
||||
config.toyRouter.rules = {
|
||||
http = {
|
||||
bounce = {
|
||||
errorMessage = "Unencrypted HTTP is banned. You must always use https://.";
|
||||
};
|
||||
};
|
||||
ssh = { drop = {}; };
|
||||
};
|
||||
}
|
||||
```
|
||||
|
||||
## Composed types {#sec-option-types-composed}
|
||||
|
||||
Composed types are types that take a type as parameter. `listOf
|
||||
|
@ -318,11 +399,6 @@ Composed types are types that take a type as parameter. `listOf
|
|||
returned instead for the same `mkIf false` definition.
|
||||
:::
|
||||
|
||||
`types.nullOr` *`t`*
|
||||
|
||||
: `null` or type *`t`*. Multiple definitions are merged according to
|
||||
type *`t`*.
|
||||
|
||||
`types.uniq` *`t`*
|
||||
|
||||
: Ensures that type *`t`* cannot be merged. It is used to ensure option
|
||||
|
@ -334,17 +410,6 @@ Composed types are types that take a type as parameter. `listOf
|
|||
the line `The option <option path> is defined multiple times.` and before
|
||||
a list of definition locations.
|
||||
|
||||
`types.either` *`t1 t2`*
|
||||
|
||||
: Type *`t1`* or type *`t2`*, e.g. `with types; either int str`.
|
||||
Multiple definitions cannot be merged.
|
||||
|
||||
`types.oneOf` \[ *`t1 t2`* ... \]
|
||||
|
||||
: Type *`t1`* or type *`t2`* and so forth, e.g.
|
||||
`with types; oneOf [ int str bool ]`. Multiple definitions cannot be
|
||||
merged.
|
||||
|
||||
`types.coercedTo` *`from f to`*
|
||||
|
||||
: Type *`to`* or type *`from`* which will be coerced to type *`to`* using
|
||||
|
|
Loading…
Reference in a new issue