typioca: 2.7.0 -> 2.8.0
[NixPkgs.git] / lib / generators.nix
blob8e93ed04916e22301500ff94a97f7716234600a1
1 /* Functions that generate widespread file
2  * formats from nix data structures.
3  *
4  * They all follow a similar interface:
5  * generator { config-attrs } data
6  *
7  * `config-attrs` are “holes” in the generators
8  * with sensible default implementations that
9  * can be overwritten. The default implementations
10  * are mostly generators themselves, called with
11  * their respective default values; they can be reused.
12  *
13  * Tests can be found in ./tests/misc.nix
14  * Documentation in the manual, #sec-generators
15  */
16 { lib }:
17 with (lib).trivial;
18 let
19   libStr = lib.strings;
20   libAttr = lib.attrsets;
22   inherit (lib) isFunction;
25 rec {
27   ## -- HELPER FUNCTIONS & DEFAULTS --
29   /* Convert a value to a sensible default string representation.
30    * The builtin `toString` function has some strange defaults,
31    * suitable for bash scripts but not much else.
32    */
33   mkValueStringDefault = {}: v: with builtins;
34     let err = t: v: abort
35           ("generators.mkValueStringDefault: " +
36            "${t} not supported: ${toPretty {} v}");
37     in   if isInt      v then toString v
38     # convert derivations to store paths
39     else if lib.isDerivation v then toString v
40     # we default to not quoting strings
41     else if isString   v then v
42     # isString returns "1", which is not a good default
43     else if true  ==   v then "true"
44     # here it returns to "", which is even less of a good default
45     else if false ==   v then "false"
46     else if null  ==   v then "null"
47     # if you have lists you probably want to replace this
48     else if isList     v then err "lists" v
49     # same as for lists, might want to replace
50     else if isAttrs    v then err "attrsets" v
51     # functions can’t be printed of course
52     else if isFunction v then err "functions" v
53     # Floats currently can't be converted to precise strings,
54     # condition warning on nix version once this isn't a problem anymore
55     # See https://github.com/NixOS/nix/pull/3480
56     else if isFloat    v then libStr.floatToString v
57     else err "this value is" (toString v);
60   /* Generate a line of key k and value v, separated by
61    * character sep. If sep appears in k, it is escaped.
62    * Helper for synaxes with different separators.
63    *
64    * mkValueString specifies how values should be formatted.
65    *
66    * mkKeyValueDefault {} ":" "f:oo" "bar"
67    * > "f\:oo:bar"
68    */
69   mkKeyValueDefault = {
70     mkValueString ? mkValueStringDefault {}
71   }: sep: k: v:
72     "${libStr.escape [sep] k}${sep}${mkValueString v}";
75   ## -- FILE FORMAT GENERATORS --
78   /* Generate a key-value-style config file from an attrset.
79    *
80    * mkKeyValue is the same as in toINI.
81    */
82   toKeyValue = {
83     mkKeyValue ? mkKeyValueDefault {} "=",
84     listsAsDuplicateKeys ? false,
85     indent ? ""
86   }:
87   let mkLine = k: v: indent + mkKeyValue k v + "\n";
88       mkLines = if listsAsDuplicateKeys
89         then k: v: map (mkLine k) (if lib.isList v then v else [v])
90         else k: v: [ (mkLine k v) ];
91   in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs));
94   /* Generate an INI-style config file from an
95    * attrset of sections to an attrset of key-value pairs.
96    *
97    * generators.toINI {} {
98    *   foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
99    *   baz = { "also, integers" = 42; };
100    * }
101    *
102    *> [baz]
103    *> also, integers=42
104    *>
105    *> [foo]
106    *> ciao=bar
107    *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
108    *
109    * The mk* configuration attributes can generically change
110    * the way sections and key-value strings are generated.
111    *
112    * For more examples see the test cases in ./tests/misc.nix.
113    */
114   toINI = {
115     # apply transformations (e.g. escapes) to section names
116     mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
117     # format a setting line from key and value
118     mkKeyValue    ? mkKeyValueDefault {} "=",
119     # allow lists as values for duplicate keys
120     listsAsDuplicateKeys ? false
121   }: attrsOfAttrs:
122     let
123         # map function to string for each key val
124         mapAttrsToStringsSep = sep: mapFn: attrs:
125           libStr.concatStringsSep sep
126             (libAttr.mapAttrsToList mapFn attrs);
127         mkSection = sectName: sectValues: ''
128           [${mkSectionName sectName}]
129         '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
130     in
131       # map input to ini sections
132       mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
134   /* Generate an INI-style config file from an attrset
135    * specifying the global section (no header), and an
136    * attrset of sections to an attrset of key-value pairs.
137    *
138    * generators.toINIWithGlobalSection {} {
139    *   globalSection = {
140    *     someGlobalKey = "hi";
141    *   };
142    *   sections = {
143    *     foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
144    *     baz = { "also, integers" = 42; };
145    * }
146    *
147    *> someGlobalKey=hi
148    *>
149    *> [baz]
150    *> also, integers=42
151    *>
152    *> [foo]
153    *> ciao=bar
154    *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
155    *
156    * The mk* configuration attributes can generically change
157    * the way sections and key-value strings are generated.
158    *
159    * For more examples see the test cases in ./tests/misc.nix.
160    *
161    * If you don’t need a global section, you can also use
162    * `generators.toINI` directly, which only takes
163    * the part in `sections`.
164    */
165   toINIWithGlobalSection = {
166     # apply transformations (e.g. escapes) to section names
167     mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
168     # format a setting line from key and value
169     mkKeyValue    ? mkKeyValueDefault {} "=",
170     # allow lists as values for duplicate keys
171     listsAsDuplicateKeys ? false
172   }: { globalSection, sections ? {} }:
173     ( if globalSection == {}
174       then ""
175       else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection)
176            + "\n")
177     + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
179   /* Generate a git-config file from an attrset.
180    *
181    * It has two major differences from the regular INI format:
182    *
183    * 1. values are indented with tabs
184    * 2. sections can have sub-sections
185    *
186    * generators.toGitINI {
187    *   url."ssh://git@github.com/".insteadOf = "https://github.com";
188    *   user.name = "edolstra";
189    * }
190    *
191    *> [url "ssh://git@github.com/"]
192    *>   insteadOf = "https://github.com"
193    *>
194    *> [user]
195    *>   name = "edolstra"
196    */
197   toGitINI = attrs:
198     with builtins;
199     let
200       mkSectionName = name:
201         let
202           containsQuote = libStr.hasInfix ''"'' name;
203           sections = libStr.splitString "." name;
204           section = head sections;
205           subsections = tail sections;
206           subsection = concatStringsSep "." subsections;
207         in if containsQuote || subsections == [ ] then
208           name
209         else
210           ''${section} "${subsection}"'';
212       mkValueString = v:
213         let
214           escapedV = ''
215             "${
216               replaceStrings [ "\n" "   " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v
217             }"'';
218         in mkValueStringDefault { } (if isString v then escapedV else v);
220       # generation for multiple ini values
221       mkKeyValue = k: v:
222         let mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k;
223         in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v));
225       # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
226       gitFlattenAttrs = let
227         recurse = path: value:
228           if isAttrs value && !lib.isDerivation value then
229             lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
230           else if length path > 1 then {
231             ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value;
232           } else {
233             ${head path} = value;
234           };
235       in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));
237       toINI_ = toINI { inherit mkKeyValue mkSectionName; };
238     in
239       toINI_ (gitFlattenAttrs attrs);
241   # mkKeyValueDefault wrapper that handles dconf INI quirks.
242   # The main differences of the format is that it requires strings to be quoted.
243   mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (lib.gvariant.mkValue v); } "=";
245   # Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
246   # for details.
247   toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
249   /* Generates JSON from an arbitrary (non-function) value.
250     * For more information see the documentation of the builtin.
251     */
252   toJSON = {}: builtins.toJSON;
255   /* YAML has been a strict superset of JSON since 1.2, so we
256     * use toJSON. Before it only had a few differences referring
257     * to implicit typing rules, so it should work with older
258     * parsers as well.
259     */
260   toYAML = toJSON;
262   withRecursion =
263     {
264       /* If this option is not null, the given value will stop evaluating at a certain depth */
265       depthLimit
266       /* If this option is true, an error will be thrown, if a certain given depth is exceeded */
267     , throwOnDepthLimit ? true
268     }:
269       assert builtins.isInt depthLimit;
270       let
271         specialAttrs = [
272           "__functor"
273           "__functionArgs"
274           "__toString"
275           "__pretty"
276         ];
277         stepIntoAttr = evalNext: name:
278           if builtins.elem name specialAttrs
279             then id
280             else evalNext;
281         transform = depth:
282           if depthLimit != null && depth > depthLimit then
283             if throwOnDepthLimit
284               then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
285               else const "<unevaluated>"
286           else id;
287         mapAny = with builtins; depth: v:
288           let
289             evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
290           in
291             if isAttrs v then mapAttrs (stepIntoAttr evalNext) v
292             else if isList v then map evalNext v
293             else transform (depth + 1) v;
294       in
295         mapAny 0;
297   /* Pretty print a value, akin to `builtins.trace`.
298    * Should probably be a builtin as well.
299    * The pretty-printed string should be suitable for rendering default values
300    * in the NixOS manual. In particular, it should be as close to a valid Nix expression
301    * as possible.
302    */
303   toPretty = {
304     /* If this option is true, attrsets like { __pretty = fn; val = …; }
305        will use fn to convert val to a pretty printed representation.
306        (This means fn is type Val -> String.) */
307     allowPrettyValues ? false,
308     /* If this option is true, the output is indented with newlines for attribute sets and lists */
309     multiline ? true,
310     /* Initial indentation level */
311     indent ? ""
312   }:
313     let
314     go = indent: v: with builtins;
315     let     isPath   = v: typeOf v == "path";
316             introSpace = if multiline then "\n${indent}  " else " ";
317             outroSpace = if multiline then "\n${indent}" else " ";
318     in if   isInt      v then toString v
319     # toString loses precision on floats, so we use toJSON instead. This isn't perfect
320     # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for
321     # pretty-printing purposes this is acceptable.
322     else if isFloat    v then builtins.toJSON v
323     else if isString   v then
324       let
325         lines = filter (v: ! isList v) (builtins.split "\n" v);
326         escapeSingleline = libStr.escape [ "\\" "\"" "\${" ];
327         escapeMultiline = libStr.replaceStrings [ "\${" "''" ] [ "''\${" "'''" ];
328         singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\"";
329         multilineResult = let
330           escapedLines = map escapeMultiline lines;
331           # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
332           # indentation level. Otherwise, '' is appended to the last line.
333           lastLine = lib.last escapedLines;
334         in "''" + introSpace + concatStringsSep introSpace (lib.init escapedLines)
335                 + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''";
336       in
337         if multiline && length lines > 1 then multilineResult else singlelineResult
338     else if true  ==   v then "true"
339     else if false ==   v then "false"
340     else if null  ==   v then "null"
341     else if isPath     v then toString v
342     else if isList     v then
343       if v == [] then "[ ]"
344       else "[" + introSpace
345         + libStr.concatMapStringsSep introSpace (go (indent + "  ")) v
346         + outroSpace + "]"
347     else if isFunction v then
348       let fna = lib.functionArgs v;
349           showFnas = concatStringsSep ", " (libAttr.mapAttrsToList
350                        (name: hasDefVal: if hasDefVal then name + "?" else name)
351                        fna);
352       in if fna == {}    then "<function>"
353                          else "<function, args: {${showFnas}}>"
354     else if isAttrs    v then
355       # apply pretty values if allowed
356       if allowPrettyValues && v ? __pretty && v ? val
357          then v.__pretty v.val
358       else if v == {} then "{ }"
359       else if v ? type && v.type == "derivation" then
360         "<derivation ${v.name or "???"}>"
361       else "{" + introSpace
362           + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
363               (name: value:
364                 "${libStr.escapeNixIdentifier name} = ${
365                   builtins.addErrorContext "while evaluating an attribute `${name}`"
366                     (go (indent + "  ") value)
367                 };") v)
368         + outroSpace + "}"
369     else abort "generators.toPretty: should never happen (v = ${v})";
370   in go indent;
372   # PLIST handling
373   toPlist = {}: v: let
374     isFloat = builtins.isFloat or (x: false);
375     isPath = x: builtins.typeOf x == "path";
376     expr = ind: x:  with builtins;
377       if x == null  then "" else
378       if isBool x   then bool ind x else
379       if isInt x    then int ind x else
380       if isString x then str ind x else
381       if isList x   then list ind x else
382       if isAttrs x  then attrs ind x else
383       if isPath x   then str ind (toString x) else
384       if isFloat x  then float ind x else
385       abort "generators.toPlist: should never happen (v = ${v})";
387     literal = ind: x: ind + x;
389     bool = ind: x: literal ind  (if x then "<true/>" else "<false/>");
390     int = ind: x: literal ind "<integer>${toString x}</integer>";
391     str = ind: x: literal ind "<string>${x}</string>";
392     key = ind: x: literal ind "<key>${x}</key>";
393     float = ind: x: literal ind "<real>${toString x}</real>";
395     indent = ind: expr "\t${ind}";
397     item = ind: libStr.concatMapStringsSep "\n" (indent ind);
399     list = ind: x: libStr.concatStringsSep "\n" [
400       (literal ind "<array>")
401       (item ind x)
402       (literal ind "</array>")
403     ];
405     attrs = ind: x: libStr.concatStringsSep "\n" [
406       (literal ind "<dict>")
407       (attr ind x)
408       (literal ind "</dict>")
409     ];
411     attr = let attrFilter = name: value: name != "_module" && value != null;
412     in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
413       (name: value: lib.optionals (attrFilter name value) [
414       (key "\t${ind}" name)
415       (expr "\t${ind}" value)
416     ]) x));
418   in ''<?xml version="1.0" encoding="UTF-8"?>
419 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
420 <plist version="1.0">
421 ${expr "" v}
422 </plist>'';
424   /* Translate a simple Nix expression to Dhall notation.
425    * Note that integers are translated to Integer and never
426    * the Natural type.
427   */
428   toDhall = { }@args: v:
429     with builtins;
430     let concatItems = lib.strings.concatStringsSep ", ";
431     in if isAttrs v then
432       "{ ${
433         concatItems (lib.attrsets.mapAttrsToList
434           (key: value: "${key} = ${toDhall args value}") v)
435       } }"
436     else if isList v then
437       "[ ${concatItems (map (toDhall args) v)} ]"
438     else if isInt v then
439       "${if v < 0 then "" else "+"}${toString v}"
440     else if isBool v then
441       (if v then "True" else "False")
442     else if isFunction v then
443       abort "generators.toDhall: cannot convert a function to Dhall"
444     else if v == null then
445       abort "generators.toDhall: cannot convert a null to Dhall"
446     else
447       builtins.toJSON v;
449   /*
450    Translate a simple Nix expression to Lua representation with occasional
451    Lua-inlines that can be constructed by mkLuaInline function.
453    Configuration:
454      * multiline - by default is true which results in indented block-like view.
455      * indent - initial indent.
456      * asBindings - by default generate single value, but with this use attrset to set global vars.
458    Attention:
459      Regardless of multiline parameter there is no trailing newline.
461    Example:
462      generators.toLua {}
463        {
464          cmd = [ "typescript-language-server" "--stdio" ];
465          settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)'';
466        }
467      ->
468       {
469         ["cmd"] = {
470           "typescript-language-server",
471           "--stdio"
472         },
473         ["settings"] = {
474           ["workspace"] = {
475             ["library"] = (vim.api.nvim_get_runtime_file("", true))
476           }
477         }
478       }
480    Type:
481      toLua :: AttrSet -> Any -> String
482   */
483   toLua = {
484     /* If this option is true, the output is indented with newlines for attribute sets and lists */
485     multiline ? true,
486     /* Initial indentation level */
487     indent ? "",
488     /* Interpret as variable bindings */
489     asBindings ? false,
490   }@args: v:
491     with builtins;
492     let
493       innerIndent = "${indent}  ";
494       introSpace = if multiline then "\n${innerIndent}" else " ";
495       outroSpace = if multiline then "\n${indent}" else " ";
496       innerArgs = args // {
497         indent = if asBindings then indent else innerIndent;
498         asBindings = false;
499       };
500       concatItems = concatStringsSep ",${introSpace}";
501       isLuaInline = { _type ? null, ... }: _type == "lua-inline";
503       generatedBindings =
504           assert lib.assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
505           libStr.concatStrings (
506             lib.attrsets.mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
507             );
509       # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
510       matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
511       badVarNames = filter (name: matchVarName name == null) (attrNames v);
512     in
513     if asBindings then
514       generatedBindings
515     else if v == null then
516       "nil"
517     else if isInt v || isFloat v || isString v || isBool v then
518       builtins.toJSON v
519     else if isList v then
520       (if v == [ ] then "{}" else
521       "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
522     else if isAttrs v then
523       (
524         if isLuaInline v then
525           "(${v.expr})"
526         else if v == { } then
527           "{}"
528         else
529           "{${introSpace}${concatItems (
530             lib.attrsets.mapAttrsToList (key: value: "[${builtins.toJSON key}] = ${toLua innerArgs value}") v
531             )}${outroSpace}}"
532       )
533     else
534       abort "generators.toLua: type ${typeOf v} is unsupported";
536   /*
537    Mark string as Lua expression to be inlined when processed by toLua.
539    Type:
540      mkLuaInline :: String -> AttrSet
541   */
542   mkLuaInline = expr: { _type = "lua-inline"; inherit expr; };