libretro.mame: unstable-2024-09-15 -> unstable-2024-09-27 (#345139)
[NixPkgs.git] / lib / generators.nix
blob4317e49c2538f385e0a373e01abd4688a717ef3d
1 /**
2   Functions that generate widespread file
3   formats from nix data structures.
5   They all follow a similar interface:
7   ```nix
8   generator { config-attrs } data
9   ```
11   `config-attrs` are “holes” in the generators
12   with sensible default implementations that
13   can be overwritten. The default implementations
14   are mostly generators themselves, called with
15   their respective default values; they can be reused.
17   Tests can be found in ./tests/misc.nix
19   Further Documentation can be found [here](#sec-generators).
21 { lib }:
23 let
24   inherit (lib)
25     addErrorContext
26     assertMsg
27     attrNames
28     concatLists
29     concatMapStringsSep
30     concatStrings
31     concatStringsSep
32     const
33     elem
34     escape
35     filter
36     flatten
37     foldl
38     functionArgs  # Note: not the builtin; considers `__functor` in attrsets.
39     gvariant
40     hasInfix
41     head
42     id
43     init
44     isAttrs
45     isBool
46     isDerivation
47     isFloat
48     isFunction    # Note: not the builtin; considers `__functor` in attrsets.
49     isInt
50     isList
51     isPath
52     isString
53     last
54     length
55     mapAttrs
56     mapAttrsToList
57     optionals
58     recursiveUpdate
59     replaceStrings
60     reverseList
61     splitString
62     tail
63     toList
64     ;
66   inherit (lib.strings)
67     escapeNixIdentifier
68     floatToString
69     match
70     split
71     toJSON
72     typeOf
73     ;
75   ## -- HELPER FUNCTIONS & DEFAULTS --
76 in rec {
77   /**
78     Convert a value to a sensible default string representation.
79     The builtin `toString` function has some strange defaults,
80     suitable for bash scripts but not much else.
82     # Inputs
84     Options
85     : Empty set, there may be configuration options in the future
87     `v`
88     : 2\. Function argument
89   */
90   mkValueStringDefault = {}: v:
91     let err = t: v: abort
92           ("generators.mkValueStringDefault: " +
93            "${t} not supported: ${toPretty {} v}");
94     in   if isInt      v then toString v
95     # convert derivations to store paths
96     else if isDerivation v then toString v
97     # we default to not quoting strings
98     else if isString   v then v
99     # isString returns "1", which is not a good default
100     else if true  ==   v then "true"
101     # here it returns to "", which is even less of a good default
102     else if false ==   v then "false"
103     else if null  ==   v then "null"
104     # if you have lists you probably want to replace this
105     else if isList     v then err "lists" v
106     # same as for lists, might want to replace
107     else if isAttrs    v then err "attrsets" v
108     # functions can’t be printed of course
109     else if isFunction v then err "functions" v
110     # Floats currently can't be converted to precise strings,
111     # condition warning on nix version once this isn't a problem anymore
112     # See https://github.com/NixOS/nix/pull/3480
113     else if isFloat    v then floatToString v
114     else err "this value is" (toString v);
117   /**
118     Generate a line of key k and value v, separated by
119     character sep. If sep appears in k, it is escaped.
120     Helper for synaxes with different separators.
122     mkValueString specifies how values should be formatted.
124     ```nix
125     mkKeyValueDefault {} ":" "f:oo" "bar"
126     > "f\:oo:bar"
127     ```
129     # Inputs
131     Structured function argument
132     : mkValueString (optional, default: `mkValueStringDefault {}`)
133       : Function to convert values to strings
135     `sep`
137     : 2\. Function argument
139     `k`
141     : 3\. Function argument
143     `v`
145     : 4\. Function argument
146   */
147   mkKeyValueDefault = {
148     mkValueString ? mkValueStringDefault {}
149   }: sep: k: v:
150     "${escape [sep] k}${sep}${mkValueString v}";
153   ## -- FILE FORMAT GENERATORS --
156   /**
157     Generate a key-value-style config file from an attrset.
159     # Inputs
161     Structured function argument
163     : mkKeyValue (optional, default: `mkKeyValueDefault {} "="`)
164       : format a setting line from key and value
166     : listsAsDuplicateKeys (optional, default: `false`)
167       : allow lists as values for duplicate keys
169     : indent (optional, default: `""`)
170       : Initial indentation level
172   */
173   toKeyValue = {
174     mkKeyValue ? mkKeyValueDefault {} "=",
175     listsAsDuplicateKeys ? false,
176     indent ? ""
177   }:
178   let mkLine = k: v: indent + mkKeyValue k v + "\n";
179       mkLines = if listsAsDuplicateKeys
180         then k: v: map (mkLine k) (if isList v then v else [v])
181         else k: v: [ (mkLine k v) ];
182   in attrs: concatStrings (concatLists (mapAttrsToList mkLines attrs));
185   /**
186     Generate an INI-style config file from an
187     attrset of sections to an attrset of key-value pairs.
189     # Inputs
191     Structured function argument
193     : mkSectionName (optional, default: `(name: escape [ "[" "]" ] name)`)
194       : apply transformations (e.g. escapes) to section names
196     : mkKeyValue (optional, default: `{} "="`)
197       : format a setting line from key and value
199     : listsAsDuplicateKeys (optional, default: `false`)
200       : allow lists as values for duplicate keys
202     # Examples
203     :::{.example}
204     ## `lib.generators.toINI` usage example
206     ```nix
207     generators.toINI {} {
208       foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
209       baz = { "also, integers" = 42; };
210     }
212     > [baz]
213     > also, integers=42
214     >
215     > [foo]
216     > ciao=bar
217     > hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
218     ```
220     The mk* configuration attributes can generically change
221     the way sections and key-value strings are generated.
223     For more examples see the test cases in ./tests/misc.nix.
225     :::
226   */
227   toINI = {
228     mkSectionName ? (name: escape [ "[" "]" ] name),
229     mkKeyValue    ? mkKeyValueDefault {} "=",
230     listsAsDuplicateKeys ? false
231   }: attrsOfAttrs:
232     let
233         # map function to string for each key val
234         mapAttrsToStringsSep = sep: mapFn: attrs:
235           concatStringsSep sep
236             (mapAttrsToList mapFn attrs);
237         mkSection = sectName: sectValues: ''
238           [${mkSectionName sectName}]
239         '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
240     in
241       # map input to ini sections
242       mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
244   /**
245     Generate an INI-style config file from an attrset
246     specifying the global section (no header), and an
247     attrset of sections to an attrset of key-value pairs.
249     # Inputs
251     1\. Structured function argument
253     : mkSectionName (optional, default: `(name: escape [ "[" "]" ] name)`)
254       : apply transformations (e.g. escapes) to section names
256     : mkKeyValue (optional, default: `{} "="`)
257       : format a setting line from key and value
259     : listsAsDuplicateKeys (optional, default: `false`)
260       : allow lists as values for duplicate keys
262     2\. Structured function argument
264     : globalSection (required)
265       : global section key-value pairs
267     : sections (optional, default: `{}`)
268       : attrset of sections to key-value pairs
270     # Examples
271     :::{.example}
272     ## `lib.generators.toINIWithGlobalSection` usage example
274     ```nix
275     generators.toINIWithGlobalSection {} {
276       globalSection = {
277         someGlobalKey = "hi";
278       };
279       sections = {
280         foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
281         baz = { "also, integers" = 42; };
282     }
284     > someGlobalKey=hi
285     >
286     > [baz]
287     > also, integers=42
288     >
289     > [foo]
290     > ciao=bar
291     > hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
292     ```
294     The mk* configuration attributes can generically change
295     the way sections and key-value strings are generated.
297     For more examples see the test cases in ./tests/misc.nix.
299     :::
301     If you don’t need a global section, you can also use
302     `generators.toINI` directly, which only takes
303     the part in `sections`.
304   */
305   toINIWithGlobalSection = {
306     mkSectionName ? (name: escape [ "[" "]" ] name),
307     mkKeyValue    ? mkKeyValueDefault {} "=",
308     listsAsDuplicateKeys ? false
309   }: { globalSection, sections ? {} }:
310     ( if globalSection == {}
311       then ""
312       else (toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection)
313            + "\n")
314     + (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
316   /**
317     Generate a git-config file from an attrset.
319     It has two major differences from the regular INI format:
321     1. values are indented with tabs
322     2. sections can have sub-sections
324     Further: https://git-scm.com/docs/git-config#EXAMPLES
326     # Examples
327     :::{.example}
328     ## `lib.generators.toGitINI` usage example
330     ```nix
331     generators.toGitINI {
332       url."ssh://git@github.com/".insteadOf = "https://github.com";
333       user.name = "edolstra";
334     }
336     > [url "ssh://git@github.com/"]
337     >   insteadOf = "https://github.com"
338     >
339     > [user]
340     >   name = "edolstra"
341     ```
343     :::
345     # Inputs
347     `attrs`
349     : Key-value pairs to be converted to a git-config file.
350       See: https://git-scm.com/docs/git-config#_variables for possible values.
352   */
353   toGitINI = attrs:
354     let
355       mkSectionName = name:
356         let
357           containsQuote = hasInfix ''"'' name;
358           sections = splitString "." name;
359           section = head sections;
360           subsections = tail sections;
361           subsection = concatStringsSep "." subsections;
362         in if containsQuote || subsections == [ ] then
363           name
364         else
365           ''${section} "${subsection}"'';
367       mkValueString = v:
368         let
369           escapedV = ''
370             "${
371               replaceStrings [ "\n" "   " ''"'' "\\" ] [ "\\n" "\\t" ''\"'' "\\\\" ] v
372             }"'';
373         in mkValueStringDefault { } (if isString v then escapedV else v);
375       # generation for multiple ini values
376       mkKeyValue = k: v:
377         let mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k;
378         in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (toList v));
380       # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
381       gitFlattenAttrs = let
382         recurse = path: value:
383           if isAttrs value && !isDerivation value then
384             mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
385           else if length path > 1 then {
386             ${concatStringsSep "." (reverseList (tail path))}.${head path} = value;
387           } else {
388             ${head path} = value;
389           };
390       in attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs));
392       toINI_ = toINI { inherit mkKeyValue mkSectionName; };
393     in
394       toINI_ (gitFlattenAttrs attrs);
396   /**
397     mkKeyValueDefault wrapper that handles dconf INI quirks.
398     The main differences of the format is that it requires strings to be quoted.
399   */
400   mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (gvariant.mkValue v); } "=";
402   /**
403     Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
404     for details.
405   */
406   toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
408   /**
409     Recurses through a `Value` limited to a certain depth. (`depthLimit`)
411     If the depth is exceeded, an error is thrown, unless `throwOnDepthLimit` is set to `false`.
413     # Inputs
415     Structured function argument
417     : depthLimit (required)
418       : If this option is not null, the given value will stop evaluating at a certain depth
420     : throwOnDepthLimit (optional, default: `true`)
421       : If this option is true, an error will be thrown, if a certain given depth is exceeded
423     Value
424     : The value to be evaluated recursively
425   */
426   withRecursion =
427     {
428       depthLimit,
429       throwOnDepthLimit ? true
430     }:
431       assert isInt depthLimit;
432       let
433         specialAttrs = [
434           "__functor"
435           "__functionArgs"
436           "__toString"
437           "__pretty"
438         ];
439         stepIntoAttr = evalNext: name:
440           if elem name specialAttrs
441             then id
442             else evalNext;
443         transform = depth:
444           if depthLimit != null && depth > depthLimit then
445             if throwOnDepthLimit
446               then throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
447               else const "<unevaluated>"
448           else id;
449         mapAny = depth: v:
450           let
451             evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
452           in
453             if isAttrs v then mapAttrs (stepIntoAttr evalNext) v
454             else if isList v then map evalNext v
455             else transform (depth + 1) v;
456       in
457         mapAny 0;
459   /**
460     Pretty print a value, akin to `builtins.trace`.
462     Should probably be a builtin as well.
464     The pretty-printed string should be suitable for rendering default values
465     in the NixOS manual. In particular, it should be as close to a valid Nix expression
466     as possible.
468     # Inputs
470     Structured function argument
471     : allowPrettyValues
472       : If this option is true, attrsets like { __pretty = fn; val = …; }
473         will use fn to convert val to a pretty printed representation.
474         (This means fn is type Val -> String.)
475     : multiline
476       : If this option is true, the output is indented with newlines for attribute sets and lists
477     : indent
478       : Initial indentation level
480     Value
481     : The value to be pretty printed
482   */
483   toPretty = {
484     allowPrettyValues ? false,
485     multiline ? true,
486     indent ? ""
487   }:
488     let
489     go = indent: v:
490     let     introSpace = if multiline then "\n${indent}  " else " ";
491             outroSpace = if multiline then "\n${indent}" else " ";
492     in if   isInt      v then toString v
493     # toString loses precision on floats, so we use toJSON instead. This isn't perfect
494     # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for
495     # pretty-printing purposes this is acceptable.
496     else if isFloat    v then builtins.toJSON v
497     else if isString   v then
498       let
499         lines = filter (v: ! isList v) (split "\n" v);
500         escapeSingleline = escape [ "\\" "\"" "\${" ];
501         escapeMultiline = replaceStrings [ "\${" "''" ] [ "''\${" "'''" ];
502         singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\"";
503         multilineResult = let
504           escapedLines = map escapeMultiline lines;
505           # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
506           # indentation level. Otherwise, '' is appended to the last line.
507           lastLine = last escapedLines;
508         in "''" + introSpace + concatStringsSep introSpace (init escapedLines)
509                 + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''";
510       in
511         if multiline && length lines > 1 then multilineResult else singlelineResult
512     else if true  ==   v then "true"
513     else if false ==   v then "false"
514     else if null  ==   v then "null"
515     else if isPath     v then toString v
516     else if isList     v then
517       if v == [] then "[ ]"
518       else "[" + introSpace
519         + concatMapStringsSep introSpace (go (indent + "  ")) v
520         + outroSpace + "]"
521     else if isFunction v then
522       let fna = functionArgs v;
523           showFnas = concatStringsSep ", " (mapAttrsToList
524                        (name: hasDefVal: if hasDefVal then name + "?" else name)
525                        fna);
526       in if fna == {}    then "<function>"
527                          else "<function, args: {${showFnas}}>"
528     else if isAttrs    v then
529       # apply pretty values if allowed
530       if allowPrettyValues && v ? __pretty && v ? val
531          then v.__pretty v.val
532       else if v == {} then "{ }"
533       else if v ? type && v.type == "derivation" then
534         "<derivation ${v.name or "???"}>"
535       else "{" + introSpace
536           + concatStringsSep introSpace (mapAttrsToList
537               (name: value:
538                 "${escapeNixIdentifier name} = ${
539                   addErrorContext "while evaluating an attribute `${name}`"
540                     (go (indent + "  ") value)
541                 };") v)
542         + outroSpace + "}"
543     else abort "generators.toPretty: should never happen (v = ${v})";
544   in go indent;
546   /**
547     Translate a simple Nix expression to [Plist notation](https://en.wikipedia.org/wiki/Property_list).
549     # Inputs
551     Options
552     : Empty set, there may be configuration options in the future
554     Value
555       : The value to be converted to Plist
556   */
557   toPlist = {}: v: let
558     expr = ind: x:
559       if x == null  then "" else
560       if isBool x   then bool ind x else
561       if isInt x    then int ind x else
562       if isString x then str ind x else
563       if isList x   then list ind x else
564       if isAttrs x  then attrs ind x else
565       if isPath x   then str ind (toString x) else
566       if isFloat x  then float ind x else
567       abort "generators.toPlist: should never happen (v = ${v})";
569     literal = ind: x: ind + x;
571     bool = ind: x: literal ind  (if x then "<true/>" else "<false/>");
572     int = ind: x: literal ind "<integer>${toString x}</integer>";
573     str = ind: x: literal ind "<string>${x}</string>";
574     key = ind: x: literal ind "<key>${x}</key>";
575     float = ind: x: literal ind "<real>${toString x}</real>";
577     indent = ind: expr "\t${ind}";
579     item = ind: concatMapStringsSep "\n" (indent ind);
581     list = ind: x: concatStringsSep "\n" [
582       (literal ind "<array>")
583       (item ind x)
584       (literal ind "</array>")
585     ];
587     attrs = ind: x: concatStringsSep "\n" [
588       (literal ind "<dict>")
589       (attr ind x)
590       (literal ind "</dict>")
591     ];
593     attr = let attrFilter = name: value: name != "_module" && value != null;
594     in ind: x: concatStringsSep "\n" (flatten (mapAttrsToList
595       (name: value: optionals (attrFilter name value) [
596       (key "\t${ind}" name)
597       (expr "\t${ind}" value)
598     ]) x));
600   in ''<?xml version="1.0" encoding="UTF-8"?>
601 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
602 <plist version="1.0">
603 ${expr "" v}
604 </plist>'';
606   /**
607     Translate a simple Nix expression to Dhall notation.
609     Note that integers are translated to Integer and never
610     the Natural type.
612     # Inputs
614     Options
616     : Empty set, there may be configuration options in the future
618     Value
620     : The value to be converted to Dhall
621   */
622   toDhall = { }@args: v:
623     let concatItems = concatStringsSep ", ";
624     in if isAttrs v then
625       "{ ${
626         concatItems (mapAttrsToList
627           (key: value: "${key} = ${toDhall args value}") v)
628       } }"
629     else if isList v then
630       "[ ${concatItems (map (toDhall args) v)} ]"
631     else if isInt v then
632       "${if v < 0 then "" else "+"}${toString v}"
633     else if isBool v then
634       (if v then "True" else "False")
635     else if isFunction v then
636       abort "generators.toDhall: cannot convert a function to Dhall"
637     else if v == null then
638       abort "generators.toDhall: cannot convert a null to Dhall"
639     else
640       toJSON v;
642   /**
643     Translate a simple Nix expression to Lua representation with occasional
644     Lua-inlines that can be constructed by mkLuaInline function.
646     Configuration:
648     * multiline - by default is true which results in indented block-like view.
649     * indent - initial indent.
650     * asBindings - by default generate single value, but with this use attrset to set global vars.
652     Attention:
654     Regardless of multiline parameter there is no trailing newline.
657     # Inputs
659     Structured function argument
661     : multiline (optional, default: `true`)
662       : If this option is true, the output is indented with newlines for attribute sets and lists
663     : indent (optional, default: `""`)
664       : Initial indentation level
665     : asBindings (optional, default: `false`)
666       : Interpret as variable bindings
668     Value
670     : The value to be converted to Lua
672     # Type
674     ```
675     toLua :: AttrSet -> Any -> String
676     ```
678     # Examples
679     :::{.example}
680     ## `lib.generators.toLua` usage example
682     ```nix
683     generators.toLua {}
684       {
685         cmd = [ "typescript-language-server" "--stdio" ];
686         settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)'';
687       }
688     ->
689      {
690        ["cmd"] = {
691          "typescript-language-server",
692          "--stdio"
693        },
694        ["settings"] = {
695          ["workspace"] = {
696            ["library"] = (vim.api.nvim_get_runtime_file("", true))
697          }
698        }
699      }
700     ```
702     :::
703   */
704   toLua = {
705     multiline ? true,
706     indent ? "",
707     asBindings ? false,
708   }@args: v:
709     let
710       innerIndent = "${indent}  ";
711       introSpace = if multiline then "\n${innerIndent}" else " ";
712       outroSpace = if multiline then "\n${indent}" else " ";
713       innerArgs = args // {
714         indent = if asBindings then indent else innerIndent;
715         asBindings = false;
716       };
717       concatItems = concatStringsSep ",${introSpace}";
718       isLuaInline = { _type ? null, ... }: _type == "lua-inline";
720       generatedBindings =
721           assert assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
722           concatStrings (
723             mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
724             );
726       # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
727       matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
728       badVarNames = filter (name: matchVarName name == null) (attrNames v);
729     in
730     if asBindings then
731       generatedBindings
732     else if v == null then
733       "nil"
734     else if isInt v || isFloat v || isString v || isBool v then
735       toJSON v
736     else if isList v then
737       (if v == [ ] then "{}" else
738       "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
739     else if isAttrs v then
740       (
741         if isLuaInline v then
742           "(${v.expr})"
743         else if v == { } then
744           "{}"
745         else if isDerivation v then
746           ''"${toString v}"''
747         else
748           "{${introSpace}${concatItems (
749             mapAttrsToList (key: value: "[${toJSON key}] = ${toLua innerArgs value}") v
750             )}${outroSpace}}"
751       )
752     else
753       abort "generators.toLua: type ${typeOf v} is unsupported";
755   /**
756     Mark string as Lua expression to be inlined when processed by toLua.
759     # Inputs
761     `expr`
763     : 1\. Function argument
765     # Type
767     ```
768     mkLuaInline :: String -> AttrSet
769     ```
770   */
771   mkLuaInline = expr: { _type = "lua-inline"; inherit expr; };
772 } // {
773   /**
774     Generates JSON from an arbitrary (non-function) value.
775     For more information see the documentation of the builtin.
777     # Inputs
779     Options
781     : Empty set, there may be configuration options in the future
783     Value
785     : The value to be converted to JSON
786   */
787   toJSON = {}: lib.strings.toJSON;
789   /**
790     YAML has been a strict superset of JSON since 1.2, so we
791     use toJSON. Before it only had a few differences referring
792     to implicit typing rules, so it should work with older
793     parsers as well.
795     # Inputs
797     Options
799     : Empty set, there may be configuration options in the future
801     Value
803     : The value to be converted to YAML
804   */
805   toYAML = {}: lib.strings.toJSON;