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.
This commit is contained in:
Michael Gratton 2020-08-26 15:20:12 +10:00 committed by Michael James Gratton
parent d7af23201c
commit 1ba2bd0f5b
2 changed files with 284 additions and 0 deletions

View file

@ -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.
*

View file

@ -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()
);
}
}