diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 2723ed8db..f2d8c6ddc 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -16,6 +16,9 @@ Makefile @thiagokokada /modules/launchd @midchildan +/modules/lib/generators.nix @h7x4 +/test/lib/generators @h7x4 + /modules/misc/dconf.nix @rycee /modules/misc/editorconfig.nix @loicreynier diff --git a/modules/lib/default.nix b/modules/lib/default.nix index 59f707c4e..5d732a83f 100644 --- a/modules/lib/default.nix +++ b/modules/lib/default.nix @@ -6,6 +6,7 @@ rec { assertions = import ./assertions.nix { inherit lib; }; booleans = import ./booleans.nix { inherit lib; }; + generators = import ./generators.nix { inherit lib; }; gvariant = import ./gvariant.nix { inherit lib; }; maintainers = import ./maintainers.nix; strings = import ./strings.nix { inherit lib; }; diff --git a/modules/lib/generators.nix b/modules/lib/generators.nix new file mode 100644 index 000000000..98a03ba21 --- /dev/null +++ b/modules/lib/generators.nix @@ -0,0 +1,102 @@ +{ lib }: + +{ + toKDL = { }: + let + inherit (lib) concatStringsSep splitString mapAttrsToList any; + inherit (builtins) typeOf replaceStrings elem; + + # ListOf String -> String + indentStrings = let + # Although the input of this function is a list of strings, + # the strings themselves *will* contain newlines, so you need + # to normalize the list by joining and resplitting them. + unlines = lib.splitString "\n"; + lines = lib.concatStringsSep "\n"; + indentAll = lines: concatStringsSep "\n" (map (x: " " + x) lines); + in stringsWithNewlines: indentAll (unlines (lines stringsWithNewlines)); + + # String -> String + sanitizeString = replaceStrings [ "\n" ''"'' ] [ "\\n" ''\"'' ]; + + # OneOf [Int Float String Bool Null] -> String + literalValueToString = element: + lib.throwIfNot + (elem (typeOf element) [ "int" "float" "string" "bool" "null" ]) + "Cannot convert value of type ${typeOf element} to KDL literal." + (if typeOf element == "null" then + "null" + else if element == false then + "false" + else if element == true then + "true" + else if typeOf element == "string" then + ''"${sanitizeString element}"'' + else + toString element); + + # Attrset Conversion + # String -> AttrsOf Anything -> String + convertAttrsToKDL = name: attrs: + let + optArgsString = lib.optionalString (attrs ? "_args") + (lib.pipe attrs._args [ + (map literalValueToString) + (lib.concatStringsSep " ") + (s: s + " ") + ]); + + optPropsString = lib.optionalString (attrs ? "_props") + (lib.pipe attrs._props [ + (lib.mapAttrsToList + (name: value: "${name}=${literalValueToString value}")) + (lib.concatStringsSep " ") + (s: s + " ") + ]); + + children = + lib.filterAttrs (name: _: !(elem name [ "_args" "_props" ])) attrs; + in '' + ${name} ${optArgsString}${optPropsString}{ + ${indentStrings (mapAttrsToList convertAttributeToKDL children)} + }''; + + # List Conversion + # String -> ListOf (OneOf [Int Float String Bool Null]) -> String + convertListOfFlatAttrsToKDL = name: list: + let flatElements = map literalValueToString list; + in "${name} ${concatStringsSep " " flatElements}"; + + # String -> ListOf Anything -> String + convertListOfNonFlatAttrsToKDL = name: list: '' + ${name} { + ${indentStrings (map (x: convertAttributeToKDL "-" x) list)} + }''; + + # String -> ListOf Anything -> String + convertListToKDL = name: list: + let elementsAreFlat = !any (el: elem (typeOf el) [ "list" "set" ]) list; + in if elementsAreFlat then + convertListOfFlatAttrsToKDL name list + else + convertListOfNonFlatAttrsToKDL name list; + + # Combined Conversion + # String -> Anything -> String + convertAttributeToKDL = name: value: + let vType = typeOf value; + in if elem vType [ "int" "float" "bool" "null" "string" ] then + "${name} ${literalValueToString value}" + else if vType == "set" then + convertAttrsToKDL name value + else if vType == "list" then + convertListToKDL name value + else + throw '' + Cannot convert type `(${typeOf value})` to KDL: + ${name} = ${toString value} + ''; + in attrs: '' + ${concatStringsSep "\n" (mapAttrsToList convertAttributeToKDL attrs)} + ''; +} diff --git a/tests/default.nix b/tests/default.nix index 495fd5ae5..136dda549 100644 --- a/tests/default.nix +++ b/tests/default.nix @@ -47,6 +47,7 @@ import nmt { inherit lib pkgs modules; testedAttrPath = [ "home" "activationPackage" ]; tests = builtins.foldl' (a: b: a // (import b)) { } ([ + ./lib/generators ./lib/types ./modules/files ./modules/home-environment diff --git a/tests/lib/generators/default.nix b/tests/lib/generators/default.nix new file mode 100644 index 000000000..37bb20047 --- /dev/null +++ b/tests/lib/generators/default.nix @@ -0,0 +1 @@ +{ generators-tokdl = ./tokdl.nix; } diff --git a/tests/lib/generators/tokdl-result.txt b/tests/lib/generators/tokdl-result.txt new file mode 100644 index 000000000..6ad2af368 --- /dev/null +++ b/tests/lib/generators/tokdl-result.txt @@ -0,0 +1,41 @@ +a 1 +b "string" +bigFlatItems 23847590283751 1.239000 "multiline \" \" \"\nstring\n" null +c "multiline string\nwith special characters:\n\t \n \\" \"\n" +extraAttrs 2 true arg1=1 arg2=false { + nested { + a 1 + b null + } +} +flatItems 1 2 "asdf" true null +listInAttrsInList { + list1 { + - { + a 1 + } + - { + b true + } + - { + c null + d { + - { + e "asdfadfasdfasdf" + } + } + } + } + list2 { + - { + a 8 + } + } +} +nested { + - 1 2 + - true false + - + - null +} +unsafeString " \" \n " diff --git a/tests/lib/generators/tokdl.nix b/tests/lib/generators/tokdl.nix new file mode 100644 index 000000000..88c5752b8 --- /dev/null +++ b/tests/lib/generators/tokdl.nix @@ -0,0 +1,53 @@ +{ config, lib, ... }: + +{ + home.file."result.txt".text = lib.hm.generators.toKDL { } { + a = 1; + b = "string"; + c = '' + multiline string + with special characters: + \t \n \" " + ''; + unsafeString = " \" \n "; + flatItems = [ 1 2 "asdf" true null ]; + bigFlatItems = [ + 23847590283751 + 1.239 + '' + multiline " " " + string + '' + null + ]; + nested = [ [ 1 2 ] [ true false ] [ ] [ null ] ]; + extraAttrs = { + _args = [ 2 true ]; + _props = { + arg1 = 1; + arg2 = false; + }; + nested = { + a = 1; + b = null; + }; + }; + listInAttrsInList = { + list1 = [ + { a = 1; } + { b = true; } + { + c = null; + d = [{ e = "asdfadfasdfasdf"; }]; + } + ]; + list2 = [{ a = 8; }]; + }; + }; + + nmt.script = '' + assertFileContent \ + home-files/result.txt \ + ${./tokdl-result.txt} + ''; +}