diff --git a/modules/lib/default.nix b/modules/lib/default.nix index 6b2bbacc2..266aa66a4 100644 --- a/modules/lib/default.nix +++ b/modules/lib/default.nix @@ -16,8 +16,10 @@ rec { entryBefore = d.dagEntryBefore; }; + gvariant = import ./gvariant.nix { inherit lib; }; + strings = import ./strings.nix { inherit lib; }; - types = import ./types.nix { inherit dag lib; }; + types = import ./types.nix { inherit dag gvariant lib; }; shell = import ./shell.nix { inherit lib; }; zsh = import ./zsh.nix { inherit lib; }; diff --git a/modules/lib/gvariant.nix b/modules/lib/gvariant.nix new file mode 100644 index 000000000..83951c207 --- /dev/null +++ b/modules/lib/gvariant.nix @@ -0,0 +1,141 @@ +# A partial and basic implementation of GVariant formatted strings. +# +# Note, this API is not considered fully stable and it might therefore +# change in backwards incompatible ways without prior notice. + +{ lib }: + +with lib; + +let + + mkPrimitive = t: v: { + _type = "gvariant"; + type = t; + value = v; + __toString = self: "@${self.type} ${toString self.value}"; + }; + + type = { + arrayOf = t: "a${t}"; + tupleOf = ts: "(${concatStrings ts})"; + string = "s"; + boolean = "b"; + uchar = "y"; + int16 = "n"; + uint16 = "q"; + int32 = "i"; + uint32 = "u"; + int64 = "x"; + uint64 = "t"; + double = "d"; + }; + + # Returns the GVariant type of a given Nix value. If no type can be + # found for the value then the empty string is returned. + typeOf = v: + with type; + if builtins.isBool v then + boolean + else if builtins.isInt v then + int32 + else if builtins.isFloat v then + double + else if builtins.isString v then + string + else if builtins.isList v then + let elemType = elemTypeOf v; + in if elemType == "" then "" else arrayOf elemType + else if builtins.isAttrs v && v ? type then + v.type + else + ""; + + elemTypeOf = vs: + if builtins.isList vs then + if vs == [ ] then "" else typeOf (head vs) + else + ""; + +in rec { + + inherit type typeOf; + + isArray = hasPrefix "a"; + isTuple = hasPrefix "("; + + # Returns the GVariant value that most closely matches the given Nix + # value. If no GVariant value can be found then `null` is returned. + # + # No support for dictionaries, maybe types, or variants. + mkValue = v: + if builtins.isBool v then + mkBoolean v + else if builtins.isInt v then + mkInt32 v + else if builtins.isFloat v then + mkDouble v + else if builtins.isString v then + mkString v + else if builtins.isList v then + if v == [ ] then mkArray type.string [ ] else mkArray (elemTypeOf v) v + else if builtins.isAttrs v && (v._type or "") == "gvariant" then + v + else + null; + + mkArray = elemType: elems: + mkPrimitive (type.arrayOf elemType) (map mkValue elems) // { + __toString = self: + "@${self.type} [${concatMapStringsSep "," toString self.value}]"; + }; + + mkEmptyArray = elemType: mkArray elemType [ ]; + + 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})"; + }; + + mkBoolean = v: + mkPrimitive type.boolean v // { + __toString = self: if self.value then "true" else "false"; + }; + + mkString = v: + mkPrimitive type.string v // { + __toString = self: "'${escape [ "'" ] self.value}'"; + }; + + mkObjectpath = v: + mkPrimitive type.string v // { + __toString = self: "objectpath '${escape [ "'" ] self.value}'"; + }; + + mkUchar = mkPrimitive type.uchar; + + mkInt16 = mkPrimitive type.int16; + + mkUint16 = mkPrimitive type.uint16; + + mkInt32 = v: + mkPrimitive type.int32 v // { + __toString = self: toString self.value; + }; + + mkUint32 = mkPrimitive type.uint32; + + mkInt64 = mkPrimitive type.int64; + + mkUint64 = mkPrimitive type.uint64; + + mkDouble = v: + mkPrimitive type.double v // { + __toString = self: toString self.value; + }; + +} diff --git a/modules/lib/types.nix b/modules/lib/types.nix index fb871cf81..f47f717c8 100644 --- a/modules/lib/types.nix +++ b/modules/lib/types.nix @@ -1,4 +1,7 @@ -{ lib, dag ? import ./dag.nix { inherit lib; } }: +{ lib +, dag ? import ./dag.nix { inherit lib; } +, gvariant ? import ./gvariant.nix { inherit lib; } +}: with lib; @@ -6,9 +9,13 @@ let typesDag = import ./types-dag.nix { inherit dag lib; }; + # Needed since the type is called gvariant and its merge attribute + # must refer back to the type. + gvar = gvariant; + in -{ +rec { inherit (typesDag) dagOf listOrDagOf; @@ -56,4 +63,35 @@ in }; }; + gvariant = mkOptionType rec { + name = "gvariant"; + description = "GVariant value"; + check = v: gvar.mkValue v != null; + merge = loc: defs: + let + vdefs = map (d: d // { value = gvar.mkValue d.value; }) defs; + vals = map (d: d.value) vdefs; + defTypes = map (x: x.type) vals; + sameOrNull = x: y: if x == y then y else null; + # A bit naive to just check the first entry… + sharedDefType = foldl' sameOrNull (head defTypes) defTypes; + allChecked = all (x: check x) vals; + in + if sharedDefType == null then + throw ("Cannot merge definitions of `${showOption loc}' with" + + " mismatched GVariant types given in" + + " ${showFiles (getFiles defs)}.") + else if gvar.isArray sharedDefType && allChecked then + (types.listOf gvariant).merge + loc (map (d: d // { value = d.value.value; } ) vdefs) + else if gvar.isTuple sharedDefType && allChecked then + mergeOneOption loc defs + else if gvar.type.string == sharedDefType && allChecked then + types.str.merge loc defs + else if gvar.type.double == sharedDefType && allChecked then + types.float.merge loc defs + else + mergeDefaultOption loc defs; + }; + } diff --git a/tests/lib/types/default.nix b/tests/lib/types/default.nix index 9fce65f88..bc5cfbaba 100644 --- a/tests/lib/types/default.nix +++ b/tests/lib/types/default.nix @@ -1,4 +1,6 @@ { lib-types-dag-merge = ./dag-merge.nix; lib-types-list-or-dag-merge = ./list-or-dag-merge.nix; + + lib-types-gvariant-merge = ./gvariant-merge.nix; } diff --git a/tests/lib/types/gvariant-merge.nix b/tests/lib/types/gvariant-merge.nix new file mode 100644 index 000000000..0eeccf7f7 --- /dev/null +++ b/tests/lib/types/gvariant-merge.nix @@ -0,0 +1,55 @@ +{ config, lib, pkgs, ... }: + +with lib; + +let + +in { + options.examples = mkOption { type = types.attrsOf hm.types.gvariant; }; + + config = { + examples = with hm.gvariant; + mkMerge [ + { bool = true; } + { bool = true; } + + { float = 3.14; } + + { int = 42; } + { int = 42; } + + { list = [ "one" ]; } + { list = mkArray type.string [ "two" ]; } + + { emptyArray1 = [ ]; } + { emptyArray2 = mkEmptyArray type.uint32; } + + { string = "foo"; } + { string = "foo"; } + + { tuple = mkTuple [ 1 [ "foo" ] ]; } + ]; + + home.file."result.txt".text = let + mkLine = n: v: "${n} = ${toString (hm.gvariant.mkValue v)}"; + result = concatStringsSep "\n" (mapAttrsToList mkLine config.examples); + in result + "\n"; + + nmt.script = '' + assertFileContent \ + home-files/result.txt \ + ${ + pkgs.writeText "expected.txt" '' + bool = true + emptyArray1 = @as [] + emptyArray2 = @as [] + float = 3.140000 + int = 42 + list = @as ['one','two'] + string = 'foo' + tuple = @(ias) (1,@as ['foo']) + '' + } + ''; + }; +}