Merge pull request #136474 from waldheinz/etc-file-source-to-store
[NixPkgs.git] / lib / generators.nix
blobbcb0f371a9b51c05c050455a0faa76bb0b5ecb39
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.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     # we default to not quoting strings
39     else if isString   v then v
40     # isString returns "1", which is not a good default
41     else if true  ==   v then "true"
42     # here it returns to "", which is even less of a good default
43     else if false ==   v then "false"
44     else if null  ==   v then "null"
45     # if you have lists you probably want to replace this
46     else if isList     v then err "lists" v
47     # same as for lists, might want to replace
48     else if isAttrs    v then err "attrsets" v
49     # functions can’t be printed of course
50     else if isFunction v then err "functions" v
51     # Floats currently can't be converted to precise strings,
52     # condition warning on nix version once this isn't a problem anymore
53     # See https://github.com/NixOS/nix/pull/3480
54     else if isFloat    v then libStr.floatToString v
55     else err "this value is" (toString v);
58   /* Generate a line of key k and value v, separated by
59    * character sep. If sep appears in k, it is escaped.
60    * Helper for synaxes with different separators.
61    *
62    * mkValueString specifies how values should be formatted.
63    *
64    * mkKeyValueDefault {} ":" "f:oo" "bar"
65    * > "f\:oo:bar"
66    */
67   mkKeyValueDefault = {
68     mkValueString ? mkValueStringDefault {}
69   }: sep: k: v:
70     "${libStr.escape [sep] k}${sep}${mkValueString v}";
73   ## -- FILE FORMAT GENERATORS --
76   /* Generate a key-value-style config file from an attrset.
77    *
78    * mkKeyValue is the same as in toINI.
79    */
80   toKeyValue = {
81     mkKeyValue ? mkKeyValueDefault {} "=",
82     listsAsDuplicateKeys ? false
83   }:
84   let mkLine = k: v: mkKeyValue k v + "\n";
85       mkLines = if listsAsDuplicateKeys
86         then k: v: map (mkLine k) (if lib.isList v then v else [v])
87         else k: v: [ (mkLine k v) ];
88   in attrs: libStr.concatStrings (lib.concatLists (libAttr.mapAttrsToList mkLines attrs));
91   /* Generate an INI-style config file from an
92    * attrset of sections to an attrset of key-value pairs.
93    *
94    * generators.toINI {} {
95    *   foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
96    *   baz = { "also, integers" = 42; };
97    * }
98    *
99    *> [baz]
100    *> also, integers=42
101    *>
102    *> [foo]
103    *> ciao=bar
104    *> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
105    *
106    * The mk* configuration attributes can generically change
107    * the way sections and key-value strings are generated.
108    *
109    * For more examples see the test cases in ./tests.nix.
110    */
111   toINI = {
112     # apply transformations (e.g. escapes) to section names
113     mkSectionName ? (name: libStr.escape [ "[" "]" ] name),
114     # format a setting line from key and value
115     mkKeyValue    ? mkKeyValueDefault {} "=",
116     # allow lists as values for duplicate keys
117     listsAsDuplicateKeys ? false
118   }: attrsOfAttrs:
119     let
120         # map function to string for each key val
121         mapAttrsToStringsSep = sep: mapFn: attrs:
122           libStr.concatStringsSep sep
123             (libAttr.mapAttrsToList mapFn attrs);
124         mkSection = sectName: sectValues: ''
125           [${mkSectionName sectName}]
126         '' + toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
127     in
128       # map input to ini sections
129       mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
131   /* Generate a git-config file from an attrset.
132    *
133    * It has two major differences from the regular INI format:
134    *
135    * 1. values are indented with tabs
136    * 2. sections can have sub-sections
137    *
138    * generators.toGitINI {
139    *   url."ssh://git@github.com/".insteadOf = "https://github.com";
140    *   user.name = "edolstra";
141    * }
142    *
143    *> [url "ssh://git@github.com/"]
144    *>   insteadOf = https://github.com/
145    *>
146    *> [user]
147    *>   name = edolstra
148    */
149   toGitINI = attrs:
150     with builtins;
151     let
152       mkSectionName = name:
153         let
154           containsQuote = libStr.hasInfix ''"'' name;
155           sections = libStr.splitString "." name;
156           section = head sections;
157           subsections = tail sections;
158           subsection = concatStringsSep "." subsections;
159         in if containsQuote || subsections == [ ] then
160           name
161         else
162           ''${section} "${subsection}"'';
164       # generation for multiple ini values
165       mkKeyValue = k: v:
166         let mkKeyValue = mkKeyValueDefault { } " = " k;
167         in concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (lib.toList v));
169       # converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
170       gitFlattenAttrs = let
171         recurse = path: value:
172           if isAttrs value then
173             lib.mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
174           else if length path > 1 then {
175             ${concatStringsSep "." (lib.reverseList (tail path))}.${head path} = value;
176           } else {
177             ${head path} = value;
178           };
179       in attrs: lib.foldl lib.recursiveUpdate { } (lib.flatten (recurse [ ] attrs));
181       toINI_ = toINI { inherit mkKeyValue mkSectionName; };
182     in
183       toINI_ (gitFlattenAttrs attrs);
185   /* Generates JSON from an arbitrary (non-function) value.
186     * For more information see the documentation of the builtin.
187     */
188   toJSON = {}: builtins.toJSON;
191   /* YAML has been a strict superset of JSON since 1.2, so we
192     * use toJSON. Before it only had a few differences referring
193     * to implicit typing rules, so it should work with older
194     * parsers as well.
195     */
196   toYAML = {}@args: toJSON args;
199   /* Pretty print a value, akin to `builtins.trace`.
200     * Should probably be a builtin as well.
201     */
202   toPretty = {
203     /* If this option is true, attrsets like { __pretty = fn; val = …; }
204        will use fn to convert val to a pretty printed representation.
205        (This means fn is type Val -> String.) */
206     allowPrettyValues ? false,
207     /* If this option is true, the output is indented with newlines for attribute sets and lists */
208     multiline ? true
209   }@args: let
210     go = indent: v: with builtins;
211     let     isPath   = v: typeOf v == "path";
212             introSpace = if multiline then "\n${indent}  " else " ";
213             outroSpace = if multiline then "\n${indent}" else " ";
214     in if   isInt      v then toString v
215     else if isFloat    v then "~${toString v}"
216     else if isString   v then
217       let
218         # Separate a string into its lines
219         newlineSplits = filter (v: ! isList v) (builtins.split "\n" v);
220         # For a '' string terminated by a \n, which happens when the closing '' is on a new line
221         multilineResult = "''" + introSpace + concatStringsSep introSpace (lib.init newlineSplits) + outroSpace + "''";
222         # For a '' string not terminated by a \n, which happens when the closing '' is not on a new line
223         multilineResult' = "''" + introSpace + concatStringsSep introSpace newlineSplits + "''";
224         # For single lines, replace all newlines with their escaped representation
225         singlelineResult = "\"" + libStr.escape [ "\"" ] (concatStringsSep "\\n" newlineSplits) + "\"";
226       in if multiline && length newlineSplits > 1 then
227         if lib.last newlineSplits == "" then multilineResult else multilineResult'
228       else singlelineResult
229     else if true  ==   v then "true"
230     else if false ==   v then "false"
231     else if null  ==   v then "null"
232     else if isPath     v then toString v
233     else if isList     v then
234       if v == [] then "[ ]"
235       else "[" + introSpace
236         + libStr.concatMapStringsSep introSpace (go (indent + "  ")) v
237         + outroSpace + "]"
238     else if isFunction v then
239       let fna = lib.functionArgs v;
240           showFnas = concatStringsSep ", " (libAttr.mapAttrsToList
241                        (name: hasDefVal: if hasDefVal then name + "?" else name)
242                        fna);
243       in if fna == {}    then "<function>"
244                          else "<function, args: {${showFnas}}>"
245     else if isAttrs    v then
246       # apply pretty values if allowed
247       if attrNames v == [ "__pretty" "val" ] && allowPrettyValues
248          then v.__pretty v.val
249       else if v == {} then "{ }"
250       else if v ? type && v.type == "derivation" then
251         "<derivation ${v.drvPath or "???"}>"
252       else "{" + introSpace
253           + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
254               (name: value:
255                 "${libStr.escapeNixIdentifier name} = ${go (indent + "  ") value};") v)
256         + outroSpace + "}"
257     else abort "generators.toPretty: should never happen (v = ${v})";
258   in go "";
260   # PLIST handling
261   toPlist = {}: v: let
262     isFloat = builtins.isFloat or (x: false);
263     expr = ind: x:  with builtins;
264       if x == null  then "" else
265       if isBool x   then bool ind x else
266       if isInt x    then int ind x else
267       if isString x then str ind x else
268       if isList x   then list ind x else
269       if isAttrs x  then attrs ind x else
270       if isFloat x  then float ind x else
271       abort "generators.toPlist: should never happen (v = ${v})";
273     literal = ind: x: ind + x;
275     bool = ind: x: literal ind  (if x then "<true/>" else "<false/>");
276     int = ind: x: literal ind "<integer>${toString x}</integer>";
277     str = ind: x: literal ind "<string>${x}</string>";
278     key = ind: x: literal ind "<key>${x}</key>";
279     float = ind: x: literal ind "<real>${toString x}</real>";
281     indent = ind: expr "\t${ind}";
283     item = ind: libStr.concatMapStringsSep "\n" (indent ind);
285     list = ind: x: libStr.concatStringsSep "\n" [
286       (literal ind "<array>")
287       (item ind x)
288       (literal ind "</array>")
289     ];
291     attrs = ind: x: libStr.concatStringsSep "\n" [
292       (literal ind "<dict>")
293       (attr ind x)
294       (literal ind "</dict>")
295     ];
297     attr = let attrFilter = name: value: name != "_module" && value != null;
298     in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
299       (name: value: lib.optional (attrFilter name value) [
300       (key "\t${ind}" name)
301       (expr "\t${ind}" value)
302     ]) x));
304   in ''<?xml version="1.0" encoding="UTF-8"?>
305 <!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
306 <plist version="1.0">
307 ${expr "" v}
308 </plist>'';
310   /* Translate a simple Nix expression to Dhall notation.
311    * Note that integers are translated to Integer and never
312    * the Natural type.
313   */
314   toDhall = { }@args: v:
315     with builtins;
316     let concatItems = lib.strings.concatStringsSep ", ";
317     in if isAttrs v then
318       "{ ${
319         concatItems (lib.attrsets.mapAttrsToList
320           (key: value: "${key} = ${toDhall args value}") v)
321       } }"
322     else if isList v then
323       "[ ${concatItems (map (toDhall args) v)} ]"
324     else if isInt v then
325       "${if v < 0 then "" else "+"}${toString v}"
326     else if isBool v then
327       (if v then "True" else "False")
328     else if isFunction v then
329       abort "generators.toDhall: cannot convert a function to Dhall"
330     else if isNull v then
331       abort "generators.toDhall: cannot convert a null to Dhall"
332     else
333       builtins.toJSON v;