diff --git a/doc/default.nix b/doc/default.nix index f4270ae856d5..58b945c692fc 100644 --- a/doc/default.nix +++ b/doc/default.nix @@ -21,6 +21,7 @@ let { name = "filesystem"; description = "filesystem functions"; } { name = "sources"; description = "source filtering functions"; } { name = "cli"; description = "command-line serialization functions"; } + { name = "gvariant"; description = "GVariant formatted string serialization functions"; } ]; }; diff --git a/lib/default.nix b/lib/default.nix index 73b8ad871544..56af195d7c95 100644 --- a/lib/default.nix +++ b/lib/default.nix @@ -41,6 +41,7 @@ let # serialization cli = callLibs ./cli.nix; + gvariant = callLibs ./gvariant.nix; generators = callLibs ./generators.nix; # misc diff --git a/lib/gvariant.nix b/lib/gvariant.nix new file mode 100644 index 000000000000..3142ffc5f149 --- /dev/null +++ b/lib/gvariant.nix @@ -0,0 +1,290 @@ +# This file is based on https://github.com/nix-community/home-manager +# Copyright (c) 2017-2022 Home Manager contributors +# + + +{ lib }: + +/* A partial and basic implementation of GVariant formatted strings. + See https://docs.gtk.org/glib/gvariant-format-strings.html for detauls. + + Note, this API is not considered fully stable and it might therefore + change in backwards incompatible ways without prior notice. +*/ +let + inherit (lib) + concatMapStringsSep concatStrings escape head replaceStrings; + + mkPrimitive = t: v: { + _type = "gvariant"; + type = t; + value = v; + __toString = self: "@${self.type} ${toString self.value}"; # https://docs.gtk.org/glib/gvariant-text.html + }; + + type = { + arrayOf = t: "a${t}"; + maybeOf = t: "m${t}"; + tupleOf = ts: "(${concatStrings ts})"; + dictionaryEntryOf = nameType: valueType: "{${nameType}${valueType}}"; + string = "s"; + boolean = "b"; + uchar = "y"; + int16 = "n"; + uint16 = "q"; + int32 = "i"; + uint32 = "u"; + int64 = "x"; + uint64 = "t"; + double = "d"; + variant = "v"; + }; + + /* Check if a value is a GVariant value + + Type: + isGVariant :: Any -> Bool + */ + isGVariant = v: v._type or "" == "gvariant"; + +in +rec { + + inherit type isGVariant; + + /* Returns the GVariant value that most closely matches the given Nix value. + If no GVariant value can be found unambiguously then error is thrown. + + Type: + mkValue :: Any -> gvariant + */ + mkValue = v: + if builtins.isBool v then + mkBoolean v + else if builtins.isFloat v then + mkDouble v + else if builtins.isString v then + mkString v + else if builtins.isList v then + mkArray v + else if isGVariant v then + v + else + throw "The GVariant type of ${v} can't be inferred."; + + /* Returns the GVariant array from the given type of the elements and a Nix list. + + Type: + mkArray :: [Any] -> gvariant + + Example: + # Creating a string array + lib.gvariant.mkArray [ "a" "b" "c" ] + */ + mkArray = elems: + let + vs = map mkValue (lib.throwIf (elems == [ ]) "Please create empty array with mkEmptyArray." elems); + elemType = lib.throwIfNot (lib.all (t: (head vs).type == t) (map (v: v.type) vs)) + "Elements in a list should have same type." + (head vs).type; + in + mkPrimitive (type.arrayOf elemType) vs // { + __toString = self: + "@${self.type} [${concatMapStringsSep "," toString self.value}]"; + }; + + /* Returns the GVariant array from the given empty Nix list. + + Type: + mkEmptyArray :: gvariant.type -> gvariant + + Example: + # Creating an empty string array + lib.gvariant.mkEmptyArray (lib.gvariant.type.string) + */ + mkEmptyArray = elemType: mkPrimitive (type.arrayOf elemType) [ ] // { + __toString = self: "@${self.type} []"; + }; + + + /* Returns the GVariant variant from the given Nix value. Variants are containers + of different GVariant type. + + Type: + mkVariant :: Any -> gvariant + + Example: + lib.gvariant.mkArray [ + (lib.gvariant.mkVariant "a string") + (lib.gvariant.mkVariant (lib.gvariant.mkInt32 1)) + ] + */ + mkVariant = elem: + let gvarElem = mkValue elem; + in mkPrimitive type.variant gvarElem // { + __toString = self: "<${toString self.value}>"; + }; + + /* Returns the GVariant dictionary entry from the given key and value. + + Type: + mkDictionaryEntry :: String -> Any -> gvariant + + Example: + # A dictionary describing an Epiphany’s search provider + [ + (lib.gvariant.mkDictionaryEntry "url" (lib.gvariant.mkVariant "https://duckduckgo.com/?q=%s&t=epiphany")) + (lib.gvariant.mkDictionaryEntry "bang" (lib.gvariant.mkVariant "!d")) + (lib.gvariant.mkDictionaryEntry "name" (lib.gvariant.mkVariant "DuckDuckGo")) + ] + */ + mkDictionaryEntry = + # The key of the entry + name: + # The value of the entry + value: + let + name' = mkValue name; + value' = mkValue value; + dictionaryType = type.dictionaryEntryOf name'.type value'.type; + in + mkPrimitive dictionaryType { inherit name value; } // { + __toString = self: "@${self.type} {${name'},${value'}}"; + }; + + /* Returns the GVariant maybe from the given element type. + + Type: + mkMaybe :: gvariant.type -> Any -> gvariant + */ + mkMaybe = elemType: elem: + mkPrimitive (type.maybeOf elemType) elem // { + __toString = self: + if self.value == null then + "@${self.type} nothing" + else + "just ${toString self.value}"; + }; + + /* Returns the GVariant nothing from the given element type. + + Type: + mkNothing :: gvariant.type -> gvariant + */ + mkNothing = elemType: mkMaybe elemType null; + + /* Returns the GVariant just from the given Nix value. + + Type: + mkJust :: Any -> gvariant + */ + mkJust = elem: let gvarElem = mkValue elem; in mkMaybe gvarElem.type gvarElem; + + /* Returns the GVariant tuple from the given Nix list. + + Type: + mkTuple :: [Any] -> gvariant + */ + mkTuple = elems: + let + gvarElems = map mkValue elems; + tupleType = type.tupleOf (map (e: e.type) gvarElems); + in + mkPrimitive tupleType gvarElems // { + __toString = self: + "@${self.type} (${concatMapStringsSep "," toString self.value})"; + }; + + /* Returns the GVariant boolean from the given Nix bool value. + + Type: + mkBoolean :: Bool -> gvariant + */ + mkBoolean = v: + mkPrimitive type.boolean v // { + __toString = self: if self.value then "true" else "false"; + }; + + /* Returns the GVariant string from the given Nix string value. + + Type: + mkString :: String -> gvariant + */ + mkString = v: + let sanitize = s: replaceStrings [ "\n" ] [ "\\n" ] (escape [ "'" "\\" ] s); + in mkPrimitive type.string v // { + __toString = self: "'${sanitize self.value}'"; + }; + + /* Returns the GVariant object path from the given Nix string value. + + Type: + mkObjectpath :: String -> gvariant + */ + mkObjectpath = v: + mkPrimitive type.string v // { + __toString = self: "objectpath '${escape [ "'" ] self.value}'"; + }; + + /* Returns the GVariant uchar from the given Nix int value. + + Type: + mkUchar :: Int -> gvariant + */ + mkUchar = mkPrimitive type.uchar; + + /* Returns the GVariant int16 from the given Nix int value. + + Type: + mkInt16 :: Int -> gvariant + */ + mkInt16 = mkPrimitive type.int16; + + /* Returns the GVariant uint16 from the given Nix int value. + + Type: + mkUint16 :: Int -> gvariant + */ + mkUint16 = mkPrimitive type.uint16; + + /* Returns the GVariant int32 from the given Nix int value. + + Type: + mkInt32 :: Int -> gvariant + */ + mkInt32 = v: + mkPrimitive type.int32 v // { + __toString = self: toString self.value; + }; + + /* Returns the GVariant uint32 from the given Nix int value. + + Type: + mkUint32 :: Int -> gvariant + */ + mkUint32 = mkPrimitive type.uint32; + + /* Returns the GVariant int64 from the given Nix int value. + + Type: + mkInt64 :: Int -> gvariant + */ + mkInt64 = mkPrimitive type.int64; + + /* Returns the GVariant uint64 from the given Nix int value. + + Type: + mkUint64 :: Int -> gvariant + */ + mkUint64 = mkPrimitive type.uint64; + + /* Returns the GVariant double from the given Nix float value. + + Type: + mkDouble :: Float -> gvariant + */ + mkDouble = v: + mkPrimitive type.double v // { + __toString = self: toString self.value; + }; +} diff --git a/lib/tests/modules/gvariant.nix b/lib/tests/modules/gvariant.nix new file mode 100644 index 000000000000..a792ebf85b74 --- /dev/null +++ b/lib/tests/modules/gvariant.nix @@ -0,0 +1,93 @@ +{ config, lib, ... }: + +let inherit (lib) concatStringsSep mapAttrsToList mkMerge mkOption types gvariant; +in { + options.examples = mkOption { type = types.attrsOf gvariant; }; + + config = { + examples = with gvariant; + mkMerge [ + { bool = true; } + { bool = true; } + + { float = 3.14; } + + { int32 = mkInt32 (- 42); } + { int32 = mkInt32 (- 42); } + + { uint32 = mkUint32 42; } + { uint32 = mkUint32 42; } + + { int16 = mkInt16 (-42); } + { int16 = mkInt16 (-42); } + + { uint16 = mkUint16 42; } + { uint16 = mkUint16 42; } + + { int64 = mkInt64 (-42); } + { int64 = mkInt64 (-42); } + + { uint64 = mkUint64 42; } + { uint64 = mkUint64 42; } + + { array1 = [ "one" ]; } + { array1 = mkArray [ "two" ]; } + { array2 = mkArray [ (mkInt32 1) ]; } + { array2 = mkArray [ (nkUint32 2) ]; } + + { emptyArray1 = [ ]; } + { emptyArray2 = mkEmptyArray type.uint32; } + + { string = "foo"; } + { string = "foo"; } + { + escapedString = '' + '\ + ''; + } + + { tuple = mkTuple [ (mkInt32 1) [ "foo" ] ]; } + + { maybe1 = mkNothing type.string; } + { maybe2 = mkJust (mkUint32 4); } + + { variant1 = mkVariant "foo"; } + { variant2 = mkVariant 42; } + + { dictionaryEntry = mkDictionaryEntry (mkInt32 1) [ "foo" ]; } + ]; + + assertions = [ + { + assertion = ( + let + mkLine = n: v: "${n} = ${toString (gvariant.mkValue v)}"; + result = concatStringsSep "\n" (mapAttrsToList mkLine config.examples); + in + result + "\n" + ) == '' + array1 = @as ['one','two'] + array2 = @au [1,2] + bool = true + dictionaryEntry = @{ias} {1,@as ['foo']} + emptyArray1 = @as [] + emptyArray2 = @au [] + escapedString = '\'\\\n' + float = 3.140000 + int = -42 + int16 = @n -42 + int64 = @x -42 + maybe1 = @ms nothing + maybe2 = just @u 4 + string = 'foo' + tuple = @(ias) (1,@as ['foo']) + uint16 = @q 42 + uint32 = @u 42 + uint64 = @t 42 + variant1 = @v <'foo'> + variant2 = @v <42> + ''; + } + ]; + }; +}