about summary refs log tree commit diff
path: root/nixpkgs/lib/generators.nix
diff options
context:
space:
mode:
Diffstat (limited to 'nixpkgs/lib/generators.nix')
-rw-r--r--nixpkgs/lib/generators.nix158
1 files changed, 134 insertions, 24 deletions
diff --git a/nixpkgs/lib/generators.nix b/nixpkgs/lib/generators.nix
index 431b93c4ebbc..496845fc9ae4 100644
--- a/nixpkgs/lib/generators.nix
+++ b/nixpkgs/lib/generators.nix
@@ -240,10 +240,10 @@ rec {
     * to implicit typing rules, so it should work with older
     * parsers as well.
     */
-  toYAML = {}@args: toJSON args;
+  toYAML = toJSON;
 
   withRecursion =
-    args@{
+    {
       /* If this option is not null, the given value will stop evaluating at a certain depth */
       depthLimit
       /* If this option is true, an error will be thrown, if a certain given depth is exceeded */
@@ -278,36 +278,46 @@ rec {
         mapAny 0;
 
   /* Pretty print a value, akin to `builtins.trace`.
-    * Should probably be a builtin as well.
-    */
+   * Should probably be a builtin as well.
+   * The pretty-printed string should be suitable for rendering default values
+   * in the NixOS manual. In particular, it should be as close to a valid Nix expression
+   * as possible.
+   */
   toPretty = {
     /* If this option is true, attrsets like { __pretty = fn; val = …; }
        will use fn to convert val to a pretty printed representation.
        (This means fn is type Val -> String.) */
     allowPrettyValues ? false,
     /* If this option is true, the output is indented with newlines for attribute sets and lists */
-    multiline ? true
-  }@args:
+    multiline ? true,
+    /* Initial indentation level */
+    indent ? ""
+  }:
     let
     go = indent: v: with builtins;
     let     isPath   = v: typeOf v == "path";
             introSpace = if multiline then "\n${indent}  " else " ";
             outroSpace = if multiline then "\n${indent}" else " ";
     in if   isInt      v then toString v
-    else if isFloat    v then "~${toString v}"
+    # toString loses precision on floats, so we use toJSON instead. This isn't perfect
+    # as the resulting string may not parse back as a float (e.g. 42, 1e-06), but for
+    # pretty-printing purposes this is acceptable.
+    else if isFloat    v then builtins.toJSON v
     else if isString   v then
       let
-        # Separate a string into its lines
-        newlineSplits = filter (v: ! isList v) (builtins.split "\n" v);
-        # For a '' string terminated by a \n, which happens when the closing '' is on a new line
-        multilineResult = "''" + introSpace + concatStringsSep introSpace (lib.init newlineSplits) + outroSpace + "''";
-        # For a '' string not terminated by a \n, which happens when the closing '' is not on a new line
-        multilineResult' = "''" + introSpace + concatStringsSep introSpace newlineSplits + "''";
-        # For single lines, replace all newlines with their escaped representation
-        singlelineResult = "\"" + libStr.escape [ "\"" ] (concatStringsSep "\\n" newlineSplits) + "\"";
-      in if multiline && length newlineSplits > 1 then
-        if lib.last newlineSplits == "" then multilineResult else multilineResult'
-      else singlelineResult
+        lines = filter (v: ! isList v) (builtins.split "\n" v);
+        escapeSingleline = libStr.escape [ "\\" "\"" "\${" ];
+        escapeMultiline = libStr.replaceStrings [ "\${" "''" ] [ "''\${" "'''" ];
+        singlelineResult = "\"" + concatStringsSep "\\n" (map escapeSingleline lines) + "\"";
+        multilineResult = let
+          escapedLines = map escapeMultiline lines;
+          # The last line gets a special treatment: if it's empty, '' is on its own line at the "outer"
+          # indentation level. Otherwise, '' is appended to the last line.
+          lastLine = lib.last escapedLines;
+        in "''" + introSpace + concatStringsSep introSpace (lib.init escapedLines)
+                + (if lastLine == "" then outroSpace else introSpace + lastLine) + "''";
+      in
+        if multiline && length lines > 1 then multilineResult else singlelineResult
     else if true  ==   v then "true"
     else if false ==   v then "false"
     else if null  ==   v then "null"
@@ -326,22 +336,26 @@ rec {
                          else "<function, args: {${showFnas}}>"
     else if isAttrs    v then
       # apply pretty values if allowed
-      if attrNames v == [ "__pretty" "val" ] && allowPrettyValues
+      if allowPrettyValues && v ? __pretty && v ? val
          then v.__pretty v.val
       else if v == {} then "{ }"
       else if v ? type && v.type == "derivation" then
-        "<derivation ${v.drvPath or "???"}>"
+        "<derivation ${v.name or "???"}>"
       else "{" + introSpace
           + libStr.concatStringsSep introSpace (libAttr.mapAttrsToList
               (name: value:
-                "${libStr.escapeNixIdentifier name} = ${go (indent + "  ") value};") v)
+                "${libStr.escapeNixIdentifier name} = ${
+                  builtins.addErrorContext "while evaluating an attribute `${name}`"
+                    (go (indent + "  ") value)
+                };") v)
         + outroSpace + "}"
     else abort "generators.toPretty: should never happen (v = ${v})";
-  in go "";
+  in go indent;
 
   # PLIST handling
   toPlist = {}: v: let
     isFloat = builtins.isFloat or (x: false);
+    isPath = x: builtins.typeOf x == "path";
     expr = ind: x:  with builtins;
       if x == null  then "" else
       if isBool x   then bool ind x else
@@ -349,6 +363,7 @@ rec {
       if isString x then str ind x else
       if isList x   then list ind x else
       if isAttrs x  then attrs ind x else
+      if isPath x   then str ind (toString x) else
       if isFloat x  then float ind x else
       abort "generators.toPlist: should never happen (v = ${v})";
 
@@ -378,7 +393,7 @@ rec {
 
     attr = let attrFilter = name: value: name != "_module" && value != null;
     in ind: x: libStr.concatStringsSep "\n" (lib.flatten (lib.mapAttrsToList
-      (name: value: lib.optional (attrFilter name value) [
+      (name: value: lib.optionals (attrFilter name value) [
       (key "\t${ind}" name)
       (expr "\t${ind}" value)
     ]) x));
@@ -409,8 +424,103 @@ ${expr "" v}
       (if v then "True" else "False")
     else if isFunction v then
       abort "generators.toDhall: cannot convert a function to Dhall"
-    else if isNull v then
+    else if v == null then
       abort "generators.toDhall: cannot convert a null to Dhall"
     else
       builtins.toJSON v;
+
+  /*
+   Translate a simple Nix expression to Lua representation with occasional
+   Lua-inlines that can be constructed by mkLuaInline function.
+
+   Configuration:
+     * multiline - by default is true which results in indented block-like view.
+     * indent - initial indent.
+     * asBindings - by default generate single value, but with this use attrset to set global vars.
+
+   Attention:
+     Regardless of multiline parameter there is no trailing newline.
+
+   Example:
+     generators.toLua {}
+       {
+         cmd = [ "typescript-language-server" "--stdio" ];
+         settings.workspace.library = mkLuaInline ''vim.api.nvim_get_runtime_file("", true)'';
+       }
+     ->
+      {
+        ["cmd"] = {
+          "typescript-language-server",
+          "--stdio"
+        },
+        ["settings"] = {
+          ["workspace"] = {
+            ["library"] = (vim.api.nvim_get_runtime_file("", true))
+          }
+        }
+      }
+
+   Type:
+     toLua :: AttrSet -> Any -> String
+  */
+  toLua = {
+    /* If this option is true, the output is indented with newlines for attribute sets and lists */
+    multiline ? true,
+    /* Initial indentation level */
+    indent ? "",
+    /* Interpret as variable bindings */
+    asBindings ? false,
+  }@args: v:
+    with builtins;
+    let
+      innerIndent = "${indent}  ";
+      introSpace = if multiline then "\n${innerIndent}" else " ";
+      outroSpace = if multiline then "\n${indent}" else " ";
+      innerArgs = args // {
+        indent = if asBindings then indent else innerIndent;
+        asBindings = false;
+      };
+      concatItems = concatStringsSep ",${introSpace}";
+      isLuaInline = { _type ? null, ... }: _type == "lua-inline";
+
+      generatedBindings =
+          assert lib.assertMsg (badVarNames == []) "Bad Lua var names: ${toPretty {} badVarNames}";
+          libStr.concatStrings (
+            lib.attrsets.mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v
+            );
+
+      # https://en.wikibooks.org/wiki/Lua_Programming/variable#Variable_names
+      matchVarName = match "[[:alpha:]_][[:alnum:]_]*(\\.[[:alpha:]_][[:alnum:]_]*)*";
+      badVarNames = filter (name: matchVarName name == null) (attrNames v);
+    in
+    if asBindings then
+      generatedBindings
+    else if v == null then
+      "nil"
+    else if isInt v || isFloat v || isString v || isBool v then
+      builtins.toJSON v
+    else if isList v then
+      (if v == [ ] then "{}" else
+      "{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}")
+    else if isAttrs v then
+      (
+        if isLuaInline v then
+          "(${v.expr})"
+        else if v == { } then
+          "{}"
+        else
+          "{${introSpace}${concatItems (
+            lib.attrsets.mapAttrsToList (key: value: "[${builtins.toJSON key}] = ${toLua innerArgs value}") v
+            )}${outroSpace}}"
+      )
+    else
+      abort "generators.toLua: type ${typeOf v} is unsupported";
+
+  /*
+   Mark string as Lua expression to be inlined when processed by toLua.
+
+   Type:
+     mkLuaInline :: String -> AttrSet
+  */
+  mkLuaInline = expr: { _type = "lua-inline"; inherit expr; };
 }