forked from auxolotl/labs
307 lines
11 KiB
Nix
307 lines
11 KiB
Nix
|
lib: {
|
||
|
options = {
|
||
|
merge = {
|
||
|
## Merge a list of option definitions into a single value.
|
||
|
##
|
||
|
## @type Location -> List Definition -> Any
|
||
|
default = location: definitions: let
|
||
|
values = lib.options.getDefinitionValues definitions;
|
||
|
first = builtins.elemAt values 0;
|
||
|
mergedFunctions = x:
|
||
|
lib.options.mergeDefault
|
||
|
location
|
||
|
(builtins.map (f: f x) values);
|
||
|
mergedLists = builtins.concatLists values;
|
||
|
mergedAttrs = builtins.foldl' lib.attrs.merge {} values;
|
||
|
mergedBools = builtins.any lib.bools.or false values;
|
||
|
mergedStrings = lib.strings.concat values;
|
||
|
in
|
||
|
if builtins.length values == 1
|
||
|
then builtins.elemAt values 0
|
||
|
else if builtins.all builtins.isFunction values
|
||
|
then mergedFunctions
|
||
|
else if builtins.all builtins.isList values
|
||
|
then mergedLists
|
||
|
else if builtins.all builtins.isAttrs values
|
||
|
then mergedAttrs
|
||
|
else if builtins.all builtins.isBool values
|
||
|
then mergedBools
|
||
|
else if builtins.all lib.strings.isString values
|
||
|
then mergedStrings
|
||
|
else if builtins.all builtins.isInt values && builtins.all (x: x == first) values
|
||
|
then first
|
||
|
# TODO: Improve this error message to show the location and definitions for the option.
|
||
|
else builtins.throw "Cannot merge definitions.";
|
||
|
|
||
|
# TODO: Document this.
|
||
|
definitions = location: type: definitions: let
|
||
|
identifier = lib.options.getIdentifier location;
|
||
|
resolve = definition: let
|
||
|
properties =
|
||
|
builtins.addErrorContext
|
||
|
"while evaluating definitions from `${definition.__file__}`:"
|
||
|
(lib.modules.apply.properties definition.value);
|
||
|
normalize = value: {
|
||
|
__file__ = definition.__file__;
|
||
|
inherit value;
|
||
|
};
|
||
|
in
|
||
|
builtins.map normalize properties;
|
||
|
|
||
|
resolved = builtins.concatMap resolve definitions;
|
||
|
overridden = lib.modules.apply.overrides resolved;
|
||
|
|
||
|
values =
|
||
|
if builtins.any (definition: lib.types.is "order" definition.value) overridden.values
|
||
|
then lib.modules.apply.order overridden.values
|
||
|
else overridden.values;
|
||
|
|
||
|
isDefined = values != [];
|
||
|
|
||
|
invalid = builtins.filter (definition: !(type.check definition.value)) values;
|
||
|
|
||
|
merged =
|
||
|
if isDefined
|
||
|
then
|
||
|
if builtins.all (definition: type.check definition.value) values
|
||
|
then type.merge location values
|
||
|
else builtins.throw "A definition for `${identifier}` is not of type `${type.description}`. Definition values:${lib.options.getDefinitions invalid}"
|
||
|
else builtins.throw "The option `${identifier}` is used but not defined.";
|
||
|
|
||
|
optional =
|
||
|
if isDefined
|
||
|
then {value = merged;}
|
||
|
else {};
|
||
|
in {
|
||
|
inherit isDefined values merged optional;
|
||
|
|
||
|
raw = {
|
||
|
inherit values;
|
||
|
inherit (overridden) highestPriority;
|
||
|
};
|
||
|
};
|
||
|
|
||
|
declarations = location: options: let
|
||
|
merge = result: option: let
|
||
|
mergedType = result.type.mergeType option.options.type.functor;
|
||
|
isTypeMergeable = mergedType != null;
|
||
|
shared = key: option.options ? ${key} && result ? ${key};
|
||
|
typeSet = lib.attrs.when ((shared "type") && isTypeMergeable) {
|
||
|
type = mergedType;
|
||
|
};
|
||
|
files = builtins.map lib.modules.getFiles result.declarations;
|
||
|
serializedFiles = builtins.concatStringsSep " and " files;
|
||
|
getSubModules = option.options.type.getSubModules or null;
|
||
|
submodules =
|
||
|
if getSubModules != null
|
||
|
then
|
||
|
builtins.map
|
||
|
(module: {
|
||
|
__file__ = option.__file__;
|
||
|
includes = [module];
|
||
|
})
|
||
|
getSubModules
|
||
|
++ result.options
|
||
|
else result.options;
|
||
|
in
|
||
|
if shared "default" || shared "example" || shared "description" || shared "apply" || (shared "type" && !isTypeMergeable)
|
||
|
then builtins.throw "The option `${lib.options.getIdentifier location}` in `${option.__file__}` is already declared in ${serializedFiles}"
|
||
|
else
|
||
|
option.options
|
||
|
// result
|
||
|
// {
|
||
|
declarations = result.declarations ++ [option.__file__];
|
||
|
options = submodules;
|
||
|
}
|
||
|
// typeSet;
|
||
|
in
|
||
|
builtins.foldl'
|
||
|
merge
|
||
|
{
|
||
|
inherit location;
|
||
|
declarations = [];
|
||
|
options = [];
|
||
|
}
|
||
|
options;
|
||
|
|
||
|
## Merge an option, only supporting a single unique definition.
|
||
|
##
|
||
|
## @type String -> Location -> List Definition -> Any
|
||
|
unique = message: location: definitions: let
|
||
|
identifier = lib.options.getIdentifier location;
|
||
|
total = builtins.length definitions;
|
||
|
first = builtins.elemAt definitions 0;
|
||
|
in
|
||
|
if total == 1
|
||
|
then first.value
|
||
|
else if total == 0
|
||
|
then builtins.throw "Cannot merge unused option `${identifier}`.\n${message}"
|
||
|
else builtins.throw "The option `${identifier}` is defined multiple times, but must be unique.\n${message}\nDefinitions:${lib.options.getDefinitions definitions}";
|
||
|
|
||
|
## Merge a single instance of an option.
|
||
|
##
|
||
|
## @type Location -> List Definition -> Any
|
||
|
one = lib.options.merge.unique "";
|
||
|
|
||
|
equal = location: definitions: let
|
||
|
identifier = lib.options.getIdentifier location;
|
||
|
first = builtins.elemAt definitions 0;
|
||
|
rest = builtins.tail definitions;
|
||
|
merge = x: y:
|
||
|
if x != y
|
||
|
then builtins.throw "The option `${identifier}` has conflicting definitions:${lib.options.getDefinitions definitions}"
|
||
|
else x;
|
||
|
merged = builtins.foldl' merge first rest;
|
||
|
in
|
||
|
if builtins.length definitions == 0
|
||
|
then builtins.throw "Cannot merge unused option `${identifier}`."
|
||
|
else if builtins.length definitions == 1
|
||
|
then first.value
|
||
|
else merged.value;
|
||
|
};
|
||
|
|
||
|
## Check whether a value is an option.
|
||
|
##
|
||
|
## @type Attrs -> Bool
|
||
|
is = lib.types.is "option";
|
||
|
|
||
|
## Create an option.
|
||
|
##
|
||
|
## @type { type? :: String | Null, apply? :: (a -> b) | Null, default? :: { value :: a, text :: String }, example? :: String | Null, visible? :: Bool | Null, internal? :: Bool | Null, writable? :: Bool | Null, description? :: String | Null } -> Option a
|
||
|
create = settings @ {
|
||
|
type ? lib.types.unspecified,
|
||
|
apply ? null,
|
||
|
default ? {},
|
||
|
example ? null,
|
||
|
visible ? null,
|
||
|
internal ? null,
|
||
|
writable ? null,
|
||
|
description ? null,
|
||
|
}: {
|
||
|
__type__ = "option";
|
||
|
inherit type apply default example visible internal writable description;
|
||
|
};
|
||
|
|
||
|
## Create a sink option.
|
||
|
##
|
||
|
## @type @alias lib.options.create
|
||
|
sink = settings: let
|
||
|
defaults = {
|
||
|
internal = true;
|
||
|
visible = false;
|
||
|
default = false;
|
||
|
description = "A sink option for unused definitions";
|
||
|
type = lib.types.create {
|
||
|
name = "sink";
|
||
|
check = lib.fp.const true;
|
||
|
merge = lib.fp.const (lib.fp.const false);
|
||
|
};
|
||
|
apply = value: builtins.throw "Cannot read the value of a Sink option.";
|
||
|
};
|
||
|
in
|
||
|
lib.options.create (defaults // settings);
|
||
|
|
||
|
## Get the definition values from a list of options definitions.
|
||
|
##
|
||
|
## @type List Definition -> Any
|
||
|
getDefinitionValues = definitions:
|
||
|
builtins.map (definition: definition.value) definitions;
|
||
|
|
||
|
## Convert a list of option identifiers into a single identifier.
|
||
|
##
|
||
|
## @type List String -> String
|
||
|
getIdentifier = location: let
|
||
|
special = [
|
||
|
"<name>" # attrsOf (submodule {})
|
||
|
"*" # listOf (submodule {})
|
||
|
"<function body>" # functionTo
|
||
|
];
|
||
|
escape = part:
|
||
|
if builtins.elem part special
|
||
|
then part
|
||
|
else lib.strings.escape.nix.identifier part;
|
||
|
in
|
||
|
lib.strings.concatMapSep "." escape location;
|
||
|
|
||
|
## Get a string message of the definitions for an option.
|
||
|
##
|
||
|
## @type List Definition -> String
|
||
|
getDefinitions = definitions: let
|
||
|
serialize = definition: let
|
||
|
valueWithRecursionLimit =
|
||
|
lib.generators.withRecursion {
|
||
|
limit = 10;
|
||
|
throw = false;
|
||
|
}
|
||
|
definition.value;
|
||
|
|
||
|
eval = builtins.tryEval (
|
||
|
lib.generators.pretty {}
|
||
|
valueWithRecursionLimit
|
||
|
);
|
||
|
|
||
|
lines = lib.strings.split "\n" eval.value;
|
||
|
linesLength = builtins.length lines;
|
||
|
firstFiveLines = lib.lists.take 5 lines;
|
||
|
|
||
|
ellipsis = lib.lists.when (linesLength > 5) "...";
|
||
|
|
||
|
value = builtins.concatStringsSep "\n " (firstFiveLines ++ ellipsis);
|
||
|
|
||
|
result =
|
||
|
if ! eval.success
|
||
|
then ""
|
||
|
else if linesLength > 1
|
||
|
then ":\n " + value
|
||
|
else ": " + value;
|
||
|
in "\n- In `${definition.__file__}`${result}";
|
||
|
in
|
||
|
lib.strings.concatMap serialize definitions;
|
||
|
|
||
|
# TODO: Document this.
|
||
|
run = location: option: definitions: let
|
||
|
identifier = lib.options.getIdentifier location;
|
||
|
|
||
|
definitionsWithDefault =
|
||
|
if option ? default && option.default ? value
|
||
|
then
|
||
|
[
|
||
|
{
|
||
|
__file__ = builtins.head option.declarations;
|
||
|
value = lib.modules.overrides.option option.default.value;
|
||
|
}
|
||
|
]
|
||
|
++ definitions
|
||
|
else definitions;
|
||
|
|
||
|
merged =
|
||
|
if option.writable or null == false && builtins.length definitionsWithDefault > 1
|
||
|
then let
|
||
|
separatedDefinitions = builtins.map (definition:
|
||
|
definition
|
||
|
// {
|
||
|
value = (lib.options.merge.definitions location option.type [definition]).merged;
|
||
|
})
|
||
|
definitionsWithDefault;
|
||
|
in
|
||
|
builtins.throw "The option `${identifier}` is not writable, but is set more than once:${lib.options.getDefinitions separatedDefinitions}"
|
||
|
else lib.options.merge.definitions location option.type definitionsWithDefault;
|
||
|
|
||
|
value =
|
||
|
if option.apply or null != null
|
||
|
then option.apply merged.merged
|
||
|
else merged.merged;
|
||
|
in
|
||
|
option
|
||
|
// {
|
||
|
value = builtins.addErrorContext "while evaluating the option `${identifier}`:" value;
|
||
|
highestPriority = merged.raw.highestPriority;
|
||
|
isDefined = merged.isDefined;
|
||
|
files = builtins.map (definition: definition.__file__) merged.values;
|
||
|
definitions = lib.options.getDefinitionValues merged.values;
|
||
|
definitionsWithLocations = merged.values;
|
||
|
__toString = identifier;
|
||
|
};
|
||
|
};
|
||
|
}
|