summary refs log tree commit diff
path: root/lib/properties.nix
blob: 22aa8d891d8a5f4d2b7a0785e761f79b8e426327 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
# Nixpkgs/NixOS properties.  Generalize the problem of delayable (not yet
# evaluable) properties like mkIf.

let lib = import ./default.nix; in

with { inherit (builtins) head tail; };
with import ./trivial.nix;
with import ./lists.nix;
with import ./misc.nix;
with import ./attrsets.nix;

rec {

  inherit (lib) isType;

  # Tell that nothing is defined.  When properties are evaluated, this type
  # is used to remove an entry.  Thus if your property evaluation semantic
  # implies that you have to mute the content of an attribute, then your
  # property should produce this value.
  isNotdef = isType "notdef";
  mkNotdef = {_type = "notdef";};

  # General property type, it has a property attribute and a content
  # attribute.  The property attribute refers to an attribute set which
  # contains a _type attribute and a list of functions which are used to
  # evaluate this property.  The content attribute is used to stack properties
  # on top of each other.
  #
  # The optional functions which may be contained in the property attribute
  # are:
  #  - onDelay: run on a copied property.
  #  - onGlobalDelay: run on all copied properties.
  #  - onEval: run on an evaluated property.
  #  - onGlobalEval: run on a list of property stack on top of their values.
  isProperty = isType "property";
  mkProperty = p@{property, content, ...}: p // {
    _type = "property";
  };

  # Go through the stack of properties and apply the function `op' on all
  # property and call the function `nul' on the final value which is not a
  # property.  The stack is traversed in reversed order.  The `op' function
  # should expect a property with a content which have been modified.
  #
  # Warning: The `op' function expects only one argument in order to avoid
  # calls to mkProperties as the argument is already a valid property which
  # contains the result of the folding inside the content attribute.
  foldProperty = op: nul: attrs:
    if isProperty attrs then
      op (attrs // {
        content = foldProperty op nul attrs.content;
      })
    else
      nul attrs;

  # Simple function which can be used as the `op' argument of the
  # foldProperty function.  Properties that you don't want to handle can be
  # ignored with the `id' function.  `isSearched' is a function which should
  # check the type of a property and return a boolean value.  `thenFun' and
  # `elseFun' are functions which behave as the `op' argument of the
  # foldProperty function.
  foldFilter = isSearched: thenFun: elseFun: attrs:
    if isSearched attrs.property then
      thenFun attrs
    else
      elseFun attrs;


  # Move properties from the current attribute set to the attribute
  # contained in this attribute set.  This trigger property handlers called
  # `onDelay' and `onGlobalDelay'.
  delayPropertiesWithIter = iter: path: attrs:
    let cleanAttrs = rmProperties attrs; in
    if isProperty attrs then
      iter (a: v:
        lib.addErrorContext "while moving properties on the attribute `${a}':" (
          triggerPropertiesGlobalDelay a (
            triggerPropertiesDelay a (
              copyProperties attrs v
      )))) path cleanAttrs
    else
      attrs;

  delayProperties = # implicit attrs argument.
    let
      # mapAttrs except that it also recurse into potential mkMerge
      # functions.  This may cause a strictness issue because looking the
      # type of a string implies evaluating it.
      iter = fun: path: value:
        lib.mapAttrs (attr: val:
          if isProperty val && isMerge val.property then
            val // { content = map (fun attr) val.content; }
          else
            fun attr val
        ) value;
    in
      delayPropertiesWithIter iter "";

  # Call onDelay functions.
  triggerPropertiesDelay = name: attrs:
    let
      callOnDelay = p@{property, ...}:
        if property ? onDelay then
          property.onDelay name p
        else
          p;
    in
      foldProperty callOnDelay id attrs;

  # Call onGlobalDelay functions.
  triggerPropertiesGlobalDelay = name: attrs:
    let
      globalDelayFuns = uniqListExt {
        getter = property: property._type;
        inputList = foldProperty (p@{property, content, ...}:
          if property ? onGlobalDelay then
            [ property ] ++ content
          else
            content
        ) (a: []) attrs;
      };

      callOnGlobalDelay = property: content:
        property.onGlobalDelay name content;
    in
      fold callOnGlobalDelay attrs globalDelayFuns;

  # Expect a list of values which may have properties and return the same
  # list of values where all properties have been evaluated and where all
  # ignored values are removed.  This trigger property handlers called
  # `onEval' and `onGlobalEval'.
  evalProperties = valList:
    if valList != [] then
      filter (x: !isNotdef x) (
        triggerPropertiesGlobalEval (
          evalLocalProperties valList
        )
      )
    else
      valList;

  evalLocalProperties = valList:
    filter (x: !isNotdef x) (
      map triggerPropertiesEval valList
    );

  # Call onEval function
  triggerPropertiesEval = val:
    foldProperty (p@{property, ...}:
      if property ? onEval then
        property.onEval p
      else
        p
    ) id val;

  # Call onGlobalEval function
  triggerPropertiesGlobalEval = valList:
    let
      globalEvalFuns = uniqListExt {
        getter = property: property._type;
        inputList =
          fold (attrs: list:
            foldProperty (p@{property, content, ...}:
              if property ? onGlobalEval then
                [ property ] ++ content
              else
                content
            ) (a: list) attrs
          ) [] valList;
      };

      callOnGlobalEval = property: valList: property.onGlobalEval valList;
    in
      fold callOnGlobalEval valList globalEvalFuns;

  # Remove all properties on top of a value and return the value.
  rmProperties =
    foldProperty (p@{content, ...}: content) id;

  # Copy properties defined on a value on another value.
  copyProperties = attrs: newAttrs:
    foldProperty id (x: newAttrs) attrs;

  /* Merge. */

  # Create "merge" statement which is skipped by the delayProperty function
  # and interpreted by the underlying system using properties (modules).

  # Create a "Merge" property which only contains a condition.
  isMerge = isType "merge";
  mkMerge = content: mkProperty {
    property = {
      _type = "merge";
      onDelay = name: val: throw "mkMerge is not the first of the list of properties.";
      onEval = val: throw "mkMerge is not allowed on option definitions.";
    };
    inherit content;
  };

  /* If. ThenElse. Always. */

  # create "if" statement that can be delayed on sets until a "then-else" or
  # "always" set is reached.  When an always set is reached the condition
  # is ignore.

  # Create a "If" property which only contains a condition.
  isIf = isType "if";
  mkIf = condition: content: mkProperty {
    property = {
      _type = "if";
      onGlobalDelay = onIfGlobalDelay;
      onEval = onIfEval;
      inherit condition;
    };
    inherit content;
  };

  mkAssert = assertion: message: content:
    mkIf
      (if assertion then true else throw "\nFailed assertion: ${message}")
      content;

  # Evaluate the "If" statements when either "ThenElse" or "Always"
  # statement is encountered.  Otherwise it removes multiple If statements and
  # replaces them by one "If" statement where the condition is the list of all
  # conditions joined with a "and" operation.
  onIfGlobalDelay = name: content:
    let
      # extract if statements and non-if statements and repectively put them
      # in the attribute list and attrs.
      ifProps =
        foldProperty
          (foldFilter (p: isIf p)
            # then, push the condition inside the list list
            (p@{property, content, ...}:
              { inherit (content) attrs;
                list = [property] ++ content.list;
              }
            )
            # otherwise, add the propertie.
            (p@{property, content, ...}:
              { inherit (content) list;
                attrs = p // { content = content.attrs; };
              }
            )
          )
          (attrs: { list = []; inherit attrs; })
          content;

      # compute the list of if statements.
      evalIf = content: condition: list:
        if list == [] then
          mkIf condition content
        else
          let p = head list; in
          evalIf content (condition && p.condition) (tail list);
    in
      evalIf ifProps.attrs true ifProps.list;

  # Evaluate the condition of the "If" statement to either get the value or
  # to ignore the value.
  onIfEval = p@{property, content, ...}:
    if property.condition then
      content
    else
      mkNotdef;

  /* mkOverride */

  # Create an "Override" statement which allow the user to define
  # priorities between values.  The default priority is 100. The lowest
  # priorities are kept.  The template argument must reproduce the same
  # attribute set hierarchy to override leaves of the hierarchy.
  isOverride = isType "override";
  mkOverrideTemplate = priority: template: content: mkProperty {
    property = {
      _type = "override";
      onDelay = onOverrideDelay;
      onGlobalEval = onOverrideGlobalEval;
      inherit priority template;
    };
    inherit content;
  };

  # Like mkOverrideTemplate, but without the template argument.
  mkOverride = priority: content: mkOverrideTemplate priority {} content;

  # Sugar to override the default value of the option by making a new
  # default value based on the configuration.
  mkDefaultValue = mkOverride 1000;
  mkDefault = mkOverride 1000;
  mkForce = mkOverride 50;
  mkStrict = mkOverride 0;

  # Make the template traversal in function of the property traversal.  If
  # the template define a non-empty attribute set, then the property is
  # copied only on all mentionned attributes inside the template.
  # Otherwise, the property is kept on all sub-attribute definitions.
  onOverrideDelay = name: p@{property, content, ...}:
    let inherit (property) template; in
    if isAttrs template && template != {} then
      if hasAttr name template then
        p // {
          property = p.property // {
            template = builtins.getAttr name template;
          };
        }
      # Do not override the attribute \name\
      else
        content
    # Override values defined inside the attribute \name\.
    else
      p;

  # Keep values having lowest priority numbers only throwing away those having
  # a higher priority assigned.
  onOverrideGlobalEval = valList:
    let
      defaultPrio = 100;

      inherit (builtins) lessThan;

      getPrioVal =
        foldProperty
          (foldFilter isOverride
            (p@{property, content, ...}:
              if content ? priority && lessThan content.priority property.priority then
                content
              else
                content // {
                  inherit (property) priority;
                }
            )
            (p@{property, content, ...}:
              content // {
                value = p // { content = content.value; };
              }
            )
          ) (value: { inherit value; });

      addDefaultPrio = x:
        if x ? priority then x
        else x // { priority = defaultPrio; };

      prioValList = map (x: addDefaultPrio (getPrioVal x)) valList;

      higherPrio =
        if prioValList == [] then
          defaultPrio
        else
          fold (x: min:
            if lessThan x.priority min then
              x.priority
            else
              min
          ) (head prioValList).priority (tail prioValList);
    in
      map (x:
        if x.priority == higherPrio then
          x.value
        else
          mkNotdef
      ) prioValList;

  /* mkOrder */

  # Order definitions based on there index value.  This property is useful
  # when the result of the merge function depends on the order on the
  # initial list.  (e.g. concatStrings) Definitions are ordered based on
  # their rank.  The lowest ranked definition would be the first to element
  # of the list used by the merge function.  And the highest ranked
  # definition would be the last.  Definitions which does not have any rank
  # value have the default rank of 100.
  isOrder = isType "order";
  mkOrder = rank: content: mkProperty {
    property = {
      _type = "order";
      onGlobalEval = onOrderGlobalEval;
      inherit rank;
    };
    inherit content;
  };

  mkHeader = mkOrder 10;
  mkFooter = mkOrder 1000;

  # Fetch the rank of each definition (add the default rank is none) and
  # sort them based on their ranking.
  onOrderGlobalEval = valList:
    let
      defaultRank = 100;

      inherit (builtins) lessThan;

      getRankVal =
        foldProperty
          (foldFilter isOrder
            (p@{property, content, ...}:
              if content ? rank then
                content
              else
                content // {
                  inherit (property) rank;
                }
            )
            (p@{property, content, ...}:
              content // {
                value = p // { content = content.value; };
              }
            )
          ) (value: { inherit value; });

      addDefaultRank = x:
        if x ? rank then x
        else x // { rank = defaultRank; };

      rankValList = map (x: addDefaultRank (getRankVal x)) valList;

      cmp = x: y:
        builtins.lessThan x.rank y.rank;
    in
      map (x: x.value) (sort cmp rankValList);

  /* mkFixStrictness */

  # This is a hack used to restore laziness on some option definitions.
  # Some option definitions are evaluated when they are not used.  This
  # error is caused by the strictness of type checking builtins.  Builtins
  # like 'isAttrs' are too strict because they have to evaluate their
  # arguments to check if the type is correct.  This evaluation, cause the
  # strictness of properties.
  #
  # Properties can be stacked on top of each other.  The stackability of
  # properties on top of the option definition is nice for user manipulation
  # but require to check if the content of the property is not another
  # property.  Such testing implies to verify if this is an attribute set
  # and if it possess the type 'property'. (see isProperty & typeOf/isType)
  #
  # To avoid strict evaluation of option definitions, 'mkFixStrictness' is
  # introduced.  This property protects an option definition by replacing
  # the base of the stack of properties by 'mkNotDef', when this property is
  # evaluated it returns the original definition.
  #
  # This property is useful over any elements which depends on options which
  # are raising errors when they get evaluated without the proper settings.
  #
  # Plain list and attribute set are lazy structures, which means that the
  # container gets evaluated but not the content.  Thus, using this property
  # on top of plain list or attribute set is pointless.
  #
  # This is a Hack, you should avoid it!

  # This property has a long name because you should avoid it.
  isFixStrictness = attrs: (typeOf attrs) == "fix-strictness";
  mkFixStrictness = value:
    mkProperty {
      property = {
        _type = "fix-strictness";
        onEval = p: value;
      };
      content = mkNotdef;
    };

}