1
Fork 0
satellite/modules/common/korora-lua.nix

293 lines
9.4 KiB
Nix
Raw Normal View History

2023-12-24 19:19:12 +01:00
{ lib, korora, ... }:
let
k = korora;
# {{{ Lua encoders
# {{{ Helpers
helpers = rec {
xor = a: b: (a || b) && (!a || !b);
implies = a: b: !a || b;
hasProp = obj: p: (obj.${p} or null) != null;
propXor = a: b: obj: xor (hasProp obj a) (hasProp obj b);
propOnlyOne = props: obj:
1 == lib.count (prop: obj ? prop);
propImplies = a: b: obj: implies (hasProp obj a) (hasProp obj b);
mkVerify = checks: obj:
let
results = lib.lists.map checks obj;
errors = lib.lists.filter (v: v != null) results;
in
if errors == [ ]
then null
else
let prettyErrors =
lib.lists.map (s: "\n- ${s}") errors;
in
"Multiple errors occured: ${prettyErrors}";
intersection = l: r:
k.typedef'
"${l.name} ${r.name}"
(helpers.mkVerify [ l r ]);
dependentAttrsOf =
name: mkType:
let
typeError = name: v: "Expected type '${name}' but value '${toPretty v}' is of type '${typeOf v}'";
addErrorContext = context: error: if error == null then null else "${context}: ${error}";
withErrorContext = addErrorContext "in ${name} value";
in
k.typedef' name
(v:
if ! lib.isAttrs v then
typeError name v
else
withErrorContext
(mkVerify
(lib.mapAttrsToList (k: _: mkType k) v)
v));
mkRawLuaObject = chunks:
''
{
${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)}
}
'';
mkAttrName = s:
let
# These list *are* incomplete
forbiddenChars = lib.stringToCharacters "<>[]{}()'\".,;";
keywords = [ "if" "then" "else" "do" "for" "local" "" ];
in
if lib.any (c: lib.hasInfix c s) forbiddenChars || lib.elem s keywords then
"[${m.string s}]"
else s;
};
# }}}
# We provide a custom set of helpers for generating lua code for nix.enable
#
# An encoder is a function from some nix value to a string containing lua code.
# This object provides combinators for writing such encoders.
m = {
# {{{ General helpers
typed = type: toLua: type // {
unsafeToLua = toLua;
toLua = v: toLua (type.check v);
override = a: m.typed (type.override a) toLua;
};
withDefault = type: default: type // {
default.value = default;
};
typedWithDefault = type: toLua: default:
m.withDefault (m.typed type toLua) default;
unsafe = type: type // { toLua = type.unsafeToLua; };
untyped = m.typed k.any;
untype = type: m.untyped type.toLua;
withName = name: type: type // { inherit name; }; # TODO: should we use override here?
# `const` is mostly useful together with `bind`. See the lua encoder for
# lazy modules for example usage.
const = code: m.untyped (_: code);
# Conceptually, this is the monadic bind operation for encoders.
# This implementation is isomoprhic to that of the reader monad in haskell.
bind = name: higherOrder:
m.typed
(k.typedef' name (v: (higherOrder v).verify v))
(given: (higherOrder given).toLua given);
# This is probably the most useful combinnator defined in this entire object.
# Most of the combinators in the other categories are based on this.
conditional = predicate: caseTrue: caseFalse:
let base = m.bind
m.bind
"${caseTrue.name} ${caseFalse.name}"
(given: if predicate given then caseTrue else caseFalse);
in
if caseTrue ? default then
m.withDefault base caseTrue.default
else if caseFalse ? default then
m.withDefault base caseFalse.default
else
base;
try = caseTrue: caseFalse:
let base = m.bind
"${caseTrue.name} ${caseFalse.name}"
(given: if caseTrue.verify given == null then m.unsafe caseTrue else caseFalse);
in
if caseTrue ? default then
m.withDefault base caseTrue.default
else if caseFalse ? default then
m.withDefault base caseFalse.default
else
base;
oneOf = lib.foldr m.try
(m.bottom (v: "No variants matched for value ${builtins.toJSON v}"));
# This is simply left-composition of functions
cmap = f: t:
m.typed
(lib.typedef' t.name (v: t.verify (f v)))
(given: t.toLua (f given));
# This is simply right-composition of functions
map = f: t: m.typed t (given: f (t.toLua given));
filter = predicate: type: given:
m.conditional predicate type (m.untype m.nil);
# This is mostly useful for debugging
trace = message: m.cmap (f: lib.traceSeq message (lib.traceVal f));
bottom = mkMessage: m.typed (m.typedef "" mkMessage) lib.id;
# }}}
# {{{ Base types
string = m.typed k.string (given: ''"${lib.escape ["\"" "\\"] (toString given)}"'');
bool = m.typed k.bool (bool: if bool then "true" else "false");
integer = m.typed k.int toString;
float = m.typed k.float toString;
number = m.typed k.number toString;
ignored = type: m.typed type (_: "nil");
nil = m.typedWithDefault
(k.typedef "null" (v: v == null))
(_: "nil")
null;
stringOr = m.try m.string;
boolOr = m.try m.bool;
numberOr = m.try m.number;
nullOr = m.try m.nil;
anything = m.withName "" (m.oneOf [
m.markedLuaCode # Lua code expressions have priority over attrsets
m.string
m.number
m.bool
m.null
(m.listOf m.anything)
(m.attrsetOf m.anything)
]);
# }}}
# {{{ Lua code
identity = m.typed k.string lib.id;
markedLuaCode =
m.typed
(k.struct "marked lua code" {
value = k.string;
__luaEncoderTag = k.enum "lua encoder tag" [ "lua" ];
})
(obj: obj.value);
# This is the most rudimentary (and currently only) way of handling paths.
luaImport = tag:
m.typed (k.typedef "path" lib.isPath)
(path: "dofile(${m.string "${path}"}).${tag}");
# Accepts both tagged and untagged strings of lua code.
luaString = m.try m.markedLuaCode m.identity;
# This simply combines the above combinators into one.
luaCode = tag: m.try (m.luaImport tag) m.luaString;
# }}}
# {{{ Operators
conjunction = left: right: given:
m.typed (helpers.intersection left right) (
let
l = left.toLua given;
r = right.toLua given;
in
if l == "nil" then r
else if r == "nil" then l
else "${l} and ${r}"
);
all = lib.foldr m.conjunction m.nil;
# Similar to `all` but takes in a list and
# treats every element as a condition.
allIndices = name: type:
m.bind name
(g: lib.pipe g [
(lib.lists.imap0
(i: _:
m.cmap
(builtins.elemAt i)
type))
m.all
]);
# }}}
# {{{ Lists
listOf = type: list:
m.typedWithDefault
(k.listOf type)
(helpers.mkRawLuaObject (lib.lists.map type.toLua list))
[ ];
listOfOr = type: m.try (m.listOf type);
# Returns nil when given empty lists
tryNonemptyList = type:
m.typedWithDefault
(k.listOf type)
(m.filter
(l: l != [ ])
(m.listOf type))
[ ];
oneOrMany = type: m.listOfOr type type;
# Can encode:
# - zero values as nil
# - one value as itself
# - multiple values as a list
zeroOrMany = type: m.nullOr (m.oneOrMany type);
# Coerces non list values to lists of one element.
oneOrManyAsList = type: m.listOfOr type (m.map (e: [ e ]) type);
# Coerces lists of one element to said element.
listAsOneOrMany = type:
m.cmap
(l: if lib.length l == 1 then lib.head l else l)
(m.oneOrMany type);
# }}}
# {{{ Attrsets
attrsetOf = type:
m.typed (k.attrsOf type)
(object:
helpers.mkRawLuaObject (lib.mapAttrsToList
(name: value:
let result = type.toLua value;
in
lib.optionalString (result != "nil")
"${helpers.mkAttrName name} = ${result}"
)
object
)
);
# This is the most general combinator provided in this section.
#
# We accept:
# - order of props that should be interpreted as list elements
# - spec of props that should be interpreted as list elements
# - record of props that should be interpreted as attribute props
attrset = name: listOrder: spec: attrset:
m.cmap
(given: lib.mapAttrs
(key: type:
if given ? ${key} then
given.${key}
else
type.default or null)
spec)
(m.typed (k.struct name spec) (
let
listChunks = lib.lists.map
(attr:
let result = spec.${attr}.toLua (attrset.${attr} or null);
in
lib.optionalString (result != "nil") result
)
listOrder;
objectChunks = lib.mapAttrsToList
(attr: type:
let result = type.toLua (attrset.${attr} or null);
in
lib.optionalString (!(lib.elem attr listOrder) && result != "nil")
"${helpers.mkAttrName attr} = ${result}"
)
spec;
in
helpers.mkRawLuaObject (listChunks ++ objectChunks)
));
withAttrsCheck = type: verify:
type.override { inherit verify; };
# }}}
};
# }}}
in
m // { inherit helpers; }