Merge pull request #191353 from roberth/types-parentheses
lib.types: Add parentheses where description is ambiguous
This commit is contained in:
commit
f3810cc33a
2 changed files with 122 additions and 13 deletions
|
@ -1206,4 +1206,57 @@ runTests {
|
||||||
expr = strings.levenshteinAtMost 3 "hello" "Holla";
|
expr = strings.levenshteinAtMost 3 "hello" "Holla";
|
||||||
expected = true;
|
expected = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
testTypeDescriptionInt = {
|
||||||
|
expr = (with types; int).description;
|
||||||
|
expected = "signed integer";
|
||||||
|
};
|
||||||
|
testTypeDescriptionListOfInt = {
|
||||||
|
expr = (with types; listOf int).description;
|
||||||
|
expected = "list of signed integer";
|
||||||
|
};
|
||||||
|
testTypeDescriptionListOfListOfInt = {
|
||||||
|
expr = (with types; listOf (listOf int)).description;
|
||||||
|
expected = "list of list of signed integer";
|
||||||
|
};
|
||||||
|
testTypeDescriptionListOfEitherStrOrBool = {
|
||||||
|
expr = (with types; listOf (either str bool)).description;
|
||||||
|
expected = "list of (string or boolean)";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherListOfStrOrBool = {
|
||||||
|
expr = (with types; either (listOf bool) str).description;
|
||||||
|
expected = "(list of boolean) or string";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherStrOrListOfBool = {
|
||||||
|
expr = (with types; either str (listOf bool)).description;
|
||||||
|
expected = "string or list of boolean";
|
||||||
|
};
|
||||||
|
testTypeDescriptionOneOfListOfStrOrBool = {
|
||||||
|
expr = (with types; oneOf [ (listOf bool) str ]).description;
|
||||||
|
expected = "(list of boolean) or string";
|
||||||
|
};
|
||||||
|
testTypeDescriptionOneOfListOfStrOrBoolOrNumber = {
|
||||||
|
expr = (with types; oneOf [ (listOf bool) str number ]).description;
|
||||||
|
expected = "(list of boolean) or string or signed integer or floating point number";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherListOfBoolOrEitherStringOrNumber = {
|
||||||
|
expr = (with types; either (listOf bool) (either str number)).description;
|
||||||
|
expected = "(list of boolean) or string or signed integer or floating point number";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherEitherListOfBoolOrStringOrNumber = {
|
||||||
|
expr = (with types; either (either (listOf bool) str) number).description;
|
||||||
|
expected = "(list of boolean) or string or signed integer or floating point number";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherNullOrBoolOrString = {
|
||||||
|
expr = (with types; either (nullOr bool) str).description;
|
||||||
|
expected = "null or boolean or string";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherListOfEitherBoolOrStrOrInt = {
|
||||||
|
expr = (with types; either (listOf (either bool str)) int).description;
|
||||||
|
expected = "(list of (boolean or string)) or signed integer";
|
||||||
|
};
|
||||||
|
testTypeDescriptionEitherIntOrListOrEitherBoolOrStr = {
|
||||||
|
expr = (with types; either int (listOf (either bool str))).description;
|
||||||
|
expected = "signed integer or list of (boolean or string)";
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -113,6 +113,12 @@ rec {
|
||||||
name
|
name
|
||||||
, # Description of the type, defined recursively by embedding the wrapped type if any.
|
, # Description of the type, defined recursively by embedding the wrapped type if any.
|
||||||
description ? null
|
description ? null
|
||||||
|
# A hint for whether or not this description needs parentheses. Possible values:
|
||||||
|
# - "noun": a simple noun phrase such as "positive integer"
|
||||||
|
# - "conjunction": a phrase with a potentially ambiguous "or" connective.
|
||||||
|
# - "composite": a phrase with an "of" connective
|
||||||
|
# See the `optionDescriptionPhrase` function.
|
||||||
|
, descriptionClass ? null
|
||||||
, # Function applied to each definition that should return true if
|
, # Function applied to each definition that should return true if
|
||||||
# its type-correct, false otherwise.
|
# its type-correct, false otherwise.
|
||||||
check ? (x: true)
|
check ? (x: true)
|
||||||
|
@ -158,10 +164,36 @@ rec {
|
||||||
nestedTypes ? {}
|
nestedTypes ? {}
|
||||||
}:
|
}:
|
||||||
{ _type = "option-type";
|
{ _type = "option-type";
|
||||||
inherit name check merge emptyValue getSubOptions getSubModules substSubModules typeMerge functor deprecationMessage nestedTypes;
|
inherit
|
||||||
|
name check merge emptyValue getSubOptions getSubModules substSubModules
|
||||||
|
typeMerge functor deprecationMessage nestedTypes descriptionClass;
|
||||||
description = if description == null then name else description;
|
description = if description == null then name else description;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
# optionDescriptionPhrase :: (str -> bool) -> optionType -> str
|
||||||
|
#
|
||||||
|
# Helper function for producing unambiguous but readable natural language
|
||||||
|
# descriptions of types.
|
||||||
|
#
|
||||||
|
# Parameters
|
||||||
|
#
|
||||||
|
# optionDescriptionPhase unparenthesize optionType
|
||||||
|
#
|
||||||
|
# `unparenthesize`: A function from descriptionClass string to boolean.
|
||||||
|
# It must return true when the class of phrase will fit unambiguously into
|
||||||
|
# the description of the caller.
|
||||||
|
#
|
||||||
|
# `optionType`: The option type to parenthesize or not.
|
||||||
|
# The option whose description we're returning.
|
||||||
|
#
|
||||||
|
# Return value
|
||||||
|
#
|
||||||
|
# The description of the `optionType`, with parentheses if there may be an
|
||||||
|
# ambiguity.
|
||||||
|
optionDescriptionPhrase = unparenthesize: t:
|
||||||
|
if unparenthesize (t.descriptionClass or null)
|
||||||
|
then t.description
|
||||||
|
else "(${t.description})";
|
||||||
|
|
||||||
# When adding new types don't forget to document them in
|
# When adding new types don't forget to document them in
|
||||||
# nixos/doc/manual/development/option-types.xml!
|
# nixos/doc/manual/development/option-types.xml!
|
||||||
|
@ -170,6 +202,7 @@ rec {
|
||||||
raw = mkOptionType rec {
|
raw = mkOptionType rec {
|
||||||
name = "raw";
|
name = "raw";
|
||||||
description = "raw value";
|
description = "raw value";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = value: true;
|
check = value: true;
|
||||||
merge = mergeOneOption;
|
merge = mergeOneOption;
|
||||||
};
|
};
|
||||||
|
@ -177,6 +210,7 @@ rec {
|
||||||
anything = mkOptionType {
|
anything = mkOptionType {
|
||||||
name = "anything";
|
name = "anything";
|
||||||
description = "anything";
|
description = "anything";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = value: true;
|
check = value: true;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
let
|
let
|
||||||
|
@ -216,12 +250,14 @@ rec {
|
||||||
};
|
};
|
||||||
|
|
||||||
unspecified = mkOptionType {
|
unspecified = mkOptionType {
|
||||||
name = "unspecified";
|
name = "unspecified value";
|
||||||
|
descriptionClass = "noun";
|
||||||
};
|
};
|
||||||
|
|
||||||
bool = mkOptionType {
|
bool = mkOptionType {
|
||||||
name = "bool";
|
name = "bool";
|
||||||
description = "boolean";
|
description = "boolean";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = isBool;
|
check = isBool;
|
||||||
merge = mergeEqualOption;
|
merge = mergeEqualOption;
|
||||||
};
|
};
|
||||||
|
@ -229,6 +265,7 @@ rec {
|
||||||
int = mkOptionType {
|
int = mkOptionType {
|
||||||
name = "int";
|
name = "int";
|
||||||
description = "signed integer";
|
description = "signed integer";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = isInt;
|
check = isInt;
|
||||||
merge = mergeEqualOption;
|
merge = mergeEqualOption;
|
||||||
};
|
};
|
||||||
|
@ -294,6 +331,7 @@ rec {
|
||||||
float = mkOptionType {
|
float = mkOptionType {
|
||||||
name = "float";
|
name = "float";
|
||||||
description = "floating point number";
|
description = "floating point number";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = isFloat;
|
check = isFloat;
|
||||||
merge = mergeEqualOption;
|
merge = mergeEqualOption;
|
||||||
};
|
};
|
||||||
|
@ -325,6 +363,7 @@ rec {
|
||||||
str = mkOptionType {
|
str = mkOptionType {
|
||||||
name = "str";
|
name = "str";
|
||||||
description = "string";
|
description = "string";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = isString;
|
check = isString;
|
||||||
merge = mergeEqualOption;
|
merge = mergeEqualOption;
|
||||||
};
|
};
|
||||||
|
@ -332,6 +371,7 @@ rec {
|
||||||
nonEmptyStr = mkOptionType {
|
nonEmptyStr = mkOptionType {
|
||||||
name = "nonEmptyStr";
|
name = "nonEmptyStr";
|
||||||
description = "non-empty string";
|
description = "non-empty string";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = x: str.check x && builtins.match "[ \t\n]*" x == null;
|
check = x: str.check x && builtins.match "[ \t\n]*" x == null;
|
||||||
inherit (str) merge;
|
inherit (str) merge;
|
||||||
};
|
};
|
||||||
|
@ -344,6 +384,7 @@ rec {
|
||||||
mkOptionType {
|
mkOptionType {
|
||||||
name = "singleLineStr";
|
name = "singleLineStr";
|
||||||
description = "(optionally newline-terminated) single-line string";
|
description = "(optionally newline-terminated) single-line string";
|
||||||
|
descriptionClass = "noun";
|
||||||
inherit check;
|
inherit check;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
lib.removeSuffix "\n" (merge loc defs);
|
lib.removeSuffix "\n" (merge loc defs);
|
||||||
|
@ -352,6 +393,7 @@ rec {
|
||||||
strMatching = pattern: mkOptionType {
|
strMatching = pattern: mkOptionType {
|
||||||
name = "strMatching ${escapeNixString pattern}";
|
name = "strMatching ${escapeNixString pattern}";
|
||||||
description = "string matching the pattern ${pattern}";
|
description = "string matching the pattern ${pattern}";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = x: str.check x && builtins.match pattern x != null;
|
check = x: str.check x && builtins.match pattern x != null;
|
||||||
inherit (str) merge;
|
inherit (str) merge;
|
||||||
};
|
};
|
||||||
|
@ -364,6 +406,7 @@ rec {
|
||||||
then "Concatenated string" # for types.string.
|
then "Concatenated string" # for types.string.
|
||||||
else "strings concatenated with ${builtins.toJSON sep}"
|
else "strings concatenated with ${builtins.toJSON sep}"
|
||||||
;
|
;
|
||||||
|
descriptionClass = "noun";
|
||||||
check = isString;
|
check = isString;
|
||||||
merge = loc: defs: concatStringsSep sep (getValues defs);
|
merge = loc: defs: concatStringsSep sep (getValues defs);
|
||||||
functor = (defaultFunctor name) // {
|
functor = (defaultFunctor name) // {
|
||||||
|
@ -387,7 +430,7 @@ rec {
|
||||||
|
|
||||||
passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
|
passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
|
||||||
name = "passwdEntry ${entryType.name}";
|
name = "passwdEntry ${entryType.name}";
|
||||||
description = "${entryType.description}, not containing newlines or colons";
|
description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons";
|
||||||
};
|
};
|
||||||
|
|
||||||
attrs = mkOptionType {
|
attrs = mkOptionType {
|
||||||
|
@ -407,6 +450,7 @@ rec {
|
||||||
# ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
|
# ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
|
||||||
package = mkOptionType {
|
package = mkOptionType {
|
||||||
name = "package";
|
name = "package";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = x: isDerivation x || isStorePath x;
|
check = x: isDerivation x || isStorePath x;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
let res = mergeOneOption loc defs;
|
let res = mergeOneOption loc defs;
|
||||||
|
@ -427,7 +471,8 @@ rec {
|
||||||
|
|
||||||
listOf = elemType: mkOptionType rec {
|
listOf = elemType: mkOptionType rec {
|
||||||
name = "listOf";
|
name = "listOf";
|
||||||
description = "list of ${elemType.description}";
|
description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
|
||||||
|
descriptionClass = "composite";
|
||||||
check = isList;
|
check = isList;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
|
map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
|
||||||
|
@ -450,13 +495,14 @@ rec {
|
||||||
nonEmptyListOf = elemType:
|
nonEmptyListOf = elemType:
|
||||||
let list = addCheck (types.listOf elemType) (l: l != []);
|
let list = addCheck (types.listOf elemType) (l: l != []);
|
||||||
in list // {
|
in list // {
|
||||||
description = "non-empty " + list.description;
|
description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
|
||||||
emptyValue = { }; # no .value attr, meaning unset
|
emptyValue = { }; # no .value attr, meaning unset
|
||||||
};
|
};
|
||||||
|
|
||||||
attrsOf = elemType: mkOptionType rec {
|
attrsOf = elemType: mkOptionType rec {
|
||||||
name = "attrsOf";
|
name = "attrsOf";
|
||||||
description = "attribute set of ${elemType.description}";
|
description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
|
||||||
|
descriptionClass = "composite";
|
||||||
check = isAttrs;
|
check = isAttrs;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
|
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
|
||||||
|
@ -479,7 +525,8 @@ rec {
|
||||||
# error that it's not defined. Use only if conditional definitions don't make sense.
|
# error that it's not defined. Use only if conditional definitions don't make sense.
|
||||||
lazyAttrsOf = elemType: mkOptionType rec {
|
lazyAttrsOf = elemType: mkOptionType rec {
|
||||||
name = "lazyAttrsOf";
|
name = "lazyAttrsOf";
|
||||||
description = "lazy attribute set of ${elemType.description}";
|
description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
|
||||||
|
descriptionClass = "composite";
|
||||||
check = isAttrs;
|
check = isAttrs;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
zipAttrsWith (name: defs:
|
zipAttrsWith (name: defs:
|
||||||
|
@ -509,7 +556,7 @@ rec {
|
||||||
# Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
|
# Value of given type but with no merging (i.e. `uniq list`s are not concatenated).
|
||||||
uniq = elemType: mkOptionType rec {
|
uniq = elemType: mkOptionType rec {
|
||||||
name = "uniq";
|
name = "uniq";
|
||||||
inherit (elemType) description check;
|
inherit (elemType) description descriptionClass check;
|
||||||
merge = mergeOneOption;
|
merge = mergeOneOption;
|
||||||
emptyValue = elemType.emptyValue;
|
emptyValue = elemType.emptyValue;
|
||||||
getSubOptions = elemType.getSubOptions;
|
getSubOptions = elemType.getSubOptions;
|
||||||
|
@ -521,7 +568,7 @@ rec {
|
||||||
|
|
||||||
unique = { message }: type: mkOptionType rec {
|
unique = { message }: type: mkOptionType rec {
|
||||||
name = "unique";
|
name = "unique";
|
||||||
inherit (type) description check;
|
inherit (type) description descriptionClass check;
|
||||||
merge = mergeUniqueOption { inherit message; };
|
merge = mergeUniqueOption { inherit message; };
|
||||||
emptyValue = type.emptyValue;
|
emptyValue = type.emptyValue;
|
||||||
getSubOptions = type.getSubOptions;
|
getSubOptions = type.getSubOptions;
|
||||||
|
@ -534,7 +581,8 @@ rec {
|
||||||
# Null or value of ...
|
# Null or value of ...
|
||||||
nullOr = elemType: mkOptionType rec {
|
nullOr = elemType: mkOptionType rec {
|
||||||
name = "nullOr";
|
name = "nullOr";
|
||||||
description = "null or ${elemType.description}";
|
description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}";
|
||||||
|
descriptionClass = "conjunction";
|
||||||
check = x: x == null || elemType.check x;
|
check = x: x == null || elemType.check x;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
let nrNulls = count (def: def.value == null) defs; in
|
let nrNulls = count (def: def.value == null) defs; in
|
||||||
|
@ -552,7 +600,8 @@ rec {
|
||||||
|
|
||||||
functionTo = elemType: mkOptionType {
|
functionTo = elemType: mkOptionType {
|
||||||
name = "functionTo";
|
name = "functionTo";
|
||||||
description = "function that evaluates to a(n) ${elemType.description}";
|
description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
|
||||||
|
descriptionClass = "composite";
|
||||||
check = isFunction;
|
check = isFunction;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
fnArgs: (mergeDefinitions (loc ++ [ "[function body]" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
|
fnArgs: (mergeDefinitions (loc ++ [ "[function body]" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
|
||||||
|
@ -578,6 +627,7 @@ rec {
|
||||||
deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
|
deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
|
||||||
name = "deferredModule";
|
name = "deferredModule";
|
||||||
description = "module";
|
description = "module";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = x: isAttrs x || isFunction x || path.check x;
|
check = x: isAttrs x || isFunction x || path.check x;
|
||||||
merge = loc: defs: {
|
merge = loc: defs: {
|
||||||
imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
|
imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
|
||||||
|
@ -603,6 +653,7 @@ rec {
|
||||||
optionType = mkOptionType {
|
optionType = mkOptionType {
|
||||||
name = "optionType";
|
name = "optionType";
|
||||||
description = "optionType";
|
description = "optionType";
|
||||||
|
descriptionClass = "noun";
|
||||||
check = value: value._type or null == "option-type";
|
check = value: value._type or null == "option-type";
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
if length defs == 1
|
if length defs == 1
|
||||||
|
@ -749,6 +800,10 @@ rec {
|
||||||
"value ${show (builtins.head values)} (singular enum)"
|
"value ${show (builtins.head values)} (singular enum)"
|
||||||
else
|
else
|
||||||
"one of ${concatMapStringsSep ", " show values}";
|
"one of ${concatMapStringsSep ", " show values}";
|
||||||
|
descriptionClass =
|
||||||
|
if builtins.length values < 2
|
||||||
|
then "noun"
|
||||||
|
else "conjunction";
|
||||||
check = flip elem values;
|
check = flip elem values;
|
||||||
merge = mergeEqualOption;
|
merge = mergeEqualOption;
|
||||||
functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
|
functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
|
||||||
|
@ -757,7 +812,8 @@ rec {
|
||||||
# Either value of type `t1` or `t2`.
|
# Either value of type `t1` or `t2`.
|
||||||
either = t1: t2: mkOptionType rec {
|
either = t1: t2: mkOptionType rec {
|
||||||
name = "either";
|
name = "either";
|
||||||
description = "${t1.description} or ${t2.description}";
|
description = "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
|
||||||
|
descriptionClass = "conjunction";
|
||||||
check = x: t1.check x || t2.check x;
|
check = x: t1.check x || t2.check x;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
let
|
let
|
||||||
|
@ -795,7 +851,7 @@ rec {
|
||||||
coercedType.description})";
|
coercedType.description})";
|
||||||
mkOptionType rec {
|
mkOptionType rec {
|
||||||
name = "coercedTo";
|
name = "coercedTo";
|
||||||
description = "${finalType.description} or ${coercedType.description} convertible to it";
|
description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it";
|
||||||
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
|
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
|
||||||
merge = loc: defs:
|
merge = loc: defs:
|
||||||
let
|
let
|
||||||
|
|
Loading…
Reference in a new issue