1# Definitions related to run-time type checking. Used in particular
2# to type-check NixOS configurations.
3
4with import ./lists.nix;
5with import ./attrsets.nix;
6with import ./options.nix;
7with import ./trivial.nix;
8with import ./strings.nix;
9with {inherit (import ./modules.nix) mergeDefinitions filterOverrides; };
10
11rec {
12
13 isType = type: x: (x._type or "") == type;
14
15 setType = typeName: value: value // {
16 _type = typeName;
17 };
18
19
20 isOptionType = isType "option-type";
21 mkOptionType =
22 { # Human-readable representation of the type.
23 name
24 , # Function applied to each definition that should return true if
25 # its type-correct, false otherwise.
26 check ? (x: true)
27 , # Merge a list of definitions together into a single value.
28 # This function is called with two arguments: the location of
29 # the option in the configuration as a list of strings
30 # (e.g. ["boot" "loader "grub" "enable"]), and a list of
31 # definition values and locations (e.g. [ { file = "/foo.nix";
32 # value = 1; } { file = "/bar.nix"; value = 2 } ]).
33 merge ? mergeDefaultOption
34 , # Return a flat list of sub-options. Used to generate
35 # documentation.
36 getSubOptions ? prefix: {}
37 , # List of modules if any, or null if none.
38 getSubModules ? null
39 , # Function for building the same option type with a different list of
40 # modules.
41 substSubModules ? m: null
42 }:
43 { _type = "option-type";
44 inherit name check merge getSubOptions getSubModules substSubModules;
45 };
46
47
48 types = rec {
49
50 unspecified = mkOptionType {
51 name = "unspecified";
52 };
53
54 bool = mkOptionType {
55 name = "boolean";
56 check = isBool;
57 merge = mergeEqualOption;
58 };
59
60 int = mkOptionType {
61 name = "integer";
62 check = isInt;
63 merge = mergeOneOption;
64 };
65
66 str = mkOptionType {
67 name = "string";
68 check = isString;
69 merge = mergeOneOption;
70 };
71
72 # Merge multiple definitions by concatenating them (with the given
73 # separator between the values).
74 separatedString = sep: mkOptionType {
75 name = "string";
76 check = isString;
77 merge = loc: defs: concatStringsSep sep (getValues defs);
78 };
79
80 lines = separatedString "\n";
81 commas = separatedString ",";
82 envVar = separatedString ":";
83
84 # Deprecated; should not be used because it quietly concatenates
85 # strings, which is usually not what you want.
86 string = separatedString "";
87
88 attrs = mkOptionType {
89 name = "attribute set";
90 check = isAttrs;
91 merge = loc: foldl' (res: def: mergeAttrs res def.value) {};
92 };
93
94 # derivation is a reserved keyword.
95 package = mkOptionType {
96 name = "package";
97 check = x: isDerivation x || isStorePath x;
98 merge = loc: defs:
99 let res = mergeOneOption loc defs;
100 in if isDerivation res then res else toDerivation res;
101 };
102
103 shellPackage = package // {
104 check = x: (package.check x) && (hasAttr "shellPath" x);
105 };
106
107 path = mkOptionType {
108 name = "path";
109 # Hacky: there is no ‘isPath’ primop.
110 check = x: builtins.substring 0 1 (toString x) == "/";
111 merge = mergeOneOption;
112 };
113
114 # drop this in the future:
115 list = builtins.trace "`types.list' is deprecated; use `types.listOf' instead" types.listOf;
116
117 listOf = elemType: mkOptionType {
118 name = "list of ${elemType.name}s";
119 check = isList;
120 merge = loc: defs:
121 map (x: x.value) (filter (x: x ? value) (concatLists (imap (n: def:
122 if isList def.value then
123 imap (m: def':
124 (mergeDefinitions
125 (loc ++ ["[definition ${toString n}-entry ${toString m}]"])
126 elemType
127 [{ inherit (def) file; value = def'; }]
128 ).optionalValue
129 ) def.value
130 else
131 throw "The option value `${showOption loc}' in `${def.file}' is not a list.") defs)));
132 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["*"]);
133 getSubModules = elemType.getSubModules;
134 substSubModules = m: listOf (elemType.substSubModules m);
135 };
136
137 attrsOf = elemType: mkOptionType {
138 name = "attribute set of ${elemType.name}s";
139 check = isAttrs;
140 merge = loc: defs:
141 mapAttrs (n: v: v.value) (filterAttrs (n: v: v ? value) (zipAttrsWith (name: defs:
142 (mergeDefinitions (loc ++ [name]) elemType defs).optionalValue
143 )
144 # Push down position info.
145 (map (def: listToAttrs (mapAttrsToList (n: def':
146 { name = n; value = { inherit (def) file; value = def'; }; }) def.value)) defs)));
147 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name>"]);
148 getSubModules = elemType.getSubModules;
149 substSubModules = m: attrsOf (elemType.substSubModules m);
150 };
151
152 # List or attribute set of ...
153 loaOf = elemType:
154 let
155 convertIfList = defIdx: def:
156 if isList def.value then
157 { inherit (def) file;
158 value = listToAttrs (
159 imap (elemIdx: elem:
160 { name = elem.name or "unnamed-${toString defIdx}.${toString elemIdx}";
161 value = elem;
162 }) def.value);
163 }
164 else
165 def;
166 listOnly = listOf elemType;
167 attrOnly = attrsOf elemType;
168 in mkOptionType {
169 name = "list or attribute set of ${elemType.name}s";
170 check = x: isList x || isAttrs x;
171 merge = loc: defs: attrOnly.merge loc (imap convertIfList defs);
172 getSubOptions = prefix: elemType.getSubOptions (prefix ++ ["<name?>"]);
173 getSubModules = elemType.getSubModules;
174 substSubModules = m: loaOf (elemType.substSubModules m);
175 };
176
177 # List or element of ...
178 loeOf = elemType: mkOptionType {
179 name = "element or list of ${elemType.name}s";
180 check = x: isList x || elemType.check x;
181 merge = loc: defs:
182 let
183 defs' = filterOverrides defs;
184 res = (head defs').value;
185 in
186 if isList res then concatLists (getValues defs')
187 else if lessThan 1 (length defs') then
188 throw "The option `${showOption loc}' is defined multiple times, in ${showFiles (getFiles defs)}."
189 else if !isString res then
190 throw "The option `${showOption loc}' does not have a string value, in ${showFiles (getFiles defs)}."
191 else res;
192 };
193
194 uniq = elemType: mkOptionType {
195 inherit (elemType) name check;
196 merge = mergeOneOption;
197 getSubOptions = elemType.getSubOptions;
198 getSubModules = elemType.getSubModules;
199 substSubModules = m: uniq (elemType.substSubModules m);
200 };
201
202 nullOr = elemType: mkOptionType {
203 name = "null or ${elemType.name}";
204 check = x: x == null || elemType.check x;
205 merge = loc: defs:
206 let nrNulls = count (def: def.value == null) defs; in
207 if nrNulls == length defs then null
208 else if nrNulls != 0 then
209 throw "The option `${showOption loc}' is defined both null and not null, in ${showFiles (getFiles defs)}."
210 else elemType.merge loc defs;
211 getSubOptions = elemType.getSubOptions;
212 getSubModules = elemType.getSubModules;
213 substSubModules = m: nullOr (elemType.substSubModules m);
214 };
215
216 submodule = opts:
217 let
218 opts' = toList opts;
219 inherit (import ./modules.nix) evalModules;
220 in
221 mkOptionType rec {
222 name = "submodule";
223 check = x: isAttrs x || isFunction x;
224 merge = loc: defs:
225 let
226 coerce = def: if isFunction def then def else { config = def; };
227 modules = opts' ++ map (def: { _file = def.file; imports = [(coerce def.value)]; }) defs;
228 in (evalModules {
229 inherit modules;
230 args.name = last loc;
231 prefix = loc;
232 }).config;
233 getSubOptions = prefix: (evalModules
234 { modules = opts'; inherit prefix;
235 # FIXME: hack to get shit to evaluate.
236 args = { name = ""; }; }).options;
237 getSubModules = opts';
238 substSubModules = m: submodule m;
239 };
240
241 enum = values:
242 let
243 show = v:
244 if builtins.isString v then ''"${v}"''
245 else if builtins.isInt v then builtins.toString v
246 else ''<${builtins.typeOf v}>'';
247 in
248 mkOptionType {
249 name = "one of ${concatMapStringsSep ", " show values}";
250 check = flip elem values;
251 merge = mergeOneOption;
252 };
253
254 either = t1: t2: mkOptionType {
255 name = "${t1.name} or ${t2.name}";
256 check = x: t1.check x || t2.check x;
257 merge = mergeOneOption;
258 };
259
260 # Obsolete alternative to configOf. It takes its option
261 # declarations from the ‘options’ attribute of containing option
262 # declaration.
263 optionSet = mkOptionType {
264 name = /* builtins.trace "types.optionSet is deprecated; use types.submodule instead" */ "option set";
265 };
266
267 # Augment the given type with an additional type check function.
268 addCheck = elemType: check: elemType // { check = x: elemType.check x && check x; };
269
270 };
271
272}