1 # Definitions related to run-time type checking. Used in particular
2 # to type-check NixOS configurations.
34 inherit (lib.attrsets)
70 __attrsFailEvaluation = true;
71 isType = type: x: (x._type or "") == type;
73 setType = typeName: value: value // {
78 # Default type merging function
79 # takes two type functors and return the merged type
80 defaultTypeMerge = f: f':
81 let wrapped = f.wrapped.typeMerge f'.wrapped.functor;
82 payload = f.binOp f.payload f'.payload;
84 # cannot merge different types
88 else if (f.wrapped == null && f'.wrapped == null)
89 && (f.payload == null && f'.payload == null)
92 else if (f.wrapped != null && f'.wrapped != null) && (wrapped != null)
95 else if (f.payload != null && f'.payload != null) && (payload != null)
99 # Default type functor
100 defaultFunctor = name: {
102 type = types.${name} or null;
108 isOptionType = isType "option-type";
110 { # Human-readable representation of the type, should be equivalent to
111 # the type function name.
113 , # Description of the type, defined recursively by embedding the wrapped type if any.
115 # A hint for whether or not this description needs parentheses. Possible values:
116 # - "noun": a noun phrase
117 # Example description: "positive integer",
118 # - "conjunction": a phrase with a potentially ambiguous "or" connective
119 # Example description: "int or string"
120 # - "composite": a phrase with an "of" connective
121 # Example description: "list of string"
122 # - "nonRestrictiveClause": a noun followed by a comma and a clause
123 # Example description: "positive integer, meaning >0"
124 # See the `optionDescriptionPhrase` function.
125 , descriptionClass ? null
126 , # DO NOT USE WITHOUT KNOWING WHAT YOU ARE DOING!
127 # Function applied to each definition that must return false when a definition
128 # does not match the type. It should not check more than the root of the value,
129 # because checking nested values reduces laziness, leading to unnecessary
130 # infinite recursions in the module system.
131 # Further checks of nested values should be performed by throwing in
132 # the merge function.
133 # Strict and deep type checking can be performed by calling lib.deepSeq on
136 # See https://github.com/NixOS/nixpkgs/pull/6794 that introduced this change,
137 # https://github.com/NixOS/nixpkgs/pull/173568 and
138 # https://github.com/NixOS/nixpkgs/pull/168295 that attempted to revert this,
139 # https://github.com/NixOS/nixpkgs/issues/191124 and
140 # https://github.com/NixOS/nixos-search/issues/391 for what happens if you ignore
143 , # Merge a list of definitions together into a single value.
144 # This function is called with two arguments: the location of
145 # the option in the configuration as a list of strings
146 # (e.g. ["boot" "loader "grub" "enable"]), and a list of
147 # definition values and locations (e.g. [ { file = "/foo.nix";
148 # value = 1; } { file = "/bar.nix"; value = 2 } ]).
149 merge ? mergeDefaultOption
150 , # Whether this type has a value representing nothingness. If it does,
151 # this should be a value of the form { value = <the nothing value>; }
152 # If it doesn't, this should be {}
153 # This may be used when a value is required for `mkIf false`. This allows the extra laziness in e.g. `lazyAttrsOf`.
155 , # Return a flat list of sub-options. Used to generate
157 getSubOptions ? prefix: {}
158 , # List of modules if any, or null if none.
160 , # Function for building the same option type with a different list of
162 substSubModules ? m: null
163 , # Function that merge type declarations.
164 # internal, takes a functor as argument and returns the merged type.
165 # returning null means the type is not mergeable
166 typeMerge ? defaultTypeMerge functor
167 , # The type functor.
168 # internal, representation of the type as an attribute set.
169 # name: name of the type
170 # type: type function.
171 # wrapped: the type wrapped in case of compound types.
172 # payload: values of the type, two payloads of the same type must be
173 # combinable with the binOp binary operation.
174 # binOp: binary operation that merge two payloads of the same type.
175 functor ? defaultFunctor name
176 , # The deprecation message to display when this type is used by an option
177 # If null, the type isn't deprecated
178 deprecationMessage ? null
179 , # The types that occur in the definition of this type. This is used to
180 # issue deprecation warnings recursively. Can also be used to reuse
184 { _type = "option-type";
186 name check merge emptyValue getSubOptions getSubModules substSubModules
187 typeMerge functor deprecationMessage nestedTypes descriptionClass;
188 description = if description == null then name else description;
191 # optionDescriptionPhrase :: (str -> bool) -> optionType -> str
193 # Helper function for producing unambiguous but readable natural language
194 # descriptions of types.
198 # optionDescriptionPhase unparenthesize optionType
200 # `unparenthesize`: A function from descriptionClass string to boolean.
201 # It must return true when the class of phrase will fit unambiguously into
202 # the description of the caller.
204 # `optionType`: The option type to parenthesize or not.
205 # The option whose description we're returning.
209 # The description of the `optionType`, with parentheses if there may be an
211 optionDescriptionPhrase = unparenthesize: t:
212 if unparenthesize (t.descriptionClass or null)
214 else "(${t.description})";
216 # When adding new types don't forget to document them in
217 # nixos/doc/manual/development/option-types.xml!
222 description = "raw value";
223 descriptionClass = "noun";
225 merge = mergeOneOption;
228 anything = mkOptionType {
230 description = "anything";
231 descriptionClass = "noun";
236 if isAttrs value && isStringLike value
237 then "stringCoercibleSet"
238 else builtins.typeOf value;
240 # Returns the common type of all definitions, throws an error if they
241 # don't have the same type
242 commonType = foldl' (type: def:
243 if getType def.value == type
245 else throw "The option `${showOption loc}' has conflicting option types in ${showFiles (getFiles defs)}"
246 ) (getType (head defs).value) defs;
249 # Recursively merge attribute sets
250 set = (attrsOf anything).merge;
251 # Safe and deterministic behavior for lists is to only accept one definition
252 # listOf only used to apply mkIf and co.
255 then throw "The option `${showOption loc}' has conflicting definitions, in ${showFiles (getFiles defs)}."
256 else (listOf anything).merge;
257 # This is the type of packages, only accept a single definition
258 stringCoercibleSet = mergeOneOption;
259 lambda = loc: defs: arg: anything.merge
260 (loc ++ [ "<function body>" ])
263 value = def.value arg;
265 # Otherwise fall back to only allowing all equal definitions
266 }.${commonType} or mergeEqualOption;
267 in mergeFunction loc defs;
270 unspecified = mkOptionType {
271 name = "unspecified";
272 description = "unspecified value";
273 descriptionClass = "noun";
276 bool = mkOptionType {
278 description = "boolean";
279 descriptionClass = "noun";
281 merge = mergeEqualOption;
284 boolByOr = mkOptionType {
286 description = "boolean (merged using or)";
287 descriptionClass = "noun";
292 # Under the assumption that .check always runs before merge, we can assume that all defs.*.value
293 # have been forced, and therefore we assume we don't introduce order-dependent strictness here
302 description = "signed integer";
303 descriptionClass = "noun";
305 merge = mergeEqualOption;
308 # Specialized subdomains of int
311 betweenDesc = lowest: highest:
312 "${toString lowest} and ${toString highest} (both inclusive)";
313 between = lowest: highest:
314 assert lib.assertMsg (lowest <= highest)
315 "ints.between: lowest must be smaller than highest";
316 addCheck int (x: x >= lowest && x <= highest) // {
318 description = "integer between ${betweenDesc lowest highest}";
320 ign = lowest: highest: name: docStart:
321 between lowest highest // {
323 description = docStart + "; between ${betweenDesc lowest highest}";
325 unsign = bit: range: ign 0 (range - 1)
326 "unsignedInt${toString bit}" "${toString bit} bit unsigned integer";
327 sign = bit: range: ign (0 - (range / 2)) (range / 2 - 1)
328 "signedInt${toString bit}" "${toString bit} bit signed integer";
331 /* An int with a fixed range.
334 * (ints.between 0 100).check (-1)
336 * (ints.between 0 100).check (101)
338 * (ints.between 0 0).check 0
343 unsigned = addCheck types.int (x: x >= 0) // {
344 name = "unsignedInt";
345 description = "unsigned integer, meaning >=0";
346 descriptionClass = "nonRestrictiveClause";
348 positive = addCheck types.int (x: x > 0) // {
349 name = "positiveInt";
350 description = "positive integer, meaning >0";
351 descriptionClass = "nonRestrictiveClause";
354 u16 = unsign 16 65536;
355 # the biggest int Nix accepts is 2^63 - 1 (9223372036854775808)
356 # the smallest int Nix accepts is -2^63 (-9223372036854775807)
357 u32 = unsign 32 4294967296;
358 # u64 = unsign 64 18446744073709551616;
362 s32 = sign 32 4294967296;
365 # Alias of u16 for a port number
368 float = mkOptionType {
370 description = "floating point number";
371 descriptionClass = "noun";
373 merge = mergeEqualOption;
376 number = either int float;
379 betweenDesc = lowest: highest:
380 "${builtins.toJSON lowest} and ${builtins.toJSON highest} (both inclusive)";
382 between = lowest: highest:
383 assert lib.assertMsg (lowest <= highest)
384 "numbers.between: lowest must be smaller than highest";
385 addCheck number (x: x >= lowest && x <= highest) // {
386 name = "numberBetween";
387 description = "integer or floating point number between ${betweenDesc lowest highest}";
390 nonnegative = addCheck number (x: x >= 0) // {
391 name = "numberNonnegative";
392 description = "nonnegative integer or floating point number, meaning >=0";
393 descriptionClass = "nonRestrictiveClause";
395 positive = addCheck number (x: x > 0) // {
396 name = "numberPositive";
397 description = "positive integer or floating point number, meaning >0";
398 descriptionClass = "nonRestrictiveClause";
404 description = "string";
405 descriptionClass = "noun";
407 merge = mergeEqualOption;
410 nonEmptyStr = mkOptionType {
411 name = "nonEmptyStr";
412 description = "non-empty string";
413 descriptionClass = "noun";
414 check = x: str.check x && builtins.match "[ \t\n]*" x == null;
418 # Allow a newline character at the end and trim it in the merge function.
421 inherit (strMatching "[^\n\r]*\n?") check merge;
424 name = "singleLineStr";
425 description = "(optionally newline-terminated) single-line string";
426 descriptionClass = "noun";
429 lib.removeSuffix "\n" (merge loc defs);
432 strMatching = pattern: mkOptionType {
433 name = "strMatching ${escapeNixString pattern}";
434 description = "string matching the pattern ${pattern}";
435 descriptionClass = "noun";
436 check = x: str.check x && builtins.match pattern x != null;
440 # Merge multiple definitions by concatenating them (with the given
441 # separator between the values).
442 separatedString = sep: mkOptionType rec {
443 name = "separatedString";
444 description = if sep == ""
445 then "Concatenated string" # for types.string.
446 else "strings concatenated with ${builtins.toJSON sep}"
448 descriptionClass = "noun";
450 merge = loc: defs: concatStringsSep sep (getValues defs);
451 functor = (defaultFunctor name) // {
453 binOp = sepLhs: sepRhs:
454 if sepLhs == sepRhs then sepLhs
459 lines = separatedString "\n";
460 commas = separatedString ",";
461 envVar = separatedString ":";
463 # Deprecated; should not be used because it quietly concatenates
464 # strings, which is usually not what you want.
465 # We use a lib.warn because `deprecationMessage` doesn't trigger in nested types such as `attrsOf string`
467 "The type `types.string` is deprecated. See https://github.com/NixOS/nixpkgs/pull/66346 for better alternative types."
468 (separatedString "" // {
472 passwdEntry = entryType: addCheck entryType (str: !(hasInfix ":" str || hasInfix "\n" str)) // {
473 name = "passwdEntry ${entryType.name}";
474 description = "${optionDescriptionPhrase (class: class == "noun") entryType}, not containing newlines or colons";
475 descriptionClass = "nonRestrictiveClause";
478 attrs = mkOptionType {
480 description = "attribute set";
482 merge = loc: foldl' (res: def: res // def.value) {};
483 emptyValue = { value = {}; };
486 # A package is a top-level store path (/nix/store/hash-name). This includes:
488 # - more generally, attribute sets with an `outPath` or `__toString` attribute
489 # pointing to a store path, e.g. flake inputs
490 # - strings with context, e.g. "${pkgs.foo}" or (toString pkgs.foo)
491 # - hardcoded store path literals (/nix/store/hash-foo) or strings without context
492 # ("/nix/store/hash-foo"). These get a context added to them using builtins.storePath.
493 # If you don't need a *top-level* store path, consider using pathInStore instead.
494 package = mkOptionType {
496 descriptionClass = "noun";
497 check = x: isDerivation x || isStorePath x;
499 let res = mergeOneOption loc defs;
500 in if builtins.isPath res || (builtins.isString res && ! builtins.hasContext res)
501 then toDerivation res
505 shellPackage = package // {
506 check = x: isDerivation x && hasAttr "shellPath" x;
510 (unique { message = "A Nixpkgs pkgs set can not be merged with another pkgs set."; } attrs // {
512 descriptionClass = "noun";
513 description = "Nixpkgs package set";
515 (x: (x._type or null) == "pkgs");
517 path = mkOptionType {
519 descriptionClass = "noun";
520 check = x: isStringLike x && builtins.substring 0 1 (toString x) == "/";
521 merge = mergeEqualOption;
524 pathInStore = mkOptionType {
525 name = "pathInStore";
526 description = "path in the Nix store";
527 descriptionClass = "noun";
528 check = x: isStringLike x && builtins.match "${builtins.storeDir}/[^.].*" (toString x) != null;
529 merge = mergeEqualOption;
532 listOf = elemType: mkOptionType rec {
534 description = "list of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
535 descriptionClass = "composite";
538 map (x: x.value) (filter (x: x ? value) (concatLists (imap1 (n: def:
541 (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
543 [{ inherit (def) file; value = def'; }]
547 emptyValue = { value = []; };
548 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
549 getSubModules = elemType.getSubModules;
550 substSubModules = m: listOf (elemType.substSubModules m);
551 functor = (defaultFunctor name) // { wrapped = elemType; };
552 nestedTypes.elemType = elemType;
555 nonEmptyListOf = elemType:
556 let list = addCheck (types.listOf elemType) (l: l != []);
558 description = "non-empty ${optionDescriptionPhrase (class: class == "noun") list}";
559 emptyValue = { }; # no .value attr, meaning unset
560 substSubModules = m: nonEmptyListOf (elemType.substSubModules m);
563 attrsOf = elemType: mkOptionType rec {
565 description = "attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
566 descriptionClass = "composite";
569 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
570 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
572 # Push down position info.
573 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs)));
574 emptyValue = { value = {}; };
575 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
576 getSubModules = elemType.getSubModules;
577 substSubModules = m: attrsOf (elemType.substSubModules m);
578 functor = (defaultFunctor name) // { wrapped = elemType; };
579 nestedTypes.elemType = elemType;
582 # A version of attrsOf that's lazy in its values at the expense of
583 # conditional definitions not working properly. E.g. defining a value with
584 # `foo.attr = mkIf false 10`, then `foo ? attr == true`, whereas with
585 # attrsOf it would correctly be `false`. Accessing `foo.attr` would throw an
586 # error that it's not defined. Use only if conditional definitions don't make sense.
587 lazyAttrsOf = elemType: mkOptionType rec {
588 name = "lazyAttrsOf";
589 description = "lazy attribute set of ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
590 descriptionClass = "composite";
593 zipAttrsWith (name: defs:
594 let merged = mergeDefinitions (loc ++ [name]) elemType defs;
595 # mergedValue will trigger an appropriate error when accessed
596 in merged.optionalValue.value or elemType.emptyValue.value or merged.mergedValue
598 # Push down position info.
599 (map (def: mapAttrs (n: v: { inherit (def) file; value = v; }) def.value) defs);
600 emptyValue = { value = {}; };
601 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
602 getSubModules = elemType.getSubModules;
603 substSubModules = m: lazyAttrsOf (elemType.substSubModules m);
604 functor = (defaultFunctor name) // { wrapped = elemType; };
605 nestedTypes.elemType = elemType;
608 # TODO: deprecate this in the future:
609 loaOf = elemType: types.attrsOf elemType // {
611 deprecationMessage = "Mixing lists with attribute values is no longer"
612 + " possible; please use `types.attrsOf` instead. See"
613 + " https://github.com/NixOS/nixpkgs/issues/1800 for the motivation.";
614 nestedTypes.elemType = elemType;
617 uniq = unique { message = ""; };
619 unique = { message }: type: mkOptionType rec {
621 inherit (type) description descriptionClass check;
622 merge = mergeUniqueOption { inherit message; inherit (type) merge; };
623 emptyValue = type.emptyValue;
624 getSubOptions = type.getSubOptions;
625 getSubModules = type.getSubModules;
626 substSubModules = m: uniq (type.substSubModules m);
627 functor = (defaultFunctor name) // { wrapped = type; };
628 nestedTypes.elemType = type;
631 # Null or value of ...
632 nullOr = elemType: mkOptionType rec {
634 description = "null or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") elemType}";
635 descriptionClass = "conjunction";
636 check = x: x == null || elemType.check x;
638 let nrNulls = count (def: def.value == null) defs; in
639 if nrNulls == length defs then null
640 else if nrNulls != 0 then
641 throw "The option `${showOption loc}` is defined both null and not null, in ${showFiles (getFiles defs)}."
642 else elemType.merge loc defs;
643 emptyValue = { value = null; };
644 getSubOptions = elemType.getSubOptions;
645 getSubModules = elemType.getSubModules;
646 substSubModules = m: nullOr (elemType.substSubModules m);
647 functor = (defaultFunctor name) // { wrapped = elemType; };
648 nestedTypes.elemType = elemType;
651 functionTo = elemType: mkOptionType {
653 description = "function that evaluates to a(n) ${optionDescriptionPhrase (class: class == "noun" || class == "composite") elemType}";
654 descriptionClass = "composite";
657 fnArgs: (mergeDefinitions (loc ++ [ "<function body>" ]) elemType (map (fn: { inherit (fn) file; value = fn.value fnArgs; }) defs)).mergedValue;
658 getSubOptions = prefix: elemType.getSubOptions (prefix ++ [ "<function body>" ]);
659 getSubModules = elemType.getSubModules;
660 substSubModules = m: functionTo (elemType.substSubModules m);
661 functor = (defaultFunctor "functionTo") // { wrapped = elemType; };
662 nestedTypes.elemType = elemType;
665 # A submodule (like typed attribute set). See NixOS manual.
666 submodule = modules: submoduleWith {
667 shorthandOnlyDefinesConfig = true;
668 modules = toList modules;
671 # A module to be imported in some other part of the configuration.
672 deferredModule = deferredModuleWith { };
674 # A module to be imported in some other part of the configuration.
675 # `staticModules`' options will be added to the documentation, unlike
676 # options declared via `config`.
677 deferredModuleWith = attrs@{ staticModules ? [] }: mkOptionType {
678 name = "deferredModule";
679 description = "module";
680 descriptionClass = "noun";
681 check = x: isAttrs x || isFunction x || path.check x;
683 imports = staticModules ++ map (def: lib.setDefaultModuleLocation "${def.file}, via option ${showOption loc}" def.value) defs;
685 inherit (submoduleWith { modules = staticModules; })
688 substSubModules = m: deferredModuleWith (attrs // {
691 functor = defaultFunctor "deferredModuleWith" // {
692 type = types.deferredModuleWith;
694 inherit staticModules;
697 staticModules = lhs.staticModules ++ rhs.staticModules;
702 # The type of a type!
703 optionType = mkOptionType {
705 description = "optionType";
706 descriptionClass = "noun";
707 check = value: value._type or null == "option-type";
710 then (head defs).value
712 # Prepares the type definitions for mergeOptionDecls, which
713 # annotates submodules types with file locations
714 optionModules = map ({ value, file }:
717 # There's no way to merge types directly from the module system,
718 # but we can cheat a bit by just declaring an option with the type
719 options = lib.mkOption {
724 # Merges all the types into a single one, including submodule merging.
725 # This also propagates file information to all submodules
726 mergedOption = fixupOptionType loc (mergeOptionDecls loc optionModules);
727 in mergedOption.type;
733 , shorthandOnlyDefinesConfig ? false
738 inherit (lib.modules) evalModules;
740 allModules = defs: map ({ value, file }:
741 if isAttrs value && shorthandOnlyDefinesConfig
742 then { _file = file; config = value; }
743 else { _file = file; imports = [ value ]; }
747 inherit class specialArgs;
749 # This is a work-around for the fact that some sub-modules,
750 # such as the one included in an attribute set, expects an "args"
751 # attribute to be given to the sub-module. As the option
752 # evaluation does not have any specific attribute name yet, we
753 # provide a default for the documentation and the freeform type.
755 # This is necessary as some option declaration might use the
756 # "name" attribute given as argument of the submodule and use it
757 # as the default of option declarations.
759 # We use lookalike unicode single angle quotation marks because
760 # of the docbook transformation the options receive. In all uses
761 # > and < wouldn't be encoded correctly so the encoded values
762 # would be used, and use of `<` and `>` would break the XML document.
763 # It shouldn't cause an issue since this is cosmetic for the manual.
764 _module.args.name = lib.mkOptionDefault "‹name›";
768 freeformType = base._module.freeformType;
776 if description != null then description
777 else freeformType.description or name;
778 check = x: isAttrs x || isFunction x || path.check x;
780 (base.extendModules {
781 modules = [ { _module.args.name = last loc; } ] ++ allModules defs;
784 emptyValue = { value = {}; };
785 getSubOptions = prefix: (base.extendModules
786 { inherit prefix; }).options // optionalAttrs (freeformType != null) {
787 # Expose the sub options of the freeform type. Note that the option
788 # discovery doesn't care about the attribute name used here, so this
789 # is just to avoid conflicts with potential options from the submodule
790 _freeformOptions = freeformType.getSubOptions prefix;
792 getSubModules = modules;
793 substSubModules = m: submoduleWith (attrs // {
796 nestedTypes = lib.optionalAttrs (freeformType != null) {
797 freeformType = freeformType;
799 functor = defaultFunctor name // {
800 type = types.submoduleWith;
802 inherit modules class specialArgs shorthandOnlyDefinesConfig description;
806 # `or null` was added for backwards compatibility only. `class` is
807 # always set in the current version of the module system.
808 if lhs.class or null == null then rhs.class or null
809 else if rhs.class or null == null then lhs.class or null
810 else if lhs.class or null == rhs.class then lhs.class or null
811 else throw "A submoduleWith option is declared multiple times with conflicting class values \"${toString lhs.class}\" and \"${toString rhs.class}\".";
812 modules = lhs.modules ++ rhs.modules;
814 let intersecting = builtins.intersectAttrs lhs.specialArgs rhs.specialArgs;
815 in if intersecting == {}
816 then lhs.specialArgs // rhs.specialArgs
817 else throw "A submoduleWith option is declared multiple times with the same specialArgs \"${toString (attrNames intersecting)}\"";
818 shorthandOnlyDefinesConfig =
819 if lhs.shorthandOnlyDefinesConfig == null
820 then rhs.shorthandOnlyDefinesConfig
821 else if rhs.shorthandOnlyDefinesConfig == null
822 then lhs.shorthandOnlyDefinesConfig
823 else if lhs.shorthandOnlyDefinesConfig == rhs.shorthandOnlyDefinesConfig
824 then lhs.shorthandOnlyDefinesConfig
825 else throw "A submoduleWith option is declared multiple times with conflicting shorthandOnlyDefinesConfig values";
827 if lhs.description == null
829 else if rhs.description == null
831 else if lhs.description == rhs.description
833 else throw "A submoduleWith option is declared multiple times with conflicting descriptions";
838 # A value from a set of allowed ones.
841 inherit (lib.lists) unique;
843 if builtins.isString v then ''"${v}"''
844 else if builtins.isInt v then builtins.toString v
845 else if builtins.isBool v then boolToString v
846 else ''<${builtins.typeOf v}>'';
851 # Length 0 or 1 enums may occur in a design pattern with type merging
852 # where an "interface" module declares an empty enum and other modules
853 # provide implementations, each extending the enum with their own
856 "impossible (empty enum)"
857 else if builtins.length values == 1 then
858 "value ${show (builtins.head values)} (singular enum)"
860 "one of ${concatMapStringsSep ", " show values}";
862 if builtins.length values < 2
865 check = flip elem values;
866 merge = mergeEqualOption;
867 functor = (defaultFunctor name) // { payload = values; binOp = a: b: unique (a ++ b); };
870 # Either value of type `t1` or `t2`.
871 either = t1: t2: mkOptionType rec {
874 if t1.descriptionClass or null == "nonRestrictiveClause"
876 # Plain, but add comma
877 "${t1.description}, or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t2}"
879 "${optionDescriptionPhrase (class: class == "noun" || class == "conjunction") t1} or ${optionDescriptionPhrase (class: class == "noun" || class == "conjunction" || class == "composite") t2}";
880 descriptionClass = "conjunction";
881 check = x: t1.check x || t2.check x;
884 defList = map (d: d.value) defs;
886 if all (x: t1.check x) defList
887 then t1.merge loc defs
888 else if all (x: t2.check x) defList
889 then t2.merge loc defs
890 else mergeOneOption loc defs;
892 let mt1 = t1.typeMerge (elemAt f'.wrapped 0).functor;
893 mt2 = t2.typeMerge (elemAt f'.wrapped 1).functor;
895 if (name == f'.name) && (mt1 != null) && (mt2 != null)
896 then functor.type mt1 mt2
898 functor = (defaultFunctor name) // { wrapped = [ t1 t2 ]; };
899 nestedTypes.left = t1;
900 nestedTypes.right = t2;
903 # Any of the types in the given list
906 head' = if ts == [] then throw "types.oneOf needs to get at least one type in its argument" else head ts;
908 in foldl' either head' tail';
910 # Either value of type `coercedType` or `finalType`, the former is
911 # converted to `finalType` using `coerceFunc`.
912 coercedTo = coercedType: coerceFunc: finalType:
913 assert lib.assertMsg (coercedType.getSubModules == null)
914 "coercedTo: coercedType must not have submodules (it’s a ${
915 coercedType.description})";
918 description = "${optionDescriptionPhrase (class: class == "noun") finalType} or ${optionDescriptionPhrase (class: class == "noun") coercedType} convertible to it";
919 check = x: (coercedType.check x && finalType.check (coerceFunc x)) || finalType.check x;
923 if coercedType.check val then coerceFunc val
925 in finalType.merge loc (map (def: def // { value = coerceVal def.value; }) defs);
926 emptyValue = finalType.emptyValue;
927 getSubOptions = finalType.getSubOptions;
928 getSubModules = finalType.getSubModules;
929 substSubModules = m: coercedTo coercedType coerceFunc (finalType.substSubModules m);
930 typeMerge = t1: t2: null;
931 functor = (defaultFunctor name) // { wrapped = finalType; };
932 nestedTypes.coercedType = coercedType;
933 nestedTypes.finalType = finalType;
936 # Augment the given type with an additional type check function.
937 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
942 in outer_types // outer_types.types