nixpkgs-suyu/lib/types.nix

386 lines
14 KiB
Nix
Raw Normal View History

# Definitions related to run-time type checking. Used in particular
# to type-check NixOS configurations.
let lib = import ./default.nix; in
with import ./lists.nix;
with import ./attrsets.nix;
with import ./options.nix;
with import ./trivial.nix;
with import ./strings.nix;
with {inherit (import ./modules.nix) mergeDefinitions filterOverrides; };
rec {
isType = type: x: (x._type or "") == type;
setType = typeName: value: value // {
_type = typeName;
};
isOptionType = isType "option-type";
mkOptionType =
{ # Human-readable representation of the type.
name
, # Parseable representation of the type.
typerep
, # Function applied to each definition that should return true if
# its type-correct, false otherwise.
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
, # Return list of sub-options.
getSubOptions ? {}
, # Same as 'getSubOptions', but with extra information about the
# location of the option which is used to generate documentation.
getSubOptionsPrefixed ? null
, # 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
, # List of type representations (typerep) of all the elementary types
# that are nested within the type. For an elementary type the list is
# a singleton of the typerep of itself.
# NOTE: Must be specified for every container type!
nestedTypes ? null
, # List of all default values, and an empty list if no default value exists.
defaultValues ? []
}:
{ _type = "option-type";
inherit name typerep check merge getSubOptions getSubModules substSubModules defaultValues;
nestedTypes = if (isNull nestedTypes) then (singleton typerep) else nestedTypes;
getSubOptionsPrefixed = if (isNull getSubOptionsPrefixed) then (prefix: getSubOptions) else getSubOptionsPrefixed;
};
types = rec {
#
# Elementary types
#
unspecified = mkOptionType {
name = "unspecified";
typerep = "(unspecified)";
};
bool = mkOptionType {
name = "boolean";
typerep = "(boolean)";
2013-11-12 13:48:19 +01:00
check = isBool;
merge = mergeEqualOption;
};
int = mkOptionType {
name = "integer";
typerep = "(integer)";
2013-11-12 13:48:19 +01:00
check = isInt;
merge = mergeOneOption;
};
str = mkOptionType {
name = "string";
typerep = "(string)";
2013-11-12 13:48:19 +01:00
check = isString;
merge = mergeOneOption;
};
# Merge multiple definitions by concatenating them (with the given
# separator between the values).
separatedString = sep: mkOptionType {
name = "string" + (optionalString (sep != "") " separated by ${sep}");
typerep = "(separatedString(${escape ["(" ")"] sep}))";
2013-11-12 13:48:19 +01:00
check = isString;
merge = _module: loc: defs: concatStringsSep sep (getValues defs);
};
lines = separatedString "\n" // { typerep = "(lines)"; };
commas = separatedString "," // { typerep = "(commas)"; };
envVar = separatedString ":" // { typerep = "(envVar)"; };
# Deprecated; should not be used because it quietly concatenates
# strings, which is usually not what you want.
string = separatedString "" // { typerep = "(string)"; };
attrs = mkOptionType {
name = "attribute set";
typerep = "(attrs)";
check = isAttrs;
merge = _module: loc: foldl' (res: def: mergeAttrs res def.value) {};
};
# derivation is a reserved keyword.
package = mkOptionType {
lib/types: Set name of types.package to "package" Nobody seems to have noticed this (except @Profpatsch) that options with a "package" type do not get included in the manual. So debugging this was a bit more involving because while generating the manual there is an optionList' attribute built from the collected attributes of all the option declarations. Up to that point everything is fine except if it comes to builtins.toXML, where attributes with { type = "derivation" } won't get included, for example see here: nix-repl> builtins.toXML { type = "derivation"; foo = "bar"; } "<?xml version='1.0' encoding='utf-8'?>\n<expr>\n <derivation> <repeated />\n </derivation>\n</expr>\n" nix-repl> builtins.toXML { type = "somethingelse"; foo = "bar"; } "<?xml version='1.0' encoding='utf-8'?>\n<expr>\n <attrs> <attr name=\"foo\">\n <string value=\"bar\" />\n </attr> <attr name=\"type\">\n <string value=\"somethingelse\" /> </attr>\n </attrs>\n</expr>\n" The following function in libexpr/eval.cc (Nix) is responsible for toXML dropping the attributes: bool EvalState::isDerivation(Value & v) { if (v.type != tAttrs) return false; Bindings::iterator i = v.attrs->find(sType); if (i == v.attrs->end()) return false; forceValue(*i->value); if (i->value->type != tString) return false; return strcmp(i->value->string.s, "derivation") == 0; } So I've renamed this now to "package" which is not only more consistent with the option type but also shouldn't cause similar issues anymore. Tested this on base of b60ceea, because building the dependencies on recent libc/staging changes on master took too long. Signed-off-by: aszlig <aszlig@redmoonstudios.org> Reported-by: Profpatsch <mail@profpatsch.de>
2016-02-17 20:24:22 +01:00
name = "package";
typerep = "(package)";
check = x: isDerivation x || isStorePath x;
merge = _module: loc: defs:
let res = mergeOneOption _module loc defs;
in if isDerivation res then res else toDerivation res;
};
# The correct type of packageSet would be:
# packageSet = attrsOf (either package packageSet)
# (Not sure if nix would allow to define a recursive type.)
# However, currently it is not possible to check that a packageSet actually
# contains packages (that is derivations). The check 'isDerivation' is too
# eager for the current implementation of the assertion mechanism and of the
# licenses control mechanism. That means it is not generally possible to go
# into the attribute set of packages to check that every attribute would
# evaluate to a derivation if the package would actually be evaluated. Maybe
# that restriction can be lifted in the future, but for now the content of
# the packageSet is not checked.
# TODO: The 'merge' function is copied from 'mergeDefaultOption' to keep
# backwards compatibility with the 'unspecified' type that was used for
# package sets previously. Maybe review if the merge function has to change.
packageSet = mkOptionType {
name = "derivation set";
typerep = "(packageSet)";
check = isAttrs;
merge = _module: loc: defs: foldl' mergeAttrs {} (map (x: x.value) defs);
};
path = mkOptionType {
name = "path";
typerep = "(path)";
# Hacky: there is no isPath primop.
check = x: builtins.substring 0 1 (toString x) == "/";
merge = mergeOneOption;
};
#
# Container types
#
# drop this in the future:
list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf;
listOf = elemType: mkOptionType {
name = "list of ${elemType.name}s";
typerep = "(listOf${elemType.typerep})";
check = isList;
merge = _module: loc: defs:
map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def: imap (m: def':
(mergeDefinitions _module
(loc ++ ["[definition ${toString n}-entry ${toString m}]"])
elemType
[{ inherit (def) file; value = def'; }]
).optionalValue
) def.value) defs)));
getSubOptions = elemType.getSubOptions;
getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["*"]);
getSubModules = elemType.getSubModules;
substSubModules = m: listOf (elemType.substSubModules m);
nestedTypes = elemType.nestedTypes;
defaultValues = [[]];
};
attrsOf = elemType: mkOptionType {
name = "attribute set of ${elemType.name}s";
typerep = "(attrsOf${elemType.typerep})";
check = isAttrs;
merge = _module: loc: defs:
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
(mergeDefinitions _module (loc ++ [name]) elemType defs).optionalValue
)
# Push down position info.
(map (def: listToAttrs (mapAttrsToList (n: def':
{ name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs)));
getSubOptions = elemType.getSubOptions;
getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: attrsOf (elemType.substSubModules m);
nestedTypes = elemType.nestedTypes;
defaultValues = [{}];
};
# List or attribute set of ...
loaOf = elemType:
let
convertIfList = defIdx: def:
if isList def.value then
{ inherit (def) file;
value = listToAttrs (
imap (elemIdx: elem:
{ name = elem.name or "unnamed-${toString defIdx}.${toString elemIdx}";
value = elem;
}) def.value);
}
else
def;
listOnly = listOf elemType;
attrOnly = attrsOf elemType;
in mkOptionType {
name = "list or attribute set of ${elemType.name}s";
typerep = "(loaOf${elemType.typerep})";
check = x: isList x || isAttrs x;
merge = _module: loc: defs: attrOnly.merge _module loc (imap convertIfList defs);
getSubOptions = elemType.getSubOptions;
getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed (prefix ++ ["<name?>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: loaOf (elemType.substSubModules m);
nestedTypes = elemType.nestedTypes;
defaultValues = [{} []];
};
# List or element of ...
loeOf = elemType: mkOptionType {
name = "element or list of ${elemType.name}s";
typerep = "(loeOf${elemType.typerep})";
check = x: isList x || elemType.check x;
merge = _module: loc: defs:
let
defs' = filterOverrides defs;
res = (head defs').value;
in
if isList res then concatLists (getValues defs')
else if lessThan 1 (length defs') then
throw "The option `${showOption loc}' is defined multiple times, in ${showFiles (getFiles defs)}."
else if !isString res then
throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}."
else res;
nestedTypes = elemType.nestedTypes;
defaultValues = [[]] ++ elemType.defaultValues;
};
uniq = elemType: mkOptionType {
inherit (elemType) check;
name = "unique ${elemType.name}";
typerep = "(uniq${elemType.typerep})";
merge = mergeOneOption;
2013-10-28 14:25:58 +01:00
getSubOptions = elemType.getSubOptions;
getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed prefix;
getSubModules = elemType.getSubModules;
substSubModules = m: uniq (elemType.substSubModules m);
nestedTypes = elemType.nestedTypes;
defaultValues = elemType.defaultValues;
};
nullOr = elemType: mkOptionType {
name = "null or ${elemType.name}";
typerep = "(nullOr${elemType.typerep})";
check = x: x == null || elemType.check x;
merge = _module: 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 _module loc defs;
2013-10-28 14:25:58 +01:00
getSubOptions = elemType.getSubOptions;
getSubOptionsPrefixed = prefix: elemType.getSubOptionsPrefixed prefix;
getSubModules = elemType.getSubModules;
substSubModules = m: nullOr (elemType.substSubModules m);
nestedTypes = elemType.nestedTypes;
defaultValues = [null] ++ elemType.defaultValues;
};
enum = values:
let
show = v:
if builtins.isString v then ''"${v}"''
else if builtins.isInt v then builtins.toString v
else ''<${builtins.typeOf v}>'';
in
mkOptionType {
name = "one of ${concatMapStringsSep ", " show values}";
typerep = "(enum${concatMapStrings (x: "(${escape ["(" ")"] (builtins.toString x)})") values})";
check = flip elem values;
merge = mergeOneOption;
nestedTypes = [];
};
either = t1: t2: mkOptionType {
name = "${t1.name} or ${t2.name}";
typerep = "(either${t1.typerep}${t2.typerep})";
check = x: t1.check x || t2.check x;
merge = mergeOneOption;
nestedTypes = t1.nestedTypes ++ t2.nestedTypes;
defaultValues = t1.defaultValues ++ t2.defaultValues;
};
#
# Complex types
#
submodule = opts:
let
opts' = toList opts;
inherit (import ./modules.nix) evalModules;
filterVisible = filter (opt: (if opt ? visible then opt.visible else true) && (if opt ? internal then !opt.internal else true));
in
2013-10-28 14:25:58 +01:00
mkOptionType rec {
name = "submodule";
typerep = "(submodule)";
2013-11-12 13:48:19 +01:00
check = x: isAttrs x || isFunction x;
merge = _module: loc: defs:
2013-10-28 14:25:58 +01:00
let
internalModule = [ { inherit _module; } { _module.args.name = lib.mkForce (last loc); } ];
2013-11-12 13:48:19 +01:00
coerce = def: if isFunction def then def else { config = def; };
modules = opts' ++ internalModule ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
in (evalModules {
inherit modules;
prefix = loc;
}).config;
getSubOptions = getSubOptionsPrefixed [];
getSubOptionsPrefixed = prefix: _module:
let
2013-10-28 15:48:20 +01:00
# FIXME: hack to get shit to evaluate.
internalModule = [ { inherit _module; } { _module.args.name = lib.mkForce ""; } ];
in (evalModules {
modules = opts' ++ internalModule;
inherit prefix;
}).options;
getSubModules = opts';
substSubModules = m: submodule m;
nestedTypes = concatMap (opt: opt.type.nestedTypes) (collect (lib.isType "option") (getSubOptions {}));
defaultValues = [{}];
2013-10-28 14:25:58 +01:00
};
2014-05-01 17:30:20 +02:00
#
# Legacy types
#
2014-08-19 13:13:51 +02:00
# Obsolete alternative to configOf. It takes its option
# declarations from the options attribute of containing option
# declaration.
optionSet = mkOptionType {
name = /* builtins.trace "types.optionSet is deprecated; use types.submodule instead" */ "option set";
typerep = "(optionSet)";
};
# Try to remove module options of that type wherever possible.
# A module option taking a function can not be introspected and documented properly.
functionTo = resultType:
mkOptionType {
name = "function to ${resultType.name}";
typerep = "(function${resultType.typerep})";
check = builtins.isFunction;
merge = mergeOneOption;
nestedTypes = resultType.nestedTypes;
defaultValues = map (x: {...}:x) resultType.nestedTypes; # INFO: It seems as nix can't compare functions, yet.
};
#
# misc
#
# Augment the given type with an additional type check function.
addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
};
}