From 1ba2bd0f5ba655b38aff63d6332b0bb52c704119 Mon Sep 17 00:00:00 2001
From: Michael Gratton <mike@vee.net>
Date: Wed, 26 Aug 2020 15:20:12 +1000
Subject: [PATCH 069/124] Util.JS: Support converting between JSC.Value and
 GLib.Variant objects

Add `variant_to_value` and `value_to_variant` methods, document them
and add tests.
---
 src/client/util/util-js.vala       | 159 +++++++++++++++++++++++++++++
 test/client/util/util-js-test.vala | 125 +++++++++++++++++++++++
 2 files changed, 284 insertions(+)

diff --git a/src/client/util/util-js.vala b/src/client/util/util-js.vala
index 52c9428b..2f05a3e2 100644
--- a/src/client/util/util-js.vala
+++ b/src/client/util/util-js.vala
@@ -127,6 +127,165 @@ namespace Util.JS {
         }
     }
 
+    /**
+     * Converts a JS value to a GLib variant.
+     *
+     * Simple value objects (string, number, and Boolean values),
+     * arrays of these, and objects with these types as properties are
+     * supported. Arrays are converted to arrays of variants, and
+     * objects to dictionaries containing string keys and variant
+     * values. Null or undefined values are returned as an empty maybe
+     * variant type, since it is not possible to determine the actual
+     * type.
+     *
+     * Throws a type error if the given value's type is not supported.
+     */
+    public inline GLib.Variant value_to_variant(JSC.Value value)
+        throws Error {
+        if (value.is_null() || value.is_undefined()) {
+            return new GLib.Variant.maybe(GLib.VariantType.VARIANT, null);
+        }
+        if (value.is_boolean()) {
+            return new GLib.Variant.boolean(value.to_boolean());
+        }
+        if (value.is_number()) {
+            return new GLib.Variant.double(value.to_double());
+        }
+        if (value.is_string()) {
+            return new GLib.Variant.string(value.to_string());
+        }
+        if (value.is_array()) {
+            int len = to_int32(value.object_get_property("length"));
+            GLib.Variant[] values = new GLib.Variant[len];
+            for (int i = 0; i < len; i++) {
+                values[i] = new GLib.Variant.variant(
+                    value_to_variant(value.object_get_property_at_index(i))
+                );
+            }
+            return new GLib.Variant.array(GLib.VariantType.VARIANT, values);
+        }
+        if (value.is_object()) {
+            GLib.VariantDict dict = new GLib.VariantDict();
+            string[] names = value.object_enumerate_properties();
+            if (names != null) {
+                foreach (var name in names) {
+                    try {
+                        dict.insert_value(
+                            name,
+                            new GLib.Variant.variant(
+                                value_to_variant(
+                                    value.object_get_property(name)
+                                )
+                            )
+                        );
+                    } catch (Error.TYPE err) {
+                        // ignored
+                    }
+                }
+            }
+            return dict.end();
+        }
+        throw new Error.TYPE("Unsupported JS type: %s", value.to_string());
+    }
+
+    /**
+     * Converts a GLib variant to a JS value.
+     *
+     * Simple value objects (string, number, and Boolean values),
+     * arrays and tuples of these, and dictionaries with string keys
+     * are supported. Tuples and arrays are converted to JS arrays,
+     * and dictionaries or tuples containing dictionary entries are
+     * converted to JS objects.
+     *
+     * Throws a type error if the given variant's type is not supported.
+     */
+    public inline JSC.Value variant_to_value(JSC.Context context,
+                                             GLib.Variant variant)
+        throws Error.TYPE {
+        JSC.Value? value = null;
+        GLib.Variant.Class type = variant.classify();
+        if (type == MAYBE) {
+            GLib.Variant? maybe = variant.get_maybe();
+            if (maybe != null) {
+                value = variant_to_value(context, maybe);
+            } else {
+                value = new JSC.Value.null(context);
+            }
+        } else if (type == VARIANT) {
+            value = variant_to_value(context, variant.get_variant());
+        } else if (type == STRING) {
+            value = new JSC.Value.string(context, variant.get_string());
+        } else if (type == BOOLEAN) {
+            value = new JSC.Value.boolean(context, variant.get_boolean());
+        } else if (type == DOUBLE) {
+            value = new JSC.Value.number(context, variant.get_double());
+        } else if (type == INT64) {
+            value = new JSC.Value.number(context, (double) variant.get_int64());
+        } else if (type == INT32) {
+            value = new JSC.Value.number(context, (double) variant.get_int32());
+        } else if (type == INT16) {
+            value = new JSC.Value.number(context, (double) variant.get_int16());
+        } else if (type == UINT64) {
+            value = new JSC.Value.number(context, (double) variant.get_uint64());
+        } else if (type == UINT32) {
+            value = new JSC.Value.number(context, (double) variant.get_uint32());
+        } else if (type == UINT16) {
+            value = new JSC.Value.number(context, (double) variant.get_uint16());
+        } else if (type == BYTE) {
+            value = new JSC.Value.number(context, (double) variant.get_byte());
+        } else if (type == ARRAY ||
+                   type == TUPLE) {
+            size_t len = variant.n_children();
+            if (len == 0) {
+                if (type == ARRAY ||
+                    type == TUPLE) {
+                    value = new JSC.Value.array_from_garray(context, null);
+                } else {
+                    value = new JSC.Value.object(context, null, null);
+                }
+            } else {
+                var first = variant.get_child_value(0);
+                if (first.classify() == DICT_ENTRY) {
+                    value = new JSC.Value.object(context, null, null);
+                    for (size_t i = 0; i < len; i++) {
+                        var entry = variant.get_child_value(i);
+                        if (entry.classify() != DICT_ENTRY) {
+                            throw new Error.TYPE(
+                                "Variant mixes dict entries with others: %s",
+                                variant.print(true)
+                            );
+                        }
+                        var key = entry.get_child_value(0);
+                        if (key.classify() != STRING) {
+                            throw new Error.TYPE(
+                                "Dict entry key is not a string: %s",
+                                entry.print(true)
+                            );
+                        }
+                        value.object_set_property(
+                            key.get_string(),
+                            variant_to_value(context, entry.get_child_value(1))
+                        );
+                    }
+                } else {
+                    var values = new GLib.GenericArray<JSC.Value>((uint) len);
+                    for (size_t i = 0; i < len; i++) {
+                        values.add(
+                            variant_to_value(context, variant.get_child_value(i))
+                        );
+                    }
+                    value = new JSC.Value.array_from_garray(context, values);
+                }
+            }
+        }
+        if (value == null) {
+            throw new Error.TYPE(
+                "Unsupported variant type %s", variant.print(true)
+            );
+        }
+        return value;
+    }
+
     /**
      * Escapes a string so as to be safe to use as a JS string literal.
      *
diff --git a/test/client/util/util-js-test.vala b/test/client/util/util-js-test.vala
index 1fbe5276..16a01d83 100644
--- a/test/client/util/util-js-test.vala
+++ b/test/client/util/util-js-test.vala
@@ -7,9 +7,23 @@
 
 public class Util.JS.Test : TestCase {
 
+
+    private JSC.Context? context = null;
+
+
     public Test() {
         base("Util.JS.Test");
         add_test("escape_string", escape_string);
+        add_test("to_variant", to_variant);
+        add_test("to_value", to_value);
+    }
+
+    public override void set_up() throws GLib.Error {
+        this.context = new JSC.Context();
+    }
+
+    public override void tear_down() throws GLib.Error {
+        this.context = null;
     }
 
     public void escape_string() throws GLib.Error {
@@ -21,4 +35,115 @@ public class Util.JS.Test : TestCase {
 
         assert(Util.JS.escape_string("something…\n") == """something…\n""");
     }
+
+    public void to_variant() throws GLib.Error {
+        assert_equal(
+            value_to_variant(new JSC.Value.null(this.context)).print(true),
+            "@mv nothing"
+        );
+        assert_equal(
+            value_to_variant(new JSC.Value.string(this.context, "test")).print(true),
+            "'test'"
+        );
+        assert_equal(
+            value_to_variant(new JSC.Value.number(this.context, 1.0)).print(true),
+            "1.0"
+        );
+        assert_equal(
+            value_to_variant(new JSC.Value.boolean(this.context, true)).print(true),
+            "true"
+        );
+        assert_equal(
+            value_to_variant(new JSC.Value.boolean(this.context, false)).print(true),
+            "false"
+        );
+
+        var value = new JSC.Value.array_from_garray(this.context, null);
+        assert_equal(
+            value_to_variant(value).print(true),
+            "@av []"
+        );
+        var array = new GLib.GenericArray<JSC.Value>();
+        array.add(new JSC.Value.string(this.context, "test"));
+        value = new JSC.Value.array_from_garray(this.context, array);
+        assert_equal(
+            value_to_variant(value).print(true),
+            "[<'test'>]"
+        );
+        value = new JSC.Value.object(this.context, null, null);
+        assert_equal(
+            value_to_variant(value).print(true),
+            "@a{sv} {}"
+        );
+        value.object_set_property(
+            "test", new JSC.Value.boolean(this.context, true)
+        );
+        assert_equal(
+            value_to_variant(value).print(true),
+            "{'test': <<true>>}"
+        );
+    }
+
+    public void to_value() throws GLib.Error {
+        var variant = new GLib.Variant.maybe(GLib.VariantType.STRING, null);
+        var value = variant_to_value(this.context, variant);
+        assert_true(value.is_null(), variant.print(true));
+
+        variant = new GLib.Variant.string("test");
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_string(), variant.print(true));
+        assert_equal(value.to_string(), "test", variant.print(true));
+
+        variant = new GLib.Variant.int32(42);
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_number(), variant.print(true));
+        assert_equal<int32?>(value.to_int32(), 42, variant.print(true));
+
+        variant = new GLib.Variant.double(42.0);
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_number(), variant.print(true));
+        assert_within(value.to_double(), 42.0, 0.0000001, variant.print(true));
+
+        variant = new GLib.Variant.boolean(true);
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_boolean(), variant.print(true));
+        assert_true(value.to_boolean(), variant.print(true));
+
+        variant = new GLib.Variant.boolean(false);
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_boolean(), variant.print(true));
+        assert_false(value.to_boolean(), variant.print(true));
+
+        variant = new GLib.Variant.strv({"test"});
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_array(), variant.print(true));
+        assert_true(
+            value.object_get_property_at_index(0).is_string(),
+            variant.print(true)
+        );
+        assert_equal(
+            value.object_get_property_at_index(0).to_string(),
+            "test",
+            variant.print(true)
+        );
+
+        var dict = new GLib.VariantDict();
+        variant = dict.end();
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_object(), variant.print(true));
+
+        dict = new GLib.VariantDict();
+        dict.insert_value("test", new GLib.Variant.boolean(true));
+        variant = dict.end();
+        value = variant_to_value(this.context, variant);
+        assert_true(value.is_object(), variant.print(true));
+        assert_true(
+            value.object_get_property("test").is_boolean(),
+            value.to_string()
+        );
+        assert_true(
+            value.object_get_property("test").to_boolean(),
+            value.to_string()
+        );
+    }
 }
-- 
2.29.2

