module system: extensible option types

Changed files
+314 -67
lib
nixos
doc
manual
modules
installer
+21 -11
lib/modules.nix
···
correspond to the definition of 'loc' in 'opt.file'. */
mergeOptionDecls = loc: opts:
foldl' (res: opt:
-
if opt.options ? default && res ? default ||
-
opt.options ? example && res ? example ||
-
opt.options ? description && res ? description ||
-
opt.options ? apply && res ? apply ||
-
# Accept to merge options which have identical types.
-
opt.options ? type && res ? type && opt.options.type.name != res.type.name
+
let t = res.type;
+
t' = opt.options.type;
+
mergedType = t.typeMerge t'.functor;
+
typesMergeable = mergedType != null;
+
typeSet = if (bothHave "type") && typesMergeable
+
then { type = mergedType; }
+
else {};
+
bothHave = k: opt.options ? ${k} && res ? ${k};
+
in
+
if bothHave "default" ||
+
bothHave "example" ||
+
bothHave "description" ||
+
bothHave "apply" ||
+
(bothHave "type" && (! typesMergeable))
then
throw "The option `${showOption loc}' in `${opt.file}' is already declared in ${showFiles res.declarations}."
else
···
in opt.options // res //
{ declarations = res.declarations ++ [opt.file];
options = submodules;
-
}
+
} // typeSet
) { inherit loc; declarations = []; options = []; } opts;
/* Merge all the definitions of an option to produce the final
···
options = opt.options or
(throw "Option `${showOption loc'}' has type optionSet but has no option attribute, in ${showFiles opt.declarations}.");
f = tp:
+
let optionSetIn = type: (tp.name == type) && (tp.functor.wrapped.name == "optionSet");
+
in
if tp.name == "option set" || tp.name == "submodule" then
throw "The option ${showOption loc} uses submodules without a wrapping type, in ${showFiles opt.declarations}."
-
else if tp.name == "attribute set of option sets" then types.attrsOf (types.submodule options)
-
else if tp.name == "list or attribute set of option sets" then types.loaOf (types.submodule options)
-
else if tp.name == "list of option sets" then types.listOf (types.submodule options)
-
else if tp.name == "null or option set" then types.nullOr (types.submodule options)
+
else if optionSetIn "attrsOf" then types.attrsOf (types.submodule options)
+
else if optionSetIn "loaOf" then types.loaOf (types.submodule options)
+
else if optionSetIn "listOf" then types.listOf (types.submodule options)
+
else if optionSetIn "nullOr" then types.nullOr (types.submodule options)
else tp;
in
if opt.type.getSubModules or null == null
+1 -1
lib/options.nix
···
internal = opt.internal or false;
visible = opt.visible or true;
readOnly = opt.readOnly or false;
-
type = opt.type.name or null;
+
type = opt.type.description or null;
}
// (if opt ? example then { example = scrubOptionValue opt.example; } else {})
// (if opt ? default then { default = scrubOptionValue opt.default; } else {})
+115 -27
lib/types.nix
···
};
+
# Default type merging function
+
# takes two type functors and return the merged type
+
defaultTypeMerge = f: f':
+
let wrapped = f.wrapped.typeMerge f'.wrapped.functor;
+
payload = f.binOp f.payload f'.payload;
+
in
+
# cannot merge different types
+
if f.name != f'.name
+
then null
+
# simple types
+
else if (f.wrapped == null && f'.wrapped == null)
+
&& (f.payload == null && f'.payload == null)
+
then f.type
+
# composed types
+
else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
+
then f.type wrapped
+
# value types
+
else if (f.payload != null && f'.payload != null) && (payload != null)
+
then f.type payload
+
else null;
+
+
# Default type functor
+
defaultFunctor = name: {
+
inherit name;
+
type = types."${name}" or null;
+
wrapped = null;
+
payload = null;
+
binOp = a: b: null;
+
};
+
isOptionType = isType "option-type";
mkOptionType =
-
{ # Human-readable representation of the type.
+
{ # Human-readable representation of the type, should be equivalent to
+
# the type function name.
name
+
, # Description of the type, defined recursively by embedding the the wrapped type if any.
+
description ? null
, # Function applied to each definition that should return true if
# its type-correct, false otherwise.
check ? (x: true)
···
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
+
, # Function for building the same option type with a different list of
# modules.
substSubModules ? m: null
+
, # Function that merge type declarations.
+
# 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
+
# combinable with the binOp binary operation.
+
# binOp: binary operation that merge two payloads of the same type.
+
functor ? defaultFunctor name
}:
{ _type = "option-type";
-
inherit name check merge getSubOptions getSubModules substSubModules;
+
inherit name check merge getSubOptions getSubModules substSubModules typeMerge functor;
+
description = if description == null then name else description;
};
···
};
bool = mkOptionType {
-
name = "boolean";
+
name = "bool";
+
description = "boolean";
check = isBool;
merge = mergeEqualOption;
};
-
int = mkOptionType {
-
name = "integer";
+
int = mkOptionType rec {
+
name = "int";
+
description = "integer";
check = isInt;
merge = mergeOneOption;
};
str = mkOptionType {
-
name = "string";
+
name = "str";
+
description = "string";
check = isString;
merge = mergeOneOption;
};
# Merge multiple definitions by concatenating them (with the given
# separator between the values).
-
separatedString = sep: mkOptionType {
-
name = "string";
+
separatedString = sep: mkOptionType rec {
+
name = "separatedString";
+
description = "string";
check = isString;
merge = loc: defs: concatStringsSep sep (getValues defs);
+
functor = (defaultFunctor name) // {
+
payload = sep;
+
binOp = sepLhs: sepRhs:
+
if sepLhs == sepRhs then sepLhs
+
else null;
+
};
};
lines = separatedString "\n";
···
string = separatedString "";
attrs = mkOptionType {
-
name = "attribute set";
+
name = "attrs";
+
description = "attribute set";
check = isAttrs;
merge = loc: foldl' (res: def: mergeAttrs res def.value) {};
};
···
# 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";
+
listOf = elemType: mkOptionType rec {
+
name = "listOf";
+
description = "list of ${elemType.description}s";
check = isList;
merge = loc: defs:
map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def:
···
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
getSubModules = elemType.getSubModules;
substSubModules = m: listOf (elemType.substSubModules m);
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
-
attrsOf = elemType: mkOptionType {
-
name = "attribute set of ${elemType.name}s";
+
attrsOf = elemType: mkOptionType rec {
+
name = "attrsOf";
+
description = "attribute set of ${elemType.description}s";
check = isAttrs;
merge = loc: defs:
mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
···
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: attrsOf (elemType.substSubModules m);
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
# List or attribute set of ...
···
def;
listOnly = listOf elemType;
attrOnly = attrsOf elemType;
-
in mkOptionType {
-
name = "list or attribute set of ${elemType.name}s";
+
in mkOptionType rec {
+
name = "loaOf";
+
description = "list or attribute set of ${elemType.description}s";
check = x: isList x || isAttrs x;
merge = loc: defs: attrOnly.merge loc (imap convertIfList defs);
getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]);
getSubModules = elemType.getSubModules;
substSubModules = m: loaOf (elemType.substSubModules m);
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
# List or element of ...
-
loeOf = elemType: mkOptionType {
-
name = "element or list of ${elemType.name}s";
+
loeOf = elemType: mkOptionType rec {
+
name = "loeOf";
+
description = "element or list of ${elemType.description}s";
check = x: isList x || elemType.check x;
merge = loc: defs:
let
···
else if !isString res then
throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}."
else res;
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
-
uniq = elemType: mkOptionType {
-
inherit (elemType) name check;
+
uniq = elemType: mkOptionType rec {
+
name = "uniq";
+
inherit (elemType) description check;
merge = mergeOneOption;
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: uniq (elemType.substSubModules m);
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
-
nullOr = elemType: mkOptionType {
-
name = "null or ${elemType.name}";
+
nullOr = elemType: mkOptionType rec {
+
name = "nullOr";
+
description = "null or ${elemType.description}";
check = x: x == null || elemType.check x;
merge = loc: defs:
let nrNulls = count (def: def.value == null) defs; in
···
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: nullOr (elemType.substSubModules m);
+
functor = (defaultFunctor name) // { wrapped = elemType; };
};
submodule = opts:
···
args = { name = ""; }; }).options;
getSubModules = opts';
substSubModules = m: submodule m;
+
functor = (defaultFunctor name) // {
+
# Merging of submodules is done as part of mergeOptionDecls, as we have to annotate
+
# each submodule with its location.
+
payload = [];
+
binOp = lhs: rhs: [];
+
};
};
enum = values:
···
else if builtins.isInt v then builtins.toString v
else ''<${builtins.typeOf v}>'';
in
-
mkOptionType {
-
name = "one of ${concatMapStringsSep ", " show values}";
+
mkOptionType rec {
+
name = "enum";
+
description = "one of ${concatMapStringsSep ", " show values}";
check = flip elem values;
merge = mergeOneOption;
+
functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
};
-
either = t1: t2: mkOptionType {
-
name = "${t1.name} or ${t2.name}";
+
either = t1: t2: mkOptionType rec {
+
name = "either";
+
description = "${t1.description} or ${t2.description}";
check = x: t1.check x || t2.check x;
merge = mergeOneOption;
+
typeMerge = f':
+
let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
+
mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
+
in
+
if (name == f'.name) && (mt1 != null) && (mt2 != null)
+
then functor.type mt1 mt2
+
else null;
+
functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
};
# 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";
+
name = builtins.trace "types.optionSet is deprecated; use types.submodule instead" "optionSet";
+
description = "option set";
};
# Augment the given type with an additional type check function.
+88
nixos/doc/manual/development/option-declarations.xml
···
</para>
+
<section xml:id="sec-option-declarations-eot"><title>Extensible Option
+
Types</title>
+
+
<para>Extensible option types is a feature that allow to extend certain types
+
declaration through multiple module files.
+
This feature only work with a restricted set of types, namely
+
<literal>enum</literal> and <literal>submodules</literal> and any composed
+
forms of them.</para>
+
+
<para>Extensible option types can be used for <literal>enum</literal> options
+
that affects multiple modules, or as an alternative to related
+
<literal>enable</literal> options.</para>
+
+
<para>As an example, we will take the case of display managers. There is a
+
central display manager module for generic display manager options and a
+
module file per display manager backend (slim, kdm, gdm ...).
+
</para>
+
+
<para>There are two approach to this module structure:
+
+
<itemizedlist>
+
<listitem><para>Managing the display managers independently by adding an
+
enable option to every display manager module backend. (NixOS)</para>
+
</listitem>
+
<listitem><para>Managing the display managers in the central module by
+
adding an option to select which display manager backend to use.</para>
+
</listitem>
+
</itemizedlist>
+
</para>
+
+
<para>Both approachs have problems.</para>
+
+
<para>Making backends independent can quickly become hard to manage. For
+
display managers, there can be only one enabled at a time, but the type
+
system can not enforce this restriction as there is no relation between
+
each backend <literal>enable</literal> option. As a result, this restriction
+
has to be done explicitely by adding assertions in each display manager
+
backend module.</para>
+
+
<para>On the other hand, managing the display managers backends in the
+
central module will require to change the central module option every time
+
a new backend is added or removed.</para>
+
+
<para>By using extensible option types, it is possible to create a placeholder
+
option in the central module (<xref linkend='ex-option-declaration-eot-service'
+
/>), and to extend it in each backend module (<xref
+
linkend='ex-option-declaration-eot-backend-slim' />, <xref
+
linkend='ex-option-declaration-eot-backend-kdm' />).</para>
+
+
<para>As a result, <literal>displayManager.enable</literal> option values can
+
be added without changing the main service module file and the type system
+
automatically enforce that there can only be a single display manager
+
enabled.</para>
+
+
<example xml:id='ex-option-declaration-eot-service'><title>Extensible type
+
placeholder in the service module</title>
+
<screen>
+
services.xserver.displayManager.enable = mkOption {
+
description = "Display manager to use";
+
type = with types; nullOr (enum [ ]);
+
};</screen></example>
+
+
<example xml:id='ex-option-declaration-eot-backend-slim'><title>Extending
+
<literal>services.xserver.displayManager.enable</literal> in the
+
<literal>slim</literal> module</title>
+
<screen>
+
services.xserver.displayManager.enable = mkOption {
+
type = with types; nullOr (enum [ "slim" ]);
+
};</screen></example>
+
+
<example xml:id='ex-option-declaration-eot-backend-kdm'><title>Extending
+
<literal>services.foo.backend</literal> in the <literal>kdm</literal>
+
module</title>
+
<screen>
+
services.xserver.displayManager.enable = mkOption {
+
type = with types; nullOr (enum [ "kdm" ]);
+
};</screen></example>
+
+
<para>The placeholder declaration is a standard <literal>mkOption</literal>
+
declaration, but it is important that extensible option declarations only use
+
the <literal>type</literal> argument.</para>
+
+
<para>Extensible option types work with any of the composed variants of
+
<literal>enum</literal> such as
+
<literal>with types; nullOr (enum [ "foo" "bar" ])</literal>
+
or <literal>with types; listOf (enum [ "foo" "bar" ])</literal>.</para>
+
+
</section>
</section>
+84 -26
nixos/doc/manual/development/option-types.xml
···
<listitem><para>A string. Multiple definitions are concatenated with a
collon <literal>":"</literal>.</para></listitem>
</varlistentry>
+
</variablelist>
+
+
</section>
+
+
<section><title>Value Types</title>
+
+
<para>Value types are type that take a value parameter. The only value type
+
in the library is <literal>enum</literal>.</para>
+
+
<variablelist>
<varlistentry>
-
<term><varname>types.separatedString</varname>
+
<term><varname>types.enum</varname> <replaceable>l</replaceable></term>
+
<listitem><para>One element of the list <replaceable>l</replaceable>, e.g.
+
<literal>types.enum [ "left" "right" ]</literal>. Multiple definitions
+
cannot be merged.</para></listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>types.separatedString</varname>
<replaceable>sep</replaceable></term>
-
<listitem><para>A string with a custom separator
-
<replaceable>sep</replaceable>, e.g. <literal>types.separatedString
+
<listitem><para>A string with a custom separator
+
<replaceable>sep</replaceable>, e.g. <literal>types.separatedString
"|"</literal>.</para></listitem>
</varlistentry>
+
<varlistentry>
+
<term><varname>types.submodule</varname> <replaceable>o</replaceable></term>
+
<listitem><para>A set of sub options <replaceable>o</replaceable>.
+
<replaceable>o</replaceable> can be an attribute set or a function
+
returning an attribute set. Submodules are used in composed types to
+
create modular options. Submodule are detailed in <xref
+
linkend='section-option-types-submodule' />.</para></listitem>
+
</varlistentry>
</variablelist>
-
</section>
<section><title>Composed Types</title>
-
<para>Composed types allow to create complex types by taking another type(s)
-
or value(s) as parameter(s).
-
It is possible to compose types multiple times, e.g. <literal>with types;
-
nullOr (enum [ "left" "right" ])</literal>.</para>
+
<para>Composed types are types that take a type as parameter. <literal>listOf
+
int</literal> and <literal>either int str</literal> are examples of
+
composed types.</para>
<variablelist>
<varlistentry>
···
once.</para></listitem>
</varlistentry>
<varlistentry>
-
<term><varname>types.enum</varname> <replaceable>l</replaceable></term>
-
<listitem><para>One element of the list <replaceable>l</replaceable>, e.g.
-
<literal>types.enum [ "left" "right" ]</literal>. Multiple definitions
-
cannot be merged</para></listitem>
-
</varlistentry>
-
<varlistentry>
<term><varname>types.either</varname> <replaceable>t1</replaceable>
<replaceable>t2</replaceable></term>
<listitem><para>Type <replaceable>t1</replaceable> or type
<replaceable>t2</replaceable>, e.g. <literal>with types; either int
str</literal>. Multiple definitions cannot be
merged.</para></listitem>
-
</varlistentry>
-
<varlistentry>
-
<term><varname>types.submodule</varname> <replaceable>o</replaceable></term>
-
<listitem><para>A set of sub options <replaceable>o</replaceable>.
-
<replaceable>o</replaceable> can be an attribute set or a function
-
returning an attribute set. Submodules are used in composed types to
-
create modular options. Submodule are detailed in <xref
-
linkend='section-option-types-submodule' />.</para></listitem>
</varlistentry>
</variablelist>
···
description = "submodule example";
type = with types; listOf (submodule modOptions);
};</screen></example>
-
<section><title>Composed with <literal>listOf</literal></title>
···
<variablelist>
<varlistentry>
<term><varname>name</varname></term>
-
<listitem><para>A string representation of the type function name, name
-
usually changes accordingly parameters passed to
-
types.</para></listitem>
+
<listitem><para>A string representation of the type function
+
name.</para></listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>definition</varname></term>
+
<listitem><para>Description of the type used in documentation. Give
+
information of the type and any of its arguments.</para></listitem>
</varlistentry>
<varlistentry>
<term><varname>check</varname></term>
···
<literal>composedType</literal> that take an <literal>elemtype</literal>
type parameter, this function should be defined as <literal>m:
composedType (elemType.substSubModules m)</literal>.</para></listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>typeMerge</varname></term>
+
<listitem><para>A function to merge multiple type declarations. Takes the
+
type to merge <literal>functor</literal> as parameter. A
+
<literal>null</literal> return value means that type cannot be
+
merged.</para>
+
<variablelist>
+
<varlistentry>
+
<term><replaceable>f</replaceable></term>
+
<listitem><para>The type to merge
+
<literal>functor</literal>.</para></listitem>
+
</varlistentry>
+
</variablelist>
+
<para>Note: There is a generic <literal>defaultTypeMerge</literal> that
+
work with most of value and composed types.</para>
+
</listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>functor</varname></term>
+
<listitem><para>An attribute set representing the type. It is used for type
+
operations and has the following keys:</para>
+
<variablelist>
+
<varlistentry>
+
<term><varname>type</varname></term>
+
<listitem><para>The type function.</para></listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>wrapped</varname></term>
+
<listitem><para>Holds the type parameter for composed types.</para>
+
</listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>payload</varname></term>
+
<listitem><para>Holds the value parameter for value types.
+
The types that have a <literal>payload</literal> are the
+
<literal>enum</literal>, <literal>separatedString</literal> and
+
<literal>submodule</literal> types.</para></listitem>
+
</varlistentry>
+
<varlistentry>
+
<term><varname>binOp</varname></term>
+
<listitem><para>A binary operation that can merge the payloads of two
+
same types. Defined as a function that take two payloads as
+
parameters and return the payloads merged.</para></listitem>
+
</varlistentry>
+
</variablelist>
+
</listitem>
</varlistentry>
</variablelist>
+4 -1
nixos/doc/manual/release-notes/rl-1703.xml
···
<itemizedlist>
<listitem>
-
<para></para>
+
<para>Module type system have a new extensible option types feature that
+
allow to extend certain types, such as enum, through multiple option
+
declarations of the same option across multiple modules.
+
</para>
</listitem>
</itemizedlist>
+1 -1
nixos/modules/installer/tools/nixos-option.sh
···
// optionalAttrs (opt ? default) { inherit (opt) default; }
// optionalAttrs (opt ? example) { inherit (opt) example; }
// optionalAttrs (opt ? description) { inherit (opt) description; }
-
// optionalAttrs (opt ? type) { typename = opt.type.name; }
+
// optionalAttrs (opt ? type) { typename = opt.type.description; }
// optionalAttrs (opt ? options) { inherit (opt) options; }
// {
# to disambiguate the xml output.