module system: extensible option types

This commit is contained in:
Eric Sagnes 2016-09-07 10:03:32 +09:00 committed by Nicolas B. Pierron
parent d10356b825
commit e14de56613
7 changed files with 317 additions and 70 deletions

View file

@ -231,12 +231,20 @@ rec {
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
@ -258,7 +266,7 @@ rec {
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
@ -422,12 +430,14 @@ rec {
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

View file

@ -92,7 +92,7 @@ rec {
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 {})

View file

@ -17,10 +17,43 @@ rec {
};
# 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)
@ -36,12 +69,26 @@ rec {
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;
};
@ -52,29 +99,39 @@ rec {
};
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";
@ -86,7 +143,8 @@ rec {
string = separatedString "";
attrs = mkOptionType {
name = "attribute set";
name = "attrs";
description = "attribute set";
check = isAttrs;
merge = loc: foldl' (res: def: mergeAttrs res def.value) {};
};
@ -114,8 +172,9 @@ rec {
# 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:
@ -132,10 +191,12 @@ rec {
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:
@ -147,6 +208,7 @@ rec {
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 ...
@ -165,18 +227,21 @@ rec {
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
@ -189,18 +254,22 @@ rec {
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
@ -211,6 +280,7 @@ rec {
getSubOptions = elemType.getSubOptions;
getSubModules = elemType.getSubModules;
substSubModules = m: nullOr (elemType.substSubModules m);
functor = (defaultFunctor name) // { wrapped = elemType; };
};
submodule = opts:
@ -236,6 +306,12 @@ rec {
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:
@ -245,23 +321,35 @@ rec {
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.

View file

@ -65,4 +65,92 @@ options = {
</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>

View file

@ -62,23 +62,45 @@
<listitem><para>A string. Multiple definitions are concatenated with a
collon <literal>":"</literal>.</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
"|"</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.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
"|"</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>
@ -111,12 +133,6 @@
merged. It is used to ensure option definitions are declared only
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>
@ -125,14 +141,6 @@
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>
</section>
@ -191,7 +199,6 @@ options.mod = mkOption {
type = with types; listOf (submodule modOptions);
};</screen></example>
<section><title>Composed with <literal>listOf</literal></title>
<para>When composed with <literal>listOf</literal>, submodule allows multiple
@ -317,9 +324,13 @@ code before creating a new type.</para>
<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>
@ -382,6 +393,53 @@ code before creating a new type.</para>
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>
</section>

View file

@ -75,7 +75,10 @@ following incompatible changes:</para>
<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>

View file

@ -256,7 +256,7 @@ if isOption opt then
// 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.