[js_util] Add `dartify` and some helpers.
Change-Id: I40822d87a7fef9e4de563ccb73046eee78f34b21
Reviewed-on: https://dart-review.googlesource.com/c/sdk/+/243846
Reviewed-by: Srujan Gaddam <srujzs@google.com>
Reviewed-by: Riley Porter <rileyporter@google.com>
Commit-Queue: Joshua Litt <joshualitt@google.com>
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c44a181..5a3cc15 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,10 @@
- Add `connectionState` attribute and `connectionstatechange` listener to
`RtcPeerConnection`.
+#### `dart:js_util`
+
+- Added `dartify` and a number of minor helper functions.
+
### Tools
#### Linter
diff --git a/sdk/lib/js_util/js_util.dart b/sdk/lib/js_util/js_util.dart
index ce77e95..0f57f47 100644
--- a/sdk/lib/js_util/js_util.dart
+++ b/sdk/lib/js_util/js_util.dart
@@ -417,3 +417,76 @@
JS('', '#.then(#, #)', jsPromise, success, error);
return completer.future;
}
+
+Object? _getConstructor(String constructorName) =>
+ getProperty(globalThis, constructorName);
+
+/// Like [instanceof] only takes a [String] for the object name instead of a
+/// constructor object.
+bool instanceOfString(Object? element, String objectType) {
+ Object? constructor = _getConstructor(objectType);
+ return constructor != null && instanceof(element, constructor);
+}
+
+/// Returns the prototype of a given object. Equivalent to
+/// `Object.getPrototypeOf`.
+Object? objectGetPrototypeOf(Object? object) =>
+ JS('', 'Object.getPrototypeOf(#)', object);
+
+/// Returns the `Object` prototype. Equivalent to `Object.prototype`.
+Object? get objectPrototype => JS('', 'Object.prototype');
+
+/// Returns the keys for a given object. Equivalent to `Object.keys(object)`.
+List<Object?> objectKeys(Object? object) => JS('', 'Object.keys(#)', object);
+
+/// Returns `true` if a given object is a JavaScript array.
+bool isJavaScriptArray(value) => instanceOfString(value, 'Array');
+
+/// Returns `true` if a given object is a simple JavaScript object.
+bool isJavaScriptSimpleObject(value) {
+ final Object? proto = objectGetPrototypeOf(value);
+ return proto == null || proto == objectPrototype;
+}
+
+/// Effectively the inverse of [jsify], [dartify] Takes a JavaScript object, and
+/// converts it to a Dart based object. Only JS primitives, arrays, or 'map'
+/// like JS objects are supported.
+Object? dartify(Object? o) {
+ var _convertedObjects = HashMap.identity();
+ Object? convert() {
+ if (_convertedObjects.containsKey(o)) {
+ return _convertedObjects[o];
+ }
+ if (o == null || o is bool || o is num || o is String) return o;
+ if (isJavaScriptSimpleObject(o)) {
+ Map<Object?, Object?> dartObject = {};
+ _convertedObjects[o] = dartObject;
+ List<Object?> originalKeys = objectKeys(o);
+ List<Object?> dartKeys = [];
+ for (Object? key in originalKeys) {
+ dartKeys.add(dartify(key));
+ }
+ for (int i = 0; i < originalKeys.length; i++) {
+ Object? jsKey = originalKeys[i];
+ Object? dartKey = dartKeys[i];
+ if (jsKey != null) {
+ dartObject[dartKey] = dartify(getProperty(o, jsKey));
+ }
+ }
+ return dartObject;
+ }
+ if (isJavaScriptArray(o)) {
+ List<Object?> dartObject = [];
+ _convertedObjects[o] = dartObject;
+ int length = getProperty(o, 'length');
+ for (int i = 0; i < length; i++) {
+ dartObject.add(dartify(getProperty(o, i)));
+ }
+ return dartObject;
+ }
+ throw ArgumentError(
+ "JavaScriptObject $o must be a primitive, simple object, or array");
+ }
+
+ return convert();
+}
diff --git a/tests/lib/js/js_util/dartify_test.dart b/tests/lib/js/js_util/dartify_test.dart
new file mode 100644
index 0000000..9fef877
--- /dev/null
+++ b/tests/lib/js/js_util/dartify_test.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Tests the dartify functionality of the js_util library.
+
+@JS()
+library js_util_jsify_test;
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart' as js_util;
+import 'package:expect/expect.dart';
+import 'package:expect/minitest.dart';
+
+@JS()
+external void eval(String code);
+
+main() {
+ eval(r"""
+ globalThis.arrayData = [1, 2, false, 4, 'hello', 6, [1, 2], {'foo': 'bar'}];
+ globalThis.recArrayData = [];
+ globalThis.recArrayData = [globalThis.recArrayData];
+ globalThis.objectData = {
+ 'a': 1,
+ 'b': [1, 2, 3],
+ 'c': {
+ 'a': true,
+ 'b': 'foo',
+ },
+ };
+ globalThis.recObjectData = {};
+ globalThis.recObjectData = {'foo': globalThis.recObjectData}
+ globalThis.throwData = function() {};
+ """);
+
+ test('convert an array', () {
+ Object? jsArray = js_util.getProperty(js_util.globalThis, 'arrayData');
+ Object? dartArray = js_util.dartify(jsArray);
+ List<Object?> expectedValues = [
+ 1,
+ 2,
+ false,
+ 4,
+ 'hello',
+ 6,
+ [1, 2],
+ {'foo': 'bar'}
+ ];
+ Expect.deepEquals(expectedValues, dartArray);
+ });
+
+ test('convert a recursive array', () {
+ Object? jsArray = js_util.getProperty(js_util.globalThis, 'recArrayData');
+ Object? dartArray = js_util.dartify(jsArray);
+ List<Object?> expectedValues = [[]];
+ Expect.deepEquals(expectedValues, dartArray);
+ });
+
+ test('convert an object literal', () {
+ Object? jsObject = js_util.getProperty(js_util.globalThis, 'objectData');
+ Object? dartObject = js_util.dartify(jsObject);
+ Map<Object?, Object?> expectedValues = {
+ 'a': 1,
+ 'b': [1, 2, 3],
+ 'c': {
+ 'a': true,
+ 'b': 'foo',
+ },
+ };
+ Expect.deepEquals(expectedValues, dartObject);
+ });
+
+ test('convert a recursive object literal', () {
+ Object? jsObject = js_util.getProperty(js_util.globalThis, 'recObjectData');
+ Object? dartObject = js_util.dartify(jsObject);
+ Map<Object?, Object?> expectedValues = {
+ 'foo': {},
+ };
+ Expect.deepEquals(expectedValues, dartObject);
+ });
+
+ test('throws if object is not an object literal or array', () {
+ expect(
+ () => js_util
+ .dartify(js_util.getProperty(js_util.globalThis, 'throwData')),
+ throwsArgumentError);
+ });
+}
diff --git a/tests/lib/lib_dart2js.status b/tests/lib/lib_dart2js.status
index b1d2d3b..c7caca5 100644
--- a/tests/lib/lib_dart2js.status
+++ b/tests/lib/lib_dart2js.status
@@ -88,6 +88,7 @@
html/js_typed_interop_window_property_test: SkipByDesign
html/js_util_test: SkipByDesign
html/postmessage_structured_test: SkipByDesign
+js/js_util/dartify_test: SkipByDesign
[ $compiler == dart2js && ($runtime == chrome || $runtime == ff) ]
async/slow_consumer2_test: SkipSlow # Times out. Issue 22050
diff --git a/tests/lib_2/js/js_util/dartify_test.dart b/tests/lib_2/js/js_util/dartify_test.dart
new file mode 100644
index 0000000..9fef877
--- /dev/null
+++ b/tests/lib_2/js/js_util/dartify_test.dart
@@ -0,0 +1,88 @@
+// Copyright (c) 2022, the Dart project authors. Please see the AUTHORS file
+// for details. All rights reserved. Use of this source code is governed by a
+// BSD-style license that can be found in the LICENSE file.
+
+// Tests the dartify functionality of the js_util library.
+
+@JS()
+library js_util_jsify_test;
+
+import 'package:js/js.dart';
+import 'package:js/js_util.dart' as js_util;
+import 'package:expect/expect.dart';
+import 'package:expect/minitest.dart';
+
+@JS()
+external void eval(String code);
+
+main() {
+ eval(r"""
+ globalThis.arrayData = [1, 2, false, 4, 'hello', 6, [1, 2], {'foo': 'bar'}];
+ globalThis.recArrayData = [];
+ globalThis.recArrayData = [globalThis.recArrayData];
+ globalThis.objectData = {
+ 'a': 1,
+ 'b': [1, 2, 3],
+ 'c': {
+ 'a': true,
+ 'b': 'foo',
+ },
+ };
+ globalThis.recObjectData = {};
+ globalThis.recObjectData = {'foo': globalThis.recObjectData}
+ globalThis.throwData = function() {};
+ """);
+
+ test('convert an array', () {
+ Object? jsArray = js_util.getProperty(js_util.globalThis, 'arrayData');
+ Object? dartArray = js_util.dartify(jsArray);
+ List<Object?> expectedValues = [
+ 1,
+ 2,
+ false,
+ 4,
+ 'hello',
+ 6,
+ [1, 2],
+ {'foo': 'bar'}
+ ];
+ Expect.deepEquals(expectedValues, dartArray);
+ });
+
+ test('convert a recursive array', () {
+ Object? jsArray = js_util.getProperty(js_util.globalThis, 'recArrayData');
+ Object? dartArray = js_util.dartify(jsArray);
+ List<Object?> expectedValues = [[]];
+ Expect.deepEquals(expectedValues, dartArray);
+ });
+
+ test('convert an object literal', () {
+ Object? jsObject = js_util.getProperty(js_util.globalThis, 'objectData');
+ Object? dartObject = js_util.dartify(jsObject);
+ Map<Object?, Object?> expectedValues = {
+ 'a': 1,
+ 'b': [1, 2, 3],
+ 'c': {
+ 'a': true,
+ 'b': 'foo',
+ },
+ };
+ Expect.deepEquals(expectedValues, dartObject);
+ });
+
+ test('convert a recursive object literal', () {
+ Object? jsObject = js_util.getProperty(js_util.globalThis, 'recObjectData');
+ Object? dartObject = js_util.dartify(jsObject);
+ Map<Object?, Object?> expectedValues = {
+ 'foo': {},
+ };
+ Expect.deepEquals(expectedValues, dartObject);
+ });
+
+ test('throws if object is not an object literal or array', () {
+ expect(
+ () => js_util
+ .dartify(js_util.getProperty(js_util.globalThis, 'throwData')),
+ throwsArgumentError);
+ });
+}
diff --git a/tests/lib_2/lib_2_dart2js.status b/tests/lib_2/lib_2_dart2js.status
index 1f09063..39852b5 100644
--- a/tests/lib_2/lib_2_dart2js.status
+++ b/tests/lib_2/lib_2_dart2js.status
@@ -86,6 +86,7 @@
html/js_typed_interop_window_property_test: SkipByDesign
html/js_util_test: SkipByDesign
html/postmessage_structured_test: SkipByDesign
+js/js_util/dartify_test: SkipByDesign
[ $compiler == dart2js && ($runtime == chrome || $runtime == ff) ]
async/slow_consumer2_test: SkipSlow # Times out. Issue 22050