Merge pull request #268619 from tweag/lib-descriptions
[NixPkgs.git] / pkgs / pkgs-lib / formats.nix
blob3cbda3a7ebdd96f2f9ba9a56aae9fa7a9d3f9369
1 { lib, pkgs }:
2 rec {
4   /*
6   Every following entry represents a format for program configuration files
7   used for `settings`-style options (see https://github.com/NixOS/rfcs/pull/42).
8   Each entry should look as follows:
10     <format> = <parameters>: {
11       #        ^^ Parameters for controlling the format
13       # The module system type most suitable for representing such a format
14       # The description needs to be overwritten for recursive types
15       type = ...;
17       # Utility functions for convenience, or special interactions with the
18       # format (optional)
19       lib = {
20         exampleFunction = ...
21         # Types specific to the format (optional)
22         types = { ... };
23         ...
24       };
26       # generate :: Name -> Value -> Path
27       # A function for generating a file with a value of such a type
28       generate = ...;
30     });
32   Please note that `pkgs` may not always be available for use due to the split
33   options doc build introduced in fc614c37c653, so lazy evaluation of only the
34   'type' field is required.
36   */
39   inherit (import ./formats/java-properties/default.nix { inherit lib pkgs; })
40     javaProperties;
42   libconfig = (import ./formats/libconfig/default.nix { inherit lib pkgs; }).format;
44   json = {}: {
46     type = with lib.types; let
47       valueType = nullOr (oneOf [
48         bool
49         int
50         float
51         str
52         path
53         (attrsOf valueType)
54         (listOf valueType)
55       ]) // {
56         description = "JSON value";
57       };
58     in valueType;
60     generate = name: value: pkgs.callPackage ({ runCommand, jq }: runCommand name {
61       nativeBuildInputs = [ jq ];
62       value = builtins.toJSON value;
63       passAsFile = [ "value" ];
64     } ''
65       jq . "$valuePath"> $out
66     '') {};
68   };
70   yaml = {}: {
72     generate = name: value: pkgs.callPackage ({ runCommand, remarshal }: runCommand name {
73       nativeBuildInputs = [ remarshal ];
74       value = builtins.toJSON value;
75       passAsFile = [ "value" ];
76     } ''
77       json2yaml "$valuePath" "$out"
78     '') {};
80     type = with lib.types; let
81       valueType = nullOr (oneOf [
82         bool
83         int
84         float
85         str
86         path
87         (attrsOf valueType)
88         (listOf valueType)
89       ]) // {
90         description = "YAML value";
91       };
92     in valueType;
94   };
96   ini = {
97     # Represents lists as duplicate keys
98     listsAsDuplicateKeys ? false,
99     # Alternative to listsAsDuplicateKeys, converts list to non-list
100     # listToValue :: [IniAtom] -> IniAtom
101     listToValue ? null,
102     ...
103     }@args:
104     assert !listsAsDuplicateKeys || listToValue == null;
105     {
107     type = with lib.types; let
109       singleIniAtom = nullOr (oneOf [
110         bool
111         int
112         float
113         str
114       ]) // {
115         description = "INI atom (null, bool, int, float or string)";
116       };
118       iniAtom =
119         if listsAsDuplicateKeys then
120           coercedTo singleIniAtom lib.singleton (listOf singleIniAtom) // {
121             description = singleIniAtom.description + " or a list of them for duplicate keys";
122           }
123         else if listToValue != null then
124           coercedTo singleIniAtom lib.singleton (nonEmptyListOf singleIniAtom) // {
125             description = singleIniAtom.description + " or a non-empty list of them";
126           }
127         else
128           singleIniAtom;
130     in attrsOf (attrsOf iniAtom);
132     generate = name: value:
133       let
134         transformedValue =
135           if listToValue != null
136           then
137             lib.mapAttrs (section: lib.mapAttrs (key: val:
138               if lib.isList val then listToValue val else val
139             )) value
140           else value;
141       in pkgs.writeText name (lib.generators.toINI (removeAttrs args ["listToValue"]) transformedValue);
143   };
145   keyValue = {
146     # Represents lists as duplicate keys
147     listsAsDuplicateKeys ? false,
148     # Alternative to listsAsDuplicateKeys, converts list to non-list
149     # listToValue :: [Atom] -> Atom
150     listToValue ? null,
151     ...
152     }@args:
153     assert !listsAsDuplicateKeys || listToValue == null;
154     {
156     type = with lib.types; let
158       singleAtom = nullOr (oneOf [
159         bool
160         int
161         float
162         str
163       ]) // {
164         description = "atom (null, bool, int, float or string)";
165       };
167       atom =
168         if listsAsDuplicateKeys then
169           coercedTo singleAtom lib.singleton (listOf singleAtom) // {
170             description = singleAtom.description + " or a list of them for duplicate keys";
171           }
172         else if listToValue != null then
173           coercedTo singleAtom lib.singleton (nonEmptyListOf singleAtom) // {
174             description = singleAtom.description + " or a non-empty list of them";
175           }
176         else
177           singleAtom;
179     in attrsOf atom;
181     generate = name: value:
182       let
183         transformedValue =
184           if listToValue != null
185           then
186             lib.mapAttrs (key: val:
187               if lib.isList val then listToValue val else val
188             ) value
189           else value;
190       in pkgs.writeText name (lib.generators.toKeyValue (removeAttrs args ["listToValue"]) transformedValue);
192   };
194   gitIni = { listsAsDuplicateKeys ? false, ... }@args: {
196     type = with lib.types; let
198       iniAtom = (ini args).type/*attrsOf*/.functor.wrapped/*attrsOf*/.functor.wrapped;
200     in attrsOf (attrsOf (either iniAtom (attrsOf iniAtom)));
202     generate = name: value: pkgs.writeText name (lib.generators.toGitINI value);
203   };
205   toml = {}: json {} // {
206     type = with lib.types; let
207       valueType = oneOf [
208         bool
209         int
210         float
211         str
212         path
213         (attrsOf valueType)
214         (listOf valueType)
215       ] // {
216         description = "TOML value";
217       };
218     in valueType;
220     generate = name: value: pkgs.callPackage ({ runCommand, remarshal }: runCommand name {
221       nativeBuildInputs = [ remarshal ];
222       value = builtins.toJSON value;
223       passAsFile = [ "value" ];
224     } ''
225       json2toml "$valuePath" "$out"
226     '') {};
228   };
230   /* For configurations of Elixir project, like config.exs or runtime.exs
232     Most Elixir project are configured using the [Config] Elixir DSL
234     Since Elixir has more types than Nix, we need a way to map Nix types to
235     more than 1 Elixir type. To that end, this format provides its own library,
236     and its own set of types.
238     To be more detailed, a Nix attribute set could correspond in Elixir to a
239     [Keyword list] (the more common type), or it could correspond to a [Map].
241     A Nix string could correspond in Elixir to a [String] (also called
242     "binary"), an [Atom], or a list of chars (usually discouraged).
244     A Nix array could correspond in Elixir to a [List] or a [Tuple].
246     Some more types exists, like records, regexes, but since they are less used,
247     we can leave the `mkRaw` function as an escape hatch.
249     For more information on how to use this format in modules, please refer to
250     the Elixir section of the Nixos documentation.
252     TODO: special Elixir values doesn't show up nicely in the documentation
254     [Config]: <https://hexdocs.pm/elixir/Config.html>
255     [Keyword list]: <https://hexdocs.pm/elixir/Keyword.html>
256     [Map]: <https://hexdocs.pm/elixir/Map.html>
257     [String]: <https://hexdocs.pm/elixir/String.html>
258     [Atom]: <https://hexdocs.pm/elixir/Atom.html>
259     [List]: <https://hexdocs.pm/elixir/List.html>
260     [Tuple]: <https://hexdocs.pm/elixir/Tuple.html>
261   */
262   elixirConf = { elixir ? pkgs.elixir }:
263     with lib; let
264       toElixir = value: with builtins;
265         if value == null then "nil" else
266         if value == true then "true" else
267         if value == false then "false" else
268         if isInt value || isFloat value then toString value else
269         if isString value then string value else
270         if isAttrs value then attrs value else
271         if isList value then list value else
272         abort "formats.elixirConf: should never happen (value = ${value})";
274       escapeElixir = escape [ "\\" "#" "\"" ];
275       string = value: "\"${escapeElixir value}\"";
277       attrs = set:
278         if set ? _elixirType then specialType set
279         else
280           let
281             toKeyword = name: value: "${name}: ${toElixir value}";
282             keywordList = concatStringsSep ", " (mapAttrsToList toKeyword set);
283           in
284           "[" + keywordList + "]";
286       listContent = values: concatStringsSep ", " (map toElixir values);
288       list = values: "[" + (listContent values) + "]";
290       specialType = { value, _elixirType }:
291         if _elixirType == "raw" then value else
292         if _elixirType == "atom" then value else
293         if _elixirType == "map" then elixirMap value else
294         if _elixirType == "tuple" then tuple value else
295         abort "formats.elixirConf: should never happen (_elixirType = ${_elixirType})";
297       elixirMap = set:
298         let
299           toEntry = name: value: "${toElixir name} => ${toElixir value}";
300           entries = concatStringsSep ", " (mapAttrsToList toEntry set);
301         in
302         "%{${entries}}";
304       tuple = values: "{${listContent values}}";
306       toConf = values:
307         let
308           keyConfig = rootKey: key: value:
309             "config ${rootKey}, ${key}, ${toElixir value}";
310           keyConfigs = rootKey: values: mapAttrsToList (keyConfig rootKey) values;
311           rootConfigs = flatten (mapAttrsToList keyConfigs values);
312         in
313         ''
314           import Config
316           ${concatStringsSep "\n" rootConfigs}
317         '';
318     in
319     {
320       type = with lib.types; let
321         valueType = nullOr
322           (oneOf [
323             bool
324             int
325             float
326             str
327             (attrsOf valueType)
328             (listOf valueType)
329           ]) // {
330           description = "Elixir value";
331         };
332       in
333       attrsOf (attrsOf (valueType));
335       lib =
336         let
337           mkRaw = value: {
338             inherit value;
339             _elixirType = "raw";
340           };
342         in
343         {
344           inherit mkRaw;
346           /* Fetch an environment variable at runtime, with optional fallback
347           */
348           mkGetEnv = { envVariable, fallback ? null }:
349             mkRaw "System.get_env(${toElixir envVariable}, ${toElixir fallback})";
351           /* Make an Elixir atom.
353             Note: lowercase atoms still need to be prefixed by ':'
354           */
355           mkAtom = value: {
356             inherit value;
357             _elixirType = "atom";
358           };
360           /* Make an Elixir tuple out of a list.
361           */
362           mkTuple = value: {
363             inherit value;
364             _elixirType = "tuple";
365           };
367           /* Make an Elixir map out of an attribute set.
368           */
369           mkMap = value: {
370             inherit value;
371             _elixirType = "map";
372           };
374           /* Contains Elixir types. Every type it exports can also be replaced
375              by raw Elixir code (i.e. every type is `either type rawElixir`).
377              It also reexports standard types, wrapping them so that they can
378              also be raw Elixir.
379           */
380           types = with lib.types; let
381             isElixirType = type: x: (x._elixirType or "") == type;
383             rawElixir = mkOptionType {
384               name = "rawElixir";
385               description = "raw elixir";
386               check = isElixirType "raw";
387             };
389             elixirOr = other: either other rawElixir;
390           in
391           {
392             inherit rawElixir elixirOr;
394             atom = elixirOr (mkOptionType {
395               name = "elixirAtom";
396               description = "elixir atom";
397               check = isElixirType "atom";
398             });
400             tuple = elixirOr (mkOptionType {
401               name = "elixirTuple";
402               description = "elixir tuple";
403               check = isElixirType "tuple";
404             });
406             map = elixirOr (mkOptionType {
407               name = "elixirMap";
408               description = "elixir map";
409               check = isElixirType "map";
410             });
411             # Wrap standard types, since anything in the Elixir configuration
412             # can be raw Elixir
413           } // lib.mapAttrs (_name: type: elixirOr type) lib.types;
414         };
416       generate = name: value: pkgs.runCommand name
417         {
418           value = toConf value;
419           passAsFile = [ "value" ];
420           nativeBuildInputs = [ elixir ];
421         } ''
422         cp "$valuePath" "$out"
423         mix format "$out"
424       '';
425     };
427   # Outputs a succession of Python variable assignments
428   # Useful for many Django-based services
429   pythonVars = {}: {
430     type = with lib.types; let
431       valueType = nullOr(oneOf [
432         bool
433         float
434         int
435         path
436         str
437         (attrsOf valueType)
438         (listOf valueType)
439       ]) // {
440         description = "Python value";
441       };
442     in attrsOf valueType;
443     generate = name: value: pkgs.callPackage ({ runCommand, python3, black }: runCommand name {
444       nativeBuildInputs = [ python3 black ];
445       value = builtins.toJSON value;
446       pythonGen = ''
447         import json
448         import os
450         with open(os.environ["valuePath"], "r") as f:
451             for key, value in json.load(f).items():
452                 print(f"{key} = {repr(value)}")
453       '';
454       passAsFile = [ "value" "pythonGen" ];
455     } ''
456       cat "$valuePath"
457       python3 "$pythonGenPath" > $out
458       black $out
459     '') {};
460   };