core/lib/generators.nix

758 lines
21 KiB
Nix
Raw Normal View History

2024-06-30 08:12:46 +00:00
/*
Functions that generate widespread file
formats from nix data structures.
They all follow a similar interface:
generator { config-attrs } data
`config-attrs` are holes in the generators
with sensible default implementations that
can be overwritten. The default implementations
are mostly generators themselves, called with
their respective default values; they can be reused.
Tests can be found in ./tests/misc.nix
Documentation in the manual, #sec-generators
*/
2024-05-01 22:14:04 +00:00
{ lib }:
let
inherit (lib)
addErrorContext
assertMsg
attrNames
concatLists
concatMapStringsSep
concatStrings
concatStringsSep
const
elem
escape
filter
flatten
foldl
2024-06-30 08:12:46 +00:00
functionArgs # Note: not the builtin; considers `__functor` in attrsets.
2024-05-01 22:14:04 +00:00
gvariant
hasInfix
head
id
init
isAttrs
isBool
isDerivation
isFloat
2024-06-30 08:12:46 +00:00
isFunction # Note: not the builtin; considers `__functor` in attrsets.
2024-05-01 22:14:04 +00:00
isInt
isList
isPath
isString
last
length
mapAttrs
mapAttrsToList
optionals
recursiveUpdate
replaceStrings
reverseList
splitString
tail
toList
;
inherit (lib.strings)
escapeNixIdentifier
floatToString
match
split
toJSON
typeOf
;
## -- HELPER FUNCTIONS & DEFAULTS --
2024-06-30 08:12:46 +00:00
/*
Convert a value to a sensible default string representation.
The builtin `toString` function has some strange defaults,
suitable for bash scripts but not much else.
*/
mkValueStringDefault =
{ }:
v:
let
err = t: v: abort ("generators.mkValueStringDefault: " + "${t} not supported: ${toPretty { } v}");
in
if isInt v then
toString v
2024-05-01 22:14:04 +00:00
# convert derivations to store paths
2024-06-30 08:12:46 +00:00
else if isDerivation v then
toString v
2024-05-01 22:14:04 +00:00
# we default to not quoting strings
2024-06-30 08:12:46 +00:00
else if isString v then
v
2024-05-01 22:14:04 +00:00
# isString returns "1", which is not a good default
2024-06-30 08:12:46 +00:00
else if true == v then
"true"
2024-05-01 22:14:04 +00:00
# here it returns to "", which is even less of a good default
2024-06-30 08:12:46 +00:00
else if false == v then
"false"
else if null == v then
"null"
2024-05-01 22:14:04 +00:00
# if you have lists you probably want to replace this
2024-06-30 08:12:46 +00:00
else if isList v then
err "lists" v
2024-05-01 22:14:04 +00:00
# same as for lists, might want to replace
2024-06-30 08:12:46 +00:00
else if isAttrs v then
err "attrsets" v
2024-05-01 22:14:04 +00:00
# functions cant be printed of course
2024-06-30 08:12:46 +00:00
else if isFunction v then
err "functions" v
2024-05-01 22:14:04 +00:00
# Floats currently can't be converted to precise strings,
# condition warning on nix version once this isn't a problem anymore
# See https://github.com/NixOS/nix/pull/3480
2024-06-30 08:12:46 +00:00
else if isFloat v then
floatToString v
else
err "this value is" (toString v);
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
/*
Generate a line of key k and value v, separated by
character sep. If sep appears in k, it is escaped.
Helper for synaxes with different separators.
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
mkValueString specifies how values should be formatted.
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
mkKeyValueDefault {} ":" "f:oo" "bar"
> "f\:oo:bar"
*/
mkKeyValueDefault =
{
mkValueString ? mkValueStringDefault { },
}:
sep: k: v:
"${escape [ sep ] k}${sep}${mkValueString v}";
2024-05-01 22:14:04 +00:00
## -- FILE FORMAT GENERATORS --
2024-06-30 08:12:46 +00:00
/*
Generate a key-value-style config file from an attrset.
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
mkKeyValue is the same as in toINI.
*/
toKeyValue =
{
mkKeyValue ? mkKeyValueDefault { } "=",
listsAsDuplicateKeys ? false,
indent ? "",
}:
2024-05-01 22:14:04 +00:00
let
2024-06-30 08:12:46 +00:00
mkLine = k: v: indent + mkKeyValue k v + "\n";
mkLines =
if listsAsDuplicateKeys then
k: v: map (mkLine k) (if isList v then v else [ v ])
else
k: v: [ (mkLine k v) ];
in
attrs: concatStrings (concatLists (mapAttrsToList mkLines attrs));
/*
Generate an INI-style config file from an
attrset of sections to an attrset of key-value pairs.
generators.toINI {} {
foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
baz = { "also, integers" = 42; };
}
> [baz]
> also, integers=42
>
> [foo]
> ciao=bar
> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
The mk* configuration attributes can generically change
the way sections and key-value strings are generated.
For more examples see the test cases in ./tests/misc.nix.
*/
toINI =
{
# apply transformations (e.g. escapes) to section names
mkSectionName ? (
name:
escape [
"["
"]"
] name
),
# format a setting line from key and value
mkKeyValue ? mkKeyValueDefault { } "=",
# allow lists as values for duplicate keys
listsAsDuplicateKeys ? false,
}:
attrsOfAttrs:
let
# map function to string for each key val
mapAttrsToStringsSep =
sep: mapFn: attrs:
concatStringsSep sep (mapAttrsToList mapFn attrs);
mkSection =
sectName: sectValues:
''
2024-05-01 22:14:04 +00:00
[${mkSectionName sectName}]
2024-06-30 08:12:46 +00:00
''
+ toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } sectValues;
2024-05-01 22:14:04 +00:00
in
2024-06-30 08:12:46 +00:00
# map input to ini sections
mapAttrsToStringsSep "\n" mkSection attrsOfAttrs;
/*
Generate an INI-style config file from an attrset
specifying the global section (no header), and an
attrset of sections to an attrset of key-value pairs.
generators.toINIWithGlobalSection {} {
globalSection = {
someGlobalKey = "hi";
};
sections = {
foo = { hi = "${pkgs.hello}"; ciao = "bar"; };
baz = { "also, integers" = 42; };
}
> someGlobalKey=hi
>
> [baz]
> also, integers=42
>
> [foo]
> ciao=bar
> hi=/nix/store/y93qql1p5ggfnaqjjqhxcw0vqw95rlz0-hello-2.10
The mk* configuration attributes can generically change
the way sections and key-value strings are generated.
For more examples see the test cases in ./tests/misc.nix.
If you dont need a global section, you can also use
`generators.toINI` directly, which only takes
the part in `sections`.
*/
toINIWithGlobalSection =
{
# apply transformations (e.g. escapes) to section names
mkSectionName ? (
name:
escape [
"["
"]"
] name
),
# format a setting line from key and value
mkKeyValue ? mkKeyValueDefault { } "=",
# allow lists as values for duplicate keys
listsAsDuplicateKeys ? false,
}:
{
globalSection,
sections ? { },
}:
(
if globalSection == { } then
""
else
(toKeyValue { inherit mkKeyValue listsAsDuplicateKeys; } globalSection) + "\n"
)
2024-05-01 22:14:04 +00:00
+ (toINI { inherit mkSectionName mkKeyValue listsAsDuplicateKeys; } sections);
2024-06-30 08:12:46 +00:00
/*
Generate a git-config file from an attrset.
It has two major differences from the regular INI format:
1. values are indented with tabs
2. sections can have sub-sections
generators.toGitINI {
url."ssh://git@github.com/".insteadOf = "https://github.com";
user.name = "edolstra";
}
> [url "ssh://git@github.com/"]
> insteadOf = "https://github.com"
>
> [user]
> name = "edolstra"
*/
toGitINI =
attrs:
2024-05-01 22:14:04 +00:00
let
2024-06-30 08:12:46 +00:00
mkSectionName =
name:
2024-05-01 22:14:04 +00:00
let
containsQuote = hasInfix ''"'' name;
sections = splitString "." name;
section = head sections;
subsections = tail sections;
subsection = concatStringsSep "." subsections;
2024-06-30 08:12:46 +00:00
in
if containsQuote || subsections == [ ] then name else ''${section} "${subsection}"'';
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
mkValueString =
v:
2024-05-01 22:14:04 +00:00
let
2024-06-30 08:12:46 +00:00
escapedV = ''"${
replaceStrings
[
"\n"
" "
''"''
"\\"
]
[
"\\n"
"\\t"
''\"''
"\\\\"
]
v
}"'';
in
mkValueStringDefault { } (if isString v then escapedV else v);
2024-05-01 22:14:04 +00:00
# generation for multiple ini values
2024-06-30 08:12:46 +00:00
mkKeyValue =
k: v:
let
mkKeyValue = mkKeyValueDefault { inherit mkValueString; } " = " k;
in
concatStringsSep "\n" (map (kv: "\t" + mkKeyValue kv) (toList v));
2024-05-01 22:14:04 +00:00
# converts { a.b.c = 5; } to { "a.b".c = 5; } for toINI
2024-06-30 08:12:46 +00:00
gitFlattenAttrs =
let
recurse =
path: value:
if isAttrs value && !isDerivation value then
mapAttrsToList (name: value: recurse ([ name ] ++ path) value) value
else if length path > 1 then
{ ${concatStringsSep "." (reverseList (tail path))}.${head path} = value; }
else
{ ${head path} = value; };
in
attrs: foldl recursiveUpdate { } (flatten (recurse [ ] attrs));
2024-05-01 22:14:04 +00:00
toINI_ = toINI { inherit mkKeyValue mkSectionName; };
in
2024-06-30 08:12:46 +00:00
toINI_ (gitFlattenAttrs attrs);
2024-05-01 22:14:04 +00:00
# mkKeyValueDefault wrapper that handles dconf INI quirks.
# The main differences of the format is that it requires strings to be quoted.
mkDconfKeyValue = mkKeyValueDefault { mkValueString = v: toString (gvariant.mkValue v); } "=";
# Generates INI in dconf keyfile style. See https://help.gnome.org/admin/system-admin-guide/stable/dconf-keyfiles.html.en
# for details.
toDconfINI = toINI { mkKeyValue = mkDconfKeyValue; };
withRecursion =
{
2024-06-30 08:12:46 +00:00
# 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
throwOnDepthLimit ? true,
2024-05-01 22:14:04 +00:00
}:
2024-06-30 08:12:46 +00:00
assert isInt depthLimit;
let
specialAttrs = [
"__functor"
"__functionArgs"
"__toString"
"__pretty"
];
stepIntoAttr = evalNext: name: if elem name specialAttrs then id else evalNext;
transform =
depth:
if depthLimit != null && depth > depthLimit then
if throwOnDepthLimit then
throw "Exceeded maximum eval-depth limit of ${toString depthLimit} while trying to evaluate with `generators.withRecursion'!"
else
const "<unevaluated>"
else
id;
mapAny =
depth: v:
let
evalNext = x: mapAny (depth + 1) (transform (depth + 1) x);
in
if isAttrs v then
mapAttrs (stepIntoAttr evalNext) v
else if isList v then
map evalNext v
else
transform (depth + 1) v;
in
mapAny 0;
/*
Pretty print a value, akin to `builtins.trace`.
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,
# Initial indentation level
indent ? "",
}:
let
go =
indent: v:
let
introSpace = if multiline then "\n${indent} " else " ";
outroSpace = if multiline then "\n${indent}" else " ";
in
if isInt 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
2024-05-01 22:14:04 +00:00
let
2024-06-30 08:12:46 +00:00
lines = filter (v: !isList v) (split "\n" v);
escapeSingleline = escape [
"\\"
"\""
"\${"
];
escapeMultiline =
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 = last escapedLines;
in
"''"
+ introSpace
+ concatStringsSep introSpace (init escapedLines)
+ (if lastLine == "" then outroSpace else introSpace + lastLine)
+ "''";
2024-05-01 22:14:04 +00:00
in
2024-06-30 08:12:46 +00:00
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"
else if isPath v then
toString v
else if isList v then
if v == [ ] then
"[ ]"
else
"[" + introSpace + concatMapStringsSep introSpace (go (indent + " ")) v + outroSpace + "]"
else if isFunction v then
let
fna = functionArgs v;
showFnas = concatStringsSep ", " (
mapAttrsToList (name: hasDefVal: if hasDefVal then name + "?" else name) fna
);
in
if fna == { } then "<function>" else "<function, args: {${showFnas}}>"
else if isAttrs v then
# apply pretty values if allowed
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.name or "???"}>"
else
"{"
+ introSpace
+ concatStringsSep introSpace (
mapAttrsToList (
name: value:
2024-05-01 22:14:04 +00:00
"${escapeNixIdentifier name} = ${
2024-06-30 08:12:46 +00:00
addErrorContext "while evaluating an attribute `${name}`" (go (indent + " ") value)
};"
) v
)
+ outroSpace
+ "}"
else
abort "generators.toPretty: should never happen (v = ${v})";
in
go indent;
2024-05-01 22:14:04 +00:00
# PLIST handling
2024-06-30 08:12:46 +00:00
toPlist =
{ }:
v:
let
expr =
ind: x:
if x == null then
""
else if isBool x then
bool ind x
else if isInt x then
int ind x
else 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})";
literal = ind: x: ind + x;
bool = ind: x: literal ind (if x then "<true/>" else "<false/>");
int = ind: x: literal ind "<integer>${toString x}</integer>";
str = ind: x: literal ind "<string>${x}</string>";
key = ind: x: literal ind "<key>${x}</key>";
float = ind: x: literal ind "<real>${toString x}</real>";
indent = ind: expr "\t${ind}";
item = ind: concatMapStringsSep "\n" (indent ind);
list =
ind: x:
concatStringsSep "\n" [
(literal ind "<array>")
(item ind x)
(literal ind "</array>")
];
attrs =
ind: x:
concatStringsSep "\n" [
(literal ind "<dict>")
(attr ind x)
(literal ind "</dict>")
];
attr =
let
attrFilter = name: value: name != "_module" && value != null;
in
ind: x:
concatStringsSep "\n" (
flatten (
mapAttrsToList (
name: value:
optionals (attrFilter name value) [
(key "\t${ind}" name)
(expr "\t${ind}" value)
]
) x
)
);
in
''
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
${expr "" v}
</plist>'';
/*
Translate a simple Nix expression to Dhall notation.
Note that integers are translated to Integer and never
the Natural type.
2024-05-01 22:14:04 +00:00
*/
2024-06-30 08:12:46 +00:00
toDhall =
{ }@args:
v:
let
concatItems = concatStringsSep ", ";
in
if isAttrs v then
"{ ${concatItems (mapAttrsToList (key: value: "${key} = ${toDhall args value}") v)} }"
2024-05-01 22:14:04 +00:00
else if isList v then
"[ ${concatItems (map (toDhall args) v)} ]"
else if isInt v then
"${if v < 0 then "" else "+"}${toString v}"
else if isBool v then
(if v then "True" else "False")
else if isFunction v then
abort "generators.toDhall: cannot convert a function to Dhall"
else if v == null then
abort "generators.toDhall: cannot convert a null to Dhall"
else
toJSON v;
/*
2024-06-30 08:12:46 +00:00
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)'';
}
->
2024-05-01 22:14:04 +00:00
{
2024-06-30 08:12:46 +00:00
["cmd"] = {
"typescript-language-server",
"--stdio"
},
["settings"] = {
["workspace"] = {
["library"] = (vim.api.nvim_get_runtime_file("", true))
}
}
2024-05-01 22:14:04 +00:00
}
2024-06-30 08:12:46 +00:00
Type:
toLua :: AttrSet -> Any -> String
2024-05-01 22:14:04 +00:00
*/
2024-06-30 08:12:46 +00:00
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:
2024-05-01 22:14:04 +00:00
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}";
2024-06-30 08:12:46 +00:00
isLuaInline =
{
_type ? null,
...
}:
_type == "lua-inline";
2024-05-01 22:14:04 +00:00
generatedBindings =
2024-06-30 08:12:46 +00:00
assert assertMsg (badVarNames == [ ]) "Bad Lua var names: ${toPretty { } badVarNames}";
concatStrings (mapAttrsToList (key: value: "${indent}${key} = ${toLua innerArgs value}\n") v);
2024-05-01 22:14:04 +00:00
# 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
toJSON v
else if isList v then
2024-06-30 08:12:46 +00:00
(
if v == [ ] then
"{}"
else
"{${introSpace}${concatItems (map (value: "${toLua innerArgs value}") v)}${outroSpace}}"
)
2024-05-01 22:14:04 +00:00
else if isAttrs v then
(
if isLuaInline v then
"(${v.expr})"
else if v == { } then
"{}"
else if isDerivation v then
''"${toString v}"''
else
2024-06-30 08:12:46 +00:00
"{${introSpace}${
concatItems (mapAttrsToList (key: value: "[${toJSON key}] = ${toLua innerArgs value}") v)
}${outroSpace}}"
2024-05-01 22:14:04 +00:00
)
else
abort "generators.toLua: type ${typeOf v} is unsupported";
/*
2024-06-30 08:12:46 +00:00
Mark string as Lua expression to be inlined when processed by toLua.
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
Type:
mkLuaInline :: String -> AttrSet
2024-05-01 22:14:04 +00:00
*/
2024-06-30 08:12:46 +00:00
mkLuaInline = expr: {
_type = "lua-inline";
inherit expr;
};
2024-05-01 22:14:04 +00:00
in
# Everything in this attrset is the public interface of the file.
{
inherit
mkDconfKeyValue
mkKeyValueDefault
mkLuaInline
mkValueStringDefault
toDconfINI
toDhall
toGitINI
toINI
toINIWithGlobalSection
toKeyValue
toLua
toPlist
toPretty
withRecursion
;
2024-06-30 08:12:46 +00:00
/*
Generates JSON from an arbitrary (non-function) value.
2024-05-01 22:14:04 +00:00
* For more information see the documentation of the builtin.
2024-06-30 08:12:46 +00:00
*/
toJSON = { }: toJSON;
2024-05-01 22:14:04 +00:00
2024-06-30 08:12:46 +00:00
/*
YAML has been a strict superset of JSON since 1.2, so we
2024-05-01 22:14:04 +00:00
* use toJSON. Before it only had a few differences referring
* to implicit typing rules, so it should work with older
* parsers as well.
2024-06-30 08:12:46 +00:00
*/
toYAML = { }: toJSON;
2024-05-01 22:14:04 +00:00
}