nixpkgs/lib/types.nix

Ignoring revisions in .git-blame-ignore-revs. Click here to bypass and see the normal blame view.

1478 lines
51 KiB
Nix
Raw Normal View History

# Definitions related to run-time type checking. Used in particular
# to type-check NixOS configurations.
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
{ lib }:
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
let
inherit (lib)
elem
flip
isAttrs
isBool
isDerivation
isFloat
isFunction
isInt
isList
isString
isStorePath
throwIf
toDerivation
toList
;
inherit (lib.lists)
all
concatLists
count
elemAt
filter
foldl'
head
imap1
last
length
tail
;
inherit (lib.attrsets)
attrNames
filterAttrs
hasAttr
mapAttrs
optionalAttrs
zipAttrsWith
;
inherit (lib.options)
getFiles
getValues
mergeDefaultOption
mergeEqualOption
mergeOneOption
mergeUniqueOption
showFiles
showOption
;
inherit (lib.strings)
concatMapStringsSep
concatStringsSep
escapeNixString
hasInfix
isStringLike
;
inherit (lib.trivial)
boolToString
;
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
inherit (lib.modules)
mergeDefinitions
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}";
# Internal functor to help for migrating functor.wrapped to functor.payload.elemType
# Note that individual attributes can be overriden if needed.
elemTypeFunctor =
name:
{ elemType, ... }@payload:
{
inherit name payload;
wrappedDeprecationMessage = makeWrappedDeprecationMessage payload;
type = outer_types.types.${name};
binOp =
a: b:
let
merged = a.elemType.typeMerge b.elemType.functor;
in
if merged == null then null else { elemType = merged; };
};
makeWrappedDeprecationMessage =
payload:
{ loc }:
lib.warn ''
The deprecated `${lib.optionalString (loc != null) "type."}functor.wrapped` attribute ${
lib.optionalString (loc != null) "of the option `${showOption loc}` "
}is accessed, use `${lib.optionalString (loc != null) "type."}nestedTypes.elemType` instead.
'' payload.elemType;
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
outer_types = rec {
isType = type: x: (x._type or "") == type;
setType =
typeName: value:
value
// {
_type = typeName;
};
2016-09-07 10:03:32 +09:00
# Default type merging function
# takes two type functors and return the merged type
defaultTypeMerge =
f: f':
let
mergedWrapped = f.wrapped.typeMerge f'.wrapped.functor;
mergedPayload = f.binOp f.payload f'.payload;
hasPayload =
assert (f'.payload != null) == (f.payload != null);
f.payload != null;
hasWrapped =
assert (f'.wrapped != null) == (f.wrapped != null);
f.wrapped != null;
typeFromPayload = if mergedPayload == null then null else f.type mergedPayload;
typeFromWrapped = if mergedWrapped == null then null else f.type mergedWrapped;
in
# Abort early: cannot merge different types
if f.name != f'.name then
null
else
if hasPayload then
2016-09-07 10:03:32 +09:00
# Just return the payload if returning wrapped is deprecated
if f ? wrappedDeprecationMessage then
typeFromPayload
else if hasWrapped then
# Has both wrapped and payload
throw ''
2016-09-07 10:03:32 +09:00
Type ${f.name} defines both `functor.payload` and `functor.wrapped` at the same time, which is not supported.
Use either `functor.payload` or `functor.wrapped` but not both.
If your code worked before remove either `functor.wrapped` or `functor.payload` from the type definition.
''
else
typeFromPayload
else if hasWrapped then
typeFromWrapped
else
f.type;
2016-09-07 10:03:32 +09:00
# Default type functor
defaultFunctor = name: {
inherit name;
type = types.${name} or null;
2016-09-07 10:03:32 +09:00
wrapped = null;
payload = null;
binOp = a: b: null;
};
isOptionType = isType "option-type";
mkOptionType =
{
# Human-readable representation of the type, should be equivalent to
# the type function name.
name,
# Description of the type, defined recursively by embedding the wrapped type if any.
description ? null,
# A hint for whether or not this description needs parentheses. Possible values:
# - "noun": a noun phrase
# Example description: "positive integer",
# - "conjunction": a phrase with a potentially ambiguous "or" connective
# Example description: "int or string"
# - "composite": a phrase with an "of" connective
# Example description: "list of string"
# - "nonRestrictiveClause": a noun followed by a comma and a clause
# Example description: "positive integer, meaning >0"
# See the `optionDescriptionPhrase` function.
descriptionClass ? null,
# DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
# Function applied to each definition that must return false when a definition
# does not match the type. It should not check more than the root of the value,
# because checking nested values reduces laziness, leading to unnecessary
# infinite recursions in the module system.
# Further checks of nested values should be performed by throwing in
# the merge function.
# Strict and deep type checking can be performed by calling lib.deepSeq on
# the merged value.
#
# See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change,
# https://github.com/NixOS/nixpkgs/pull/173568 and
# https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this,
# https://github.com/NixOS/nixpkgs/issues/191124 and
# https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore
# this disclaimer.
check ? (x: true),
# Merge a list of definitions together into a single value.
# This function is called with two arguments: the location of
# the option in the configuration as a list of strings
# (e.g. ["boot" "loader "grub" "enable"]), and a list of
# definition values and locations (e.g. [ { file = "/foo.nix";
# value = 1; } { file = "/bar.nix"; value = 2 } ]).
merge ? mergeDefaultOption,
# Whether this type has a value representing nothingness. If it does,
# this should be a value of the form { value = <the nothing value>; }
# 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 attrset of sub-options. Used to generate
# documentation.
getSubOptions ? prefix: { },
# List of modules if any, or null if none.
getSubModules ? null,
# Function for building the same option type with a different list of
# modules.
substSubModules ? m: null,
# Function that merge type declarations.
2016-09-07 10:03:32 +09:00
# internal, takes a functor as argument and returns the merged type.
# returning null means the type is not mergeable
typeMerge ? defaultTypeMerge functor,
# The type functor.
# internal, representation of the type as an attribute set.
# name: name of the type
# type: type function.
# wrapped: the type wrapped in case of compound types.
# payload: values of the type, two payloads of the same type must be
2016-09-07 10:03:32 +09:00
# combinable with the binOp binary operation.
# binOp: binary operation that merge two payloads of the same type.
functor ? defaultFunctor name,
# The deprecation message to display when this type is used by an option
# If null, the type isn't deprecated
deprecationMessage ? null,
# The types that occur in the definition of this type. This is used to
# issue deprecation warnings recursively. Can also be used to reuse
# nested types
nestedTypes ? { },
}:
2023-06-22 19:00:43 -04:00
{
_type = "option-type";
inherit
name
check
merge
emptyValue
getSubOptions
getSubModules
substSubModules
typeMerge
deprecationMessage
nestedTypes
descriptionClass
;
functor =
if functor ? wrappedDeprecationMessage then
functor
// {
wrapped = functor.wrappedDeprecationMessage {
loc = null;
};
}
else
functor;
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
# nixos/doc/manual/development/option-types.section.md!
types = rec {
raw = mkOptionType {
name = "raw";
description = "raw value";
descriptionClass = "noun";
check = value: true;
merge = mergeOneOption;
};
anything = mkOptionType {
name = "anything";
description = "anything";
descriptionClass = "noun";
check = value: true;
merge =
loc: defs:
let
getType =
2024-01-28 00:30:36 +01:00
value: if isAttrs value && isStringLike value then "stringCoercibleSet" else builtins.typeOf value;
2024-01-28 00:30:36 +01:00
# Returns the common type of all definitions, throws an error if they
# don't have the same type
commonType = foldl' (
type: def:
if getType def.value == type then
type
else
2024-01-28 00:30:36 +01:00
throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
) (getType (head defs).value) defs;
mergeFunction =
{
# Recursively merge attribute sets
set = (attrsOf anything).merge;
# This is the type of packages, only accept a single definition
2024-01-28 00:30:36 +01:00
stringCoercibleSet = mergeOneOption;
lambda =
loc: defs: arg:
anything.merge (loc ++ [ "<function body>" ]) (
2024-11-09 11:22:25 +01:00
map (def: {
file = def.file;
value = def.value arg;
}) defs
);
# Otherwise fall back to only allowing all equal definitions
}
.${commonType} or mergeEqualOption;
in
mergeFunction loc defs;
};
unspecified = mkOptionType {
name = "unspecified";
2024-01-28 00:30:36 +01:00
description = "unspecified value";
descriptionClass = "noun";
};
bool = mkOptionType {
2016-09-07 10:03:32 +09:00
name = "bool";
description = "boolean";
descriptionClass = "noun";
2013-11-12 13:48:19 +01:00
check = isBool;
merge = mergeEqualOption;
};
boolByOr = mkOptionType {
name = "boolByOr";
description = "boolean (merged using or)";
descriptionClass = "noun";
check = isBool;
merge =
loc: defs:
foldl' (
result: def:
# Under the assumption that .check always runs before merge, we can assume that all defs.*.value
# have been forced, and therefore we assume we don't introduce order-dependent strictness here
result || def.value
) false defs;
};
2019-08-13 21:52:01 +00:00
int = mkOptionType {
name = "int";
description = "signed integer";
descriptionClass = "noun";
check = isInt;
merge = mergeEqualOption;
};
# Specialized subdomains of int
ints =
let
2017-08-05 15:13:55 +02:00
betweenDesc = lowest: highest: "${toString lowest} and ${toString highest} (both inclusive)";
between =
lowest: highest:
assert lib.assertMsg (lowest <= highest) "ints.between: lowest must be smaller than highest";
addCheck int (x: x >= lowest && x <= highest)
// {
name = "intBetween";
description = "integer between ${betweenDesc lowest highest}";
};
ign =
lowest: highest: name: docStart:
between lowest highest
// {
inherit name;
description = docStart + "; between ${betweenDesc lowest highest}";
};
unsign =
bit: range: ign 0 (range - 1) "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
sign =
bit: range:
ign (0 - (range / 2)) (
range / 2 - 1
) "signedInt${toString bit}" "${toString bit} bit signed integer";
in
{
# TODO: Deduplicate with docs in nixos/doc/manual/development/option-types.section.md
/**
An int with a fixed range.
# Example
:::{.example}
## `lib.types.ints.between` usage example
```nix
(ints.between 0 100).check (-1)
=> false
(ints.between 0 100).check (101)
=> false
(ints.between 0 0).check 0
=> true
```
:::
*/
inherit between;
unsigned = addCheck types.int (x: x >= 0) // {
name = "unsignedInt";
description = "unsigned integer, meaning >=0";
descriptionClass = "nonRestrictiveClause";
};
positive = addCheck types.int (x: x > 0) // {
name = "positiveInt";
description = "positive integer, meaning >0";
descriptionClass = "nonRestrictiveClause";
};
u8 = unsign 8 256;
u16 = unsign 16 65536;
# the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
# the smallest int Nix accepts is -2^63 (-9223372036854775807)
u32 = unsign 32 4294967296;
# u64 = unsign 64 18446744073709551616;
s8 = sign 8 256;
s16 = sign 16 65536;
s32 = sign 32 4294967296;
};
2018-10-18 23:39:13 +02:00
# Alias of u16 for a port number
port = ints.u16;
2019-08-13 21:52:01 +00:00
float = mkOptionType {
name = "float";
description = "floating point number";
descriptionClass = "noun";
check = isFloat;
merge = mergeEqualOption;
};
2018-06-30 21:13:49 +02:00
2013-11-12 13:48:19 +01:00
number = either int float;
2021-07-20 05:17:35 +03:00
numbers =
let
2021-07-20 05:17:35 +03:00
betweenDesc =
lowest: highest: "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)";
in
{
2021-07-20 05:17:35 +03:00
between =
lowest: highest:
2021-07-20 05:17:35 +03:00
assert lib.assertMsg (lowest <= highest) "numbers.between: lowest must be smaller than highest";
addCheck number (x: x >= lowest && x <= highest)
// {
2021-07-20 05:17:35 +03:00
name = "numberBetween";
description = "integer or floating point number between ${betweenDesc lowest highest}";
};
nonnegative = addCheck number (x: x >= 0) // {
name = "numberNonnegative";
description = "nonnegative integer or floating point number, meaning >=0";
descriptionClass = "nonRestrictiveClause";
};
positive = addCheck number (x: x > 0) // {
name = "numberPositive";
description = "positive integer or floating point number, meaning >0";
descriptionClass = "nonRestrictiveClause";
};
};
str = mkOptionType {
name = "str";
description = "string";
descriptionClass = "noun";
check = isString;
merge = mergeEqualOption;
};
2017-12-02 13:04:16 -05:00
nonEmptyStr = mkOptionType {
name = "nonEmptyStr";
description = "non-empty string";
descriptionClass = "noun";
2017-12-02 13:04:16 -05:00
check = x: str.check x && builtins.match "[ \t\n]*" x == null;
inherit (str) merge;
};
2017-12-02 13:04:16 -05:00
# Allow a newline character at the end and trim it in the merge function.
2016-09-07 10:03:32 +09:00
singleLineStr =
let
2016-09-07 10:03:32 +09:00
inherit (strMatching "[^\n\r]*\n?") check merge;
in
2016-09-07 10:03:32 +09:00
mkOptionType {
name = "singleLineStr";
description = "(optionally newline-terminated) single-line string";
descriptionClass = "noun";
2013-11-12 13:48:19 +01:00
inherit check;
merge = loc: defs: lib.removeSuffix "\n" (merge loc defs);
};
strMatching =
pattern:
mkOptionType {
name = "strMatching ${escapeNixString pattern}";
description = "string matching the pattern ${pattern}";
descriptionClass = "noun";
check = x: str.check x && builtins.match pattern x != null;
inherit (str) merge;
functor = defaultFunctor "strMatching" // {
type = payload: strMatching payload.pattern;
payload = { inherit pattern; };
binOp = lhs: rhs: if lhs == rhs then lhs else null;
};
};
# Merge multiple definitions by concatenating them (with the given
# separator between the values).
separatedString =
sep:
mkOptionType rec {
2016-09-07 10:03:32 +09:00
name = "separatedString";
2023-05-07 15:27:05 +02:00
description =
if sep == "" then
"Concatenated string" # for types.string.
2016-09-07 10:03:32 +09:00
else
"strings concatenated with ${builtins.toJSON sep}";
descriptionClass = "noun";
check = isString;
merge = loc: defs: concatStringsSep sep (getValues defs);
functor = (defaultFunctor name) // {
payload = { inherit sep; };
type = payload: types.separatedString payload.sep;
binOp = lhs: rhs: if lhs.sep == rhs.sep then { inherit (lhs) sep; } else null;
};
};
lines = separatedString "\n";
commas = separatedString ",";
envVar = separatedString ":";
# Deprecated; should not be used because it quietly concatenates
# strings, which is usually not what you want.
# We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string`
string =
lib.warn
"The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."
(
separatedString ""
// {
name = "string";
}
);
passwdEntry =
entryType:
addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str))
// {
2023-06-12 13:02:00 +02:00
name = "passwdEntry ${entryType.name}";
description = "${
optionDescriptionPhrase (class: class == "noun") entryType
}, not containing newlines or colons";
descriptionClass = "nonRestrictiveClause";
};
attrs = mkOptionType {
name = "attrs";
description = "attribute set";
check = isAttrs;
merge = loc: foldl' (res: def: res // def.value) { };
emptyValue = {
value = { };
};
};
2023-05-07 15:27:05 +02:00
# A package is a top-level store path (/nix/store/hash-name). This includes:
# - derivations
2023-05-07 15:27:05 +02:00
# - more generally, attribute sets with an `outPath` or `__toString` attribute
# pointing to a store path, e.g. flake inputs
2023-05-07 15:27:05 +02:00
# - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo)
# - hardcoded store path literals (/nix/store/hash-foo) or strings without context
# ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
# If you don't need a *top-level* store path, consider using pathInStore instead.
package = mkOptionType {
name = "package";
descriptionClass = "noun";
check = x: isDerivation x || isStorePath x;
merge =
loc: defs:
2023-05-07 15:27:05 +02:00
let
res = mergeOneOption loc defs;
in
if builtins.isPath res || (builtins.isString res && !builtins.hasContext res) then
toDerivation res
else
res;
};
shellPackage = package // {
check = x: isDerivation x && hasAttr "shellPath" x;
2023-06-12 13:02:00 +02:00
};
pkgs = addCheck (
unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs
// {
name = "pkgs";
descriptionClass = "noun";
2024-11-09 11:22:25 +01:00
description = "Nixpkgs package set";
}
2023-05-07 15:27:05 +02:00
) (x: (x._type or null) == "pkgs");
path = pathWith {
absolute = true;
};
pathInStore = pathWith {
inStore = true;
};
pathWith =
{
inStore ? null,
absolute ? null,
}:
throwIf (inStore != null && absolute != null && inStore && !absolute)
"In pathWith, inStore means the path must be absolute"
2016-09-07 10:03:32 +09:00
mkOptionType
{
name = "path";
description = (
(if absolute == null then "" else (if absolute then "absolute " else "relative "))
+ "path"
+ (
if inStore == null then "" else (if inStore then " in the Nix store" else " not in the Nix store")
)
);
descriptionClass = "noun";
merge = mergeEqualOption;
functor = defaultFunctor "path" // {
type = pathWith;
payload = { inherit inStore absolute; };
binOp = lhs: rhs: if lhs == rhs then lhs else null;
};
2018-02-08 09:49:06 +01:00
check =
x:
let
isInStore = builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
isAbsolute = builtins.substring 0 1 (toString x) == "/";
2018-02-08 09:49:06 +01:00
isExpectedType = (
if inStore == null || inStore then isStringLike x else isString x # Do not allow a true path, which could be copied to the store later on.
);
in
isExpectedType
&& (inStore == null || inStore == isInStore)
2018-02-08 09:49:06 +01:00
&& (absolute == null || absolute == isAbsolute);
};
2018-02-08 09:49:06 +01:00
listOf =
elemType:
mkOptionType rec {
name = "listOf";
description = "list of ${
optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType
}";
descriptionClass = "composite";
check = isList;
merge =
loc: defs:
map (x: x.value) (
filter (x: x ? value) (
concatLists (
imap1 (
n: def:
imap1 (
m: def':
(mergeDefinitions (loc ++ [ "[definition ${toString n}-entry ${toString m}]" ]) elemType [
{
inherit (def) file;
value = def';
}
2024-11-09 11:22:25 +01:00
]).optionalValue
) def.value
) defs
)
)
);
emptyValue = {
value = [ ];
};
2024-01-29 22:29:44 +03:00
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "*" ]);
getSubModules = elemType.getSubModules;
2024-01-29 22:29:44 +03:00
substSubModules = m: listOf (elemType.substSubModules m);
functor = (elemTypeFunctor name { inherit elemType; }) // {
type = payload: types.listOf payload.elemType;
};
nestedTypes.elemType = elemType;
};
2018-02-08 09:49:06 +01:00
nonEmptyListOf =
2024-11-09 11:22:25 +01:00
elemType:
let
2024-11-09 11:22:25 +01:00
list = addCheck (types.listOf elemType) (l: l != [ ]);
in
list
// {
2024-11-09 11:22:25 +01:00
description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
emptyValue = { }; # no .value attr, meaning unset
substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
};
attrsOf = elemType: attrsWith { inherit elemType; };
# A version of attrsOf that's lazy in its values at the expense of
# conditional definitions not working properly. E.g. defining a value with
# `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
# attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
# error that it's not defined. Use only if conditional definitions don't make sense.
2024-11-09 11:22:25 +01:00
lazyAttrsOf =
elemType:
attrsWith {
inherit elemType;
lazy = true;
};
# base type for lazyAttrsOf and attrsOf
attrsWith =
let
2024-11-09 11:22:25 +01:00
# Push down position info.
pushPositions = map (
def:
2024-11-09 11:22:25 +01:00
mapAttrs (n: v: {
2024-01-28 00:30:36 +01:00
inherit (def) file;
2024-11-09 11:22:25 +01:00
value = v;
2024-01-28 00:30:36 +01:00
}) def.value
);
binOp =
lhs: rhs:
let
elemType = lhs.elemType.typeMerge rhs.elemType.functor;
lazy = if lhs.lazy == rhs.lazy then lhs.lazy else null;
placeholder =
if lhs.placeholder == rhs.placeholder then
lhs.placeholder
else if lhs.placeholder == "name" then
rhs.placeholder
else if rhs.placeholder == "name" then
lhs.placeholder
else
null;
in
if elemType == null || lazy == null || placeholder == null then
null
else
{
inherit elemType lazy placeholder;
};
in
{
elemType,
lazy ? false,
placeholder ? "name",
}:
2024-01-28 00:30:36 +01:00
mkOptionType {
name = if lazy then "lazyAttrsOf" else "attrsOf";
description =
2024-11-09 11:22:25 +01:00
(if lazy then "lazy attribute set" else "attribute set")
+ " of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
descriptionClass = "composite";
check = isAttrs;
merge =
if lazy then
(
2024-11-09 11:22:25 +01:00
# Lazy merge Function
2024-01-28 00:30:36 +01:00
loc: defs:
2024-11-09 11:22:25 +01:00
zipAttrsWith
(
2024-11-09 11:22:25 +01:00
name: defs:
let
merged = mergeDefinitions (loc ++ [ name ]) elemType defs;
# mergedValue will trigger an appropriate error when accessed
in
2024-11-09 11:22:25 +01:00
merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
)
2024-11-09 11:22:25 +01:00
# Push down position info.
(pushPositions defs)
)
else
(
2024-11-09 11:22:25 +01:00
# Non-lazy merge Function
2024-01-28 00:30:36 +01:00
loc: defs:
2024-11-09 11:22:25 +01:00
mapAttrs (n: v: v.value) (
filterAttrs (n: v: v ? value) (
zipAttrsWith (name: defs: (mergeDefinitions (loc ++ [ name ]) elemType (defs)).optionalValue)
# Push down position info.
2024-11-09 11:22:25 +01:00
(pushPositions defs)
)
)
);
emptyValue = {
2024-01-28 00:30:36 +01:00
value = { };
};
2024-11-09 11:22:25 +01:00
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<${placeholder}>" ]);
getSubModules = elemType.getSubModules;
substSubModules =
2024-11-09 11:22:25 +01:00
m:
attrsWith {
elemType = elemType.substSubModules m;
inherit lazy placeholder;
};
2024-11-09 11:22:25 +01:00
functor =
(elemTypeFunctor "attrsWith" {
inherit elemType lazy placeholder;
})
// {
# Custom type merging required because of the "placeholder" attribute
inherit binOp;
2024-11-09 11:22:25 +01:00
};
nestedTypes.elemType = elemType;
};
# TODO: deprecate this in the future:
loaOf =
elemType:
types.attrsOf elemType
// {
name = "loaOf";
deprecationMessage =
"Mixing lists with attribute values is no longer"
+ " possible; please use `types.attrsOf` instead. See"
+ " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
nestedTypes.elemType = elemType;
};
attrTag =
tags:
let
tags_ = tags;
in
2024-01-28 00:30:36 +01:00
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_;
2024-01-28 00:30:36 +01:00
choicesStr = concatMapStringsSep ", " lib.strings.escapeNixIdentifier (attrNames tags);
in
mkOptionType {
name = "attrTag";
description = "attribute-tagged union";
2024-01-28 00:30:36 +01:00
descriptionClass = "noun";
getSubOptions =
prefix:
mapAttrs (tagName: tagOption: {
"${lib.showOption prefix}" = tagOption // {
loc = prefix ++ [ tagName ];
};
2024-01-28 00:30:36 +01:00
}) 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
2024-01-28 00:30:36 +01:00
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)}.";
2024-01-28 00:30:36 +01:00
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);
};
};
};
# A value produced by `lib.mkLuaInline`
luaInline = mkOptionType {
name = "luaInline";
description = "inline lua";
descriptionClass = "noun";
check = x: x._type or null == "lua-inline";
merge = mergeEqualOption;
};
uniq = unique { message = ""; };
2016-09-07 10:03:32 +09:00
unique =
{ message }:
2016-09-07 10:03:32 +09:00
type:
mkOptionType rec {
name = "unique";
inherit (type) description descriptionClass check;
merge = mergeUniqueOption {
inherit message;
inherit (type) merge;
};
emptyValue = type.emptyValue;
2013-10-28 14:25:58 +01:00
getSubOptions = type.getSubOptions;
getSubModules = type.getSubModules;
substSubModules = m: uniq (type.substSubModules m);
functor = elemTypeFunctor name { elemType = type; } // {
type = payload: types.unique { inherit message; } payload.elemType;
};
nestedTypes.elemType = type;
};
# Null or value of ...
nullOr =
elemType:
mkOptionType rec {
name = "nullOr";
description = "null or ${
optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType
}";
descriptionClass = "conjunction";
check = x: x == null || elemType.check x;
merge =
loc: defs:
let
nrNulls = count (def: def.value == null) defs;
in
if nrNulls == length defs then
null
else if nrNulls != 0 then
throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
else
elemType.merge loc defs;
emptyValue = {
value = null;
};
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: nullOr (elemType.substSubModules m);
functor = (elemTypeFunctor name { inherit elemType; }) // {
type = payload: types.nullOr payload.elemType;
};
nestedTypes.elemType = elemType;
};
functionTo =
elemType:
mkOptionType {
name = "functionTo";
description = "function that evaluates to a(n) ${
optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType
}";
descriptionClass = "composite";
check = isFunction;
merge = loc: defs: {
# An argument attribute has a default when it has a default in all definitions
__functionArgs = lib.zipAttrsWith (_: lib.all (x: x)) (
lib.map (fn: lib.functionArgs fn.value) defs
);
__functor =
_: callerArgs:
(mergeDefinitions (loc ++ [ "<function body>" ]) elemType (
map (fn: {
2024-01-28 00:30:36 +01:00
inherit (fn) file;
2017-01-14 04:14:01 +03:00
value = fn.value callerArgs;
}) defs
)).mergedValue;
};
getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
getSubModules = elemType.getSubModules;
substSubModules = m: functionTo (elemType.substSubModules m);
functor = (elemTypeFunctor "functionTo" { inherit elemType; }) // {
type = payload: types.functionTo payload.elemType;
};
nestedTypes.elemType = elemType;
};
2022-05-25 17:45:28 +02:00
# A submodule (like typed attribute set). See NixOS manual.
submodule =
modules:
submoduleWith {
shorthandOnlyDefinesConfig = true;
2022-05-25 17:45:28 +02:00
modules = toList modules;
};
2022-03-10 22:45:41 +01:00
# A module to be imported in some other part of the configuration.
2022-05-25 17:45:28 +02:00
deferredModule = deferredModuleWith { };
2022-03-10 22:45:41 +01:00
# A module to be imported in some other part of the configuration.
2022-05-25 17:45:28 +02:00
# `staticModules`' options will be added to the documentation, unlike
# options declared via `config`.
deferredModuleWith =
attrs@{
2022-05-25 17:45:28 +02:00
staticModules ? [ ],
}:
2016-09-07 10:03:32 +09:00
mkOptionType {
2022-03-10 22:45:41 +01:00
name = "deferredModule";
description = "module";
descriptionClass = "noun";
check = x: isAttrs x || isFunction x || path.check x;
merge = loc: defs: {
imports =
2022-05-25 17:45:28 +02:00
staticModules
++ map (
def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value
) defs;
};
2022-05-25 17:45:28 +02:00
inherit (submoduleWith { modules = staticModules; })
2017-01-14 04:14:01 +03:00
getSubOptions
getSubModules
;
substSubModules =
m:
2022-05-25 17:45:28 +02:00
deferredModuleWith (
attrs
// {
2022-05-25 17:45:28 +02:00
staticModules = m;
}
);
2022-05-25 17:45:28 +02:00
functor = defaultFunctor "deferredModuleWith" // {
type = types.deferredModuleWith;
payload = {
2022-05-25 17:45:28 +02:00
inherit staticModules;
};
2022-05-25 17:45:28 +02:00
binOp = lhs: rhs: {
staticModules = lhs.staticModules ++ rhs.staticModules;
};
};
2022-05-25 17:45:28 +02:00
};
2022-03-10 22:45:41 +01:00
# The type of a type!
optionType = mkOptionType {
name = "optionType";
description = "optionType";
descriptionClass = "noun";
check = value: value._type or null == "option-type";
merge =
loc: defs:
if length defs == 1 then
(head defs).value
else
let
# Prepares the type definitions for mergeOptionDecls, which
# annotates submodules types with file locations
optionModules = map (
{ value, file }:
{
_file = file;
# There's no way to merge types directly from the module system,
# but we can cheat a bit by just declaring an option with the type
options = lib.mkOption {
type = value;
};
}
) defs;
# Merges all the types into a single one, including submodule merging.
# This also propagates file information to all submodules
mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules);
in
mergedOption.type;
};
submoduleWith =
{
modules,
specialArgs ? { },
shorthandOnlyDefinesConfig ? false,
description ? null,
class ? null,
}@attrs:
let
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
inherit (lib.modules) evalModules;
allModules =
defs:
map (
{ value, file }:
if isAttrs value && shorthandOnlyDefinesConfig then
{
_file = file;
config = value;
}
else
{
_file = file;
imports = [ value ];
}
) defs;
base = evalModules {
inherit class specialArgs;
modules = [
{
# This is a work-around for the fact that some sub-modules,
# such as the one included in an attribute set, expects an "args"
# attribute to be given to the sub-module. As the option
# evaluation does not have any specific attribute name yet, we
# provide a default for the documentation and the freeform type.
#
# This is necessary as some option declaration might use the
# "name" attribute given as argument of the submodule and use it
# as the default of option declarations.
#
# We use lookalike unicode single angle quotation marks because
# of the docbook transformation the options receive. In all uses
# &gt; and &lt; wouldn't be encoded correctly so the encoded values
# would be used, and use of `<` and `>` would break the XML document.
# It shouldn't cause an issue since this is cosmetic for the manual.
_module.args.name = lib.mkOptionDefault "name";
}
] ++ modules;
};
freeformType = base._module.freeformType;
2013-10-28 14:25:58 +01:00
name = "submodule";
in
mkOptionType {
inherit name;
description = if description != null then description else freeformType.description or name;
check = x: isAttrs x || isFunction x || path.check x;
merge =
loc: defs:
(base.extendModules {
modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
prefix = loc;
}).config;
emptyValue = {
value = { };
};
2017-01-14 04:14:01 +03:00
getSubOptions =
prefix:
(base.extendModules { inherit prefix; }).options
// optionalAttrs (freeformType != null) {
# Expose the sub options of the freeform type. Note that the option
# discovery doesn't care about the attribute name used here, so this
# is just to avoid conflicts with potential options from the submodule
_freeformOptions = freeformType.getSubOptions prefix;
};
getSubModules = modules;
substSubModules =
m:
submoduleWith (
attrs
// {
modules = m;
}
);
nestedTypes = lib.optionalAttrs (freeformType != null) {
freeformType = freeformType;
};
functor = defaultFunctor name // {
type = types.submoduleWith;
payload = {
inherit
modules
class
specialArgs
shorthandOnlyDefinesConfig
description
;
};
binOp = lhs: rhs: {
class =
# `or null` was added for backwards compatibility only. `class` is
# always set in the current version of the module system.
if lhs.class or null == null then
rhs.class or null
else if rhs.class or null == null then
lhs.class or null
else if lhs.class or null == rhs.class then
lhs.class or null
else
throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\".";
modules = lhs.modules ++ rhs.modules;
specialArgs =
let
intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
in
if intersecting == { } then
lhs.specialArgs // rhs.specialArgs
else
throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
shorthandOnlyDefinesConfig =
if lhs.shorthandOnlyDefinesConfig == null then
rhs.shorthandOnlyDefinesConfig
else if rhs.shorthandOnlyDefinesConfig == null then
lhs.shorthandOnlyDefinesConfig
else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig then
lhs.shorthandOnlyDefinesConfig
else
throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
description =
if lhs.description == null then
rhs.description
else if rhs.description == null then
lhs.description
else if lhs.description == rhs.description then
lhs.description
else
throw "A submoduleWith option is declared multiple times with conflicting descriptions";
};
};
2016-09-07 10:03:32 +09:00
};
# A value from a set of allowed ones.
enum =
values:
let
inherit (lib.lists) unique;
show =
v:
if builtins.isString v then
''"${v}"''
else if builtins.isInt v then
builtins.toString v
else if builtins.isBool v then
boolToString v
else
''<${builtins.typeOf v}>'';
in
2016-09-07 10:03:32 +09:00
mkOptionType rec {
name = "enum";
description =
# Length 0 or 1 enums may occur in a design pattern with type merging
# where an "interface" module declares an empty enum and other modules
# provide implementations, each extending the enum with their own
# identifier.
if values == [ ] then
"impossible (empty enum)"
else if builtins.length values == 1 then
"value ${show (builtins.head values)} (singular enum)"
else
"one of ${concatMapStringsSep ", " show values}";
descriptionClass = if builtins.length values < 2 then "noun" else "conjunction";
check = flip elem values;
merge = mergeEqualOption;
functor = (defaultFunctor name) // {
payload = { inherit values; };
type = payload: types.enum payload.values;
binOp = a: b: { values = unique (a.values ++ b.values); };
};
};
2014-05-01 11:30:20 -04:00
# Either value of type `t1` or `t2`.
2016-09-07 10:03:32 +09:00
either =
t1: t2:
mkOptionType rec {
name = "either";
description =
if t1.descriptionClass or null == "nonRestrictiveClause" then
# Plain, but add comma
"${t1.description}, or ${
optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2
}"
else
"${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;
2016-11-07 08:34:06 +09:00
merge =
loc: defs:
let
2016-11-07 08:34:06 +09:00
defList = map (d: d.value) defs;
in
2016-11-07 08:34:06 +09:00
if all (x: t1.check x) defList then
t1.merge loc defs
else if all (x: t2.check x) defList then
t2.merge loc defs
else
2016-11-07 08:34:06 +09:00
mergeOneOption loc defs;
2024-09-10 22:45:27 +01:00
typeMerge =
f':
let
mt1 = t1.typeMerge (elemAt f'.payload.elemType 0).functor;
mt2 = t2.typeMerge (elemAt f'.payload.elemType 1).functor;
in
2016-09-07 10:03:32 +09:00
if (name == f'.name) && (mt1 != null) && (mt2 != null) then functor.type mt1 mt2 else null;
functor = elemTypeFunctor name {
elemType = [
t1
t2
];
};
nestedTypes.left = t1;
nestedTypes.right = t2;
};
# Any of the types in the given list
oneOf =
ts:
2016-11-07 08:34:06 +09:00
let
head' =
2016-11-07 08:34:06 +09:00
if ts == [ ] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
tail' = tail ts;
2016-11-07 08:34:06 +09:00
in
foldl' either head' tail';
# Either value of type `coercedType` or `finalType`, the former is
# converted to `finalType` using `coerceFunc`.
coercedTo =
coercedType: coerceFunc: finalType:
assert lib.assertMsg (
coercedType.getSubModules == null
) "coercedTo: coercedType must not have submodules (its a ${coercedType.description})";
mkOptionType rec {
name = "coercedTo";
description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${
optionDescriptionPhrase (class: class == "noun") coercedType
} convertible to it";
2017-01-14 04:14:01 +03:00
check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
merge =
loc: defs:
let
coerceVal = val: if coercedType.check val then coerceFunc val else val;
2017-01-14 04:14:01 +03:00
in
finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
emptyValue = finalType.emptyValue;
2017-01-14 04:14:01 +03:00
getSubOptions = finalType.getSubOptions;
getSubModules = finalType.getSubModules;
substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
2024-09-10 22:45:27 +01:00
typeMerge = t: null;
functor = (defaultFunctor name) // {
wrappedDeprecationMessage = makeWrappedDeprecationMessage { elemType = finalType; };
};
nestedTypes.coercedType = coercedType;
nestedTypes.finalType = finalType;
};
2017-01-14 04:14:01 +03:00
# Augment the given type with an additional type check function.
addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
};
2024-12-12 16:17:19 +01:00
/**
Merges two option types together.
:::{.note}
Uses the type merge function of the first type, to merge it with the second type.
Usually types can only be merged if they are of the same type
:::
# Inputs
: `a` (option type): The first option type.
: `b` (option type): The second option type.
# Returns
- The merged option type.
- `{ _type = "merge-error"; error = "Cannot merge types"; }` if the types can't be merged.
# Examples
:::{.example}
## `lib.types.mergeTypes` usage example
```nix
let
enumAB = lib.types.enum ["A" "B"];
enumXY = lib.types.enum ["X" "Y"];
# This operation could be notated as: [ A ] | [ B ] -> [ A B ]
merged = lib.types.mergeTypes enumAB enumXY; # -> enum [ "A" "B" "X" "Y" ]
in
assert merged.check "A"; # true
assert merged.check "B"; # true
assert merged.check "X"; # true
assert merged.check "Y"; # true
merged.check "C" # false
```
:::
*/
mergeTypes =
a: b:
assert isOptionType a && isOptionType b;
let
merged = a.typeMerge b.functor;
in
if merged == null then setType "merge-error" { error = "Cannot merge types"; } else merged;
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
};
Convert libs to a fixed-point This does break the API of being able to import any lib file and get its libs, however I'm not sure people did this. I made this while exploring being able to swap out docFn with a stub in #2305, to avoid functor performance problems. I don't know if that is going to move forward (or if it is a problem or not,) but after doing all this work figured I'd put it up anyway :) Two notable advantages to this approach: 1. when a lib inherits another lib's functions, it doesn't automatically get put in to the scope of lib 2. when a lib implements a new obscure functions, it doesn't automatically get put in to the scope of lib Using the test script (later in this commit) I got the following diff on the API: + diff master fixed-lib 11764a11765,11766 > .types.defaultFunctor > .types.defaultTypeMerge 11774a11777,11778 > .types.isOptionType > .types.isType 11781a11786 > .types.mkOptionType 11788a11794 > .types.setType 11795a11802 > .types.types This means that this commit _adds_ to the API, however I can't find a way to fix these last remaining discrepancies. At least none are _removed_. Test script (run with nix-repl in the PATH): #!/bin/sh set -eux repl() { suff=${1:-} echo "(import ./lib)$suff" \ | nix-repl 2>&1 } attrs_to_check() { repl "${1:-}" \ | tr ';' $'\n' \ | grep "\.\.\." \ | cut -d' ' -f2 \ | sed -e "s/^/${1:-}./" \ | sort } summ() { repl "${1:-}" \ | tr ' ' $'\n' \ | sort \ | uniq } deep_summ() { suff="${1:-}" depth="${2:-4}" depth=$((depth - 1)) summ "$suff" for attr in $(attrs_to_check "$suff" | grep -v "types.types"); do if [ $depth -eq 0 ]; then summ "$attr" | sed -e "s/^/$attr./" else deep_summ "$attr" "$depth" | sed -e "s/^/$attr./" fi done } ( cd nixpkgs #git add . #git commit -m "Auto-commit, sorry" || true git checkout fixed-lib deep_summ > ../fixed-lib git checkout master deep_summ > ../master ) if diff master fixed-lib; then echo "SHALLOW MATCH!" fi ( cd nixpkgs git checkout fixed-lib repl .types )
2017-07-28 20:05:35 -04:00
in
outer_types // outer_types.types