{ config, lib, pkgs, ... }: let # {{{ Lua encoders # 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. luaEncoders = { # {{{ "Raw" helpers mkRawLuaObject = chunks: '' { ${lib.concatStringsSep "," (lib.filter (s: s != "") chunks)} } ''; # }}} # {{{ General helpers identity = given: given; # `const` is mostly useful together with `bind`. See the lua encoder for # lazy modules for example usage. const = code: _: code; # Conceptually, this is the monadic bind operation for encoders. # This implementation is isomoprhic to that of the reader monad in haskell. bind = encoder: given: encoder given 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: luaEncoders.bind (given: if predicate given then caseTrue else caseFalse); # This is simply left-composition of functions map = f: encoder: given: encoder (f given); # This is simply right-composition of functions postmap = f: encoder: given: f (encoder given); filter = f: encoder: luaEncoders.conditional f encoder luaEncoders.nil; # This is mostly useful for debugging trace = message: luaEncoders.map (f: lib.traceSeq message (lib.traceVal f)); fail = mkMessage: v: builtins.throw (mkMessage v); # }}} # {{{ Base types string = given: ''"${lib.escape ["\"" "\\"] (toString given)}"''; bool = bool: if bool then "true" else "false"; number = toString; nil = _: "nil"; stringOr = luaEncoders.conditional lib.isString luaEncoders.string; boolOr = luaEncoders.conditional lib.isBool luaEncoders.bool; numberOr = luaEncoders.conditional (e: lib.isFloat e || lib.isInt e) luaEncoders.number; nullOr = luaEncoders.conditional (e: e == null) luaEncoders.nil; # We pipe a combinator which always fail through a bunch of # `(thing)or : encoder -> encoder` functions, building up a combinator which # can handle more and more kinds of values, until we eventually build up # something that should be able to handle everything we throw at it. anything = lib.pipe (luaEncoders.fail (v: "Cannot figure out how to encode value ${builtins.toJSON v}")) [ (luaEncoders.attrsetOfOr luaEncoders.anything) (luaEncoders.listOfOr luaEncoders.anything) luaEncoders.nullOr luaEncoders.boolOr luaEncoders.numberOr luaEncoders.stringOr luaEncoders.luaCodeOr # Lua code expressions have priority over attrsets ]; # }}} # {{{ Lua code # Tagged lua code can be combined with other combinators without worrying # about conflicts regarding how strings are interpreted. luaCodeOr = luaEncoders.conditional (e: lib.isAttrs e && (e.__luaEncoderTag or null) == "lua") (obj: obj.value); # This is the most rudimentary (and currently only) way of handling paths. luaImportOr = tag: luaEncoders.conditional lib.isPath (path: "dofile(${luaEncoders.string "${path}"}).${tag}"); # Accepts both tagged and untagged strings of lua code. luaString = luaEncoders.luaCodeOr luaEncoders.identity; # This simply combines the above combinators into one. luaCode = tag: luaEncoders.luaImportOr tag luaEncoders.luaString; # }}} # {{{ Operators conjunction = left: right: given: let l = left given; r = right given; in if l == "nil" then r else if r == "nil" then l else "${l} and ${r}"; all = lib.foldr luaEncoders.conjunction luaEncoders.nil; # }}} # {{{ Lists listOf = encoder: list: luaEncoders.mkRawLuaObject (lib.lists.map encoder list); listOfOr = encoder: luaEncoders.conditional lib.isList (luaEncoders.listOf encoder); # Returns nil when given empty lists tryNonemptyList = encoder: luaEncoders.filter (l: l != [ ]) (luaEncoders.listOf encoder); oneOrMany = encoder: luaEncoders.listOfOr encoder encoder; # Can encode: # - zero values as nil # - one value as itself # - multiple values as a list zeroOrMany = encoder: luaEncoders.nullOr (luaEncoders.oneOrMany encoder); # Coerces non list values to lists of one element. oneOrManyAsList = encoder: luaEncoders.map (given: if lib.isList given then given else [ given ]) (luaEncoders.listOf encoder); # Coerces lists of one element to said element. listAsOneOrMany = encoder: luaEncoders.map (l: if lib.length l == 1 then lib.head l else l) (luaEncoders.oneOrMany encoder); # }}} # {{{ Attrsets attrName = 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 "[${luaEncoders.string s}]" else s; attrsetOf = encoder: object: luaEncoders.mkRawLuaObject (lib.mapAttrsToList (name: value: let result = encoder value; in lib.optionalString (result != "nil") "${luaEncoders.attrName name} = ${result}" ) object ); attrsetOfOr = of: luaEncoders.conditional lib.isAttrs (luaEncoders.attrsetOf of); # This is the most general combinator provided in this section. # # We accept: # - a `noNils` flag which will automatically remove any nil properties # - 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 = noNils: listOrder: spec: attrset: let shouldKeep = given: if noNils then given != "nil" else true; listChunks = lib.lists.map (attr: let result = spec.${attr} (attrset.${attr} or null); in lib.optionalString (shouldKeep result) result ) listOrder; objectChunks = lib.mapAttrsToList (attr: encoder: let result = encoder (attrset.${attr} or null); in lib.optionalString (!(lib.elem attr listOrder) && shouldKeep result) "${luaEncoders.attrName attr} = ${result}" ) spec; in luaEncoders.mkRawLuaObject (listChunks ++ objectChunks); # }}} }; # }}} in { options.satellite.lib.lua = { encoders = lib.mkOption { # I am too lazy to make this typecheck type = lib.types.anything; description = "Combinators used to encode nix values as lua values"; }; writeFile = lib.mkOption { type = with lib.types; functionTo (functionTo (functionTo path)); description = "Format and write a lua file to disk"; }; }; options.satellite.lua.styluaConfig = lib.mkOption { type = lib.types.path; description = "Config to use for formatting lua modules"; }; config.satellite.lib.lua = { encoders = luaEncoders; writeFile = path: name: text: let destination = "${path}/${name}.lua"; unformatted = pkgs.writeText "raw-lua-${name}" '' -- ❄️ This file was generated using nix ^~^ ${text} ''; in pkgs.runCommand "formatted-lua-${name}" { } '' mkdir -p $out/${path} cp --no-preserve=mode ${unformatted} $out/${destination} ${lib.getExe pkgs.stylua} --config-path ${config.satellite.lua.styluaConfig} $out/${destination} ''; }; }