mirror of
https://github.com/fluencelabs/wasm-bindgen
synced 2025-04-24 21:52:13 +00:00
Allow for js property inspection (#1876)
* Add support for #[wasm_bindgen(inspectable)] This annotation generates a `toJSON` and `toString` implementation for generated JavaScript classes which display all readable properties available via the class or its getters This is useful because wasm-bindgen classes currently serialize to display one value named `ptr`, which does not model the properties of the struct in Rust This annotation addresses rustwasm/wasm-bindgen#1857 * Support console.log for inspectable attr in Nodejs `#[wasm_bindgen(inspectable)]` now generates an implementation of `[util.inspect.custom]` for the Node.js target only. This implementation causes `console.log` and friends to yield the same class-style output, but with all readable fields of the Rust struct displayed * Reduce duplication in generated methods Generated `toString` and `[util.inspect.custom]` methods now call `toJSON` to reduce duplication * Store module name in variable
This commit is contained in:
parent
181b10be3f
commit
df34cf843e
@ -223,6 +223,7 @@ pub struct Struct {
|
||||
pub js_name: String,
|
||||
pub fields: Vec<StructField>,
|
||||
pub comments: Vec<String>,
|
||||
pub is_inspectable: bool,
|
||||
}
|
||||
|
||||
#[cfg_attr(feature = "extra-traits", derive(Debug, PartialEq, Eq))]
|
||||
|
@ -306,6 +306,7 @@ fn shared_struct<'a>(s: &'a ast::Struct, intern: &'a Interner) -> Struct<'a> {
|
||||
.map(|s| shared_struct_field(s, intern))
|
||||
.collect(),
|
||||
comments: s.comments.iter().map(|s| &**s).collect(),
|
||||
is_inspectable: s.is_inspectable,
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -57,6 +57,10 @@ pub struct ExportedClass {
|
||||
typescript: String,
|
||||
has_constructor: bool,
|
||||
wrap_needed: bool,
|
||||
/// Whether to generate helper methods for inspecting the class
|
||||
is_inspectable: bool,
|
||||
/// All readable properties of the class
|
||||
readable_properties: Vec<String>,
|
||||
/// Map from field name to type as a string plus whether it has a setter
|
||||
typescript_fields: HashMap<String, (String, bool)>,
|
||||
}
|
||||
@ -644,6 +648,54 @@ impl<'a> Context<'a> {
|
||||
));
|
||||
}
|
||||
|
||||
// If the class is inspectable, generate `toJSON` and `toString`
|
||||
// to expose all readable properties of the class. Otherwise,
|
||||
// the class shows only the "ptr" property when logged or serialized
|
||||
if class.is_inspectable {
|
||||
// Creates a `toJSON` method which returns an object of all readable properties
|
||||
// This object looks like { a: this.a, b: this.b }
|
||||
dst.push_str(&format!(
|
||||
"
|
||||
toJSON() {{
|
||||
return {{{}}};
|
||||
}}
|
||||
|
||||
toString() {{
|
||||
return JSON.stringify(this);
|
||||
}}
|
||||
",
|
||||
class
|
||||
.readable_properties
|
||||
.iter()
|
||||
.fold(String::from("\n"), |fields, field_name| {
|
||||
format!("{}{name}: this.{name},\n", fields, name = field_name)
|
||||
})
|
||||
));
|
||||
|
||||
if self.config.mode.nodejs() {
|
||||
// `util.inspect` must be imported in Node.js to define [inspect.custom]
|
||||
let module_name = self.import_name(&JsImport {
|
||||
name: JsImportName::Module {
|
||||
module: "util".to_string(),
|
||||
name: "inspect".to_string(),
|
||||
},
|
||||
fields: Vec::new(),
|
||||
})?;
|
||||
|
||||
// Node.js supports a custom inspect function to control the
|
||||
// output of `console.log` and friends. The constructor is set
|
||||
// to display the class name as a typical JavaScript class would
|
||||
dst.push_str(&format!(
|
||||
"
|
||||
[{}.custom]() {{
|
||||
return Object.assign(Object.create({{constructor: this.constructor}}), this.toJSON());
|
||||
}}
|
||||
",
|
||||
module_name
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
dst.push_str(&format!(
|
||||
"
|
||||
free() {{
|
||||
@ -2723,6 +2775,7 @@ impl<'a> Context<'a> {
|
||||
fn generate_struct(&mut self, struct_: &AuxStruct) -> Result<(), Error> {
|
||||
let class = require_class(&mut self.exported_classes, &struct_.name);
|
||||
class.comments = format_doc_comments(&struct_.comments, None);
|
||||
class.is_inspectable = struct_.is_inspectable;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@ -2975,6 +3028,7 @@ impl ExportedClass {
|
||||
/// generation is handled specially.
|
||||
fn push_getter(&mut self, docs: &str, field: &str, js: &str, ret_ty: &str) {
|
||||
self.push_accessor(docs, field, js, "get ", ret_ty);
|
||||
self.readable_properties.push(field.to_string());
|
||||
}
|
||||
|
||||
/// Used for adding a setter to a class, mainly to ensure that TypeScript
|
||||
|
@ -256,6 +256,8 @@ pub struct AuxStruct {
|
||||
pub name: String,
|
||||
/// The copied Rust comments to forward to JS
|
||||
pub comments: String,
|
||||
/// Whether to generate helper methods for inspecting the class
|
||||
pub is_inspectable: bool,
|
||||
}
|
||||
|
||||
/// All possible types of imports that can be imported by a wasm module.
|
||||
@ -1238,6 +1240,7 @@ impl<'a> Context<'a> {
|
||||
let aux = AuxStruct {
|
||||
name: struct_.name.to_string(),
|
||||
comments: concatenate_comments(&struct_.comments),
|
||||
is_inspectable: struct_.is_inspectable,
|
||||
};
|
||||
self.aux.structs.push(aux);
|
||||
|
||||
|
@ -45,6 +45,7 @@ macro_rules! attrgen {
|
||||
(readonly, Readonly(Span)),
|
||||
(js_name, JsName(Span, String, Span)),
|
||||
(js_class, JsClass(Span, String, Span)),
|
||||
(inspectable, Inspectable(Span)),
|
||||
(is_type_of, IsTypeOf(Span, syn::Expr)),
|
||||
(extends, Extends(Span, syn::Path)),
|
||||
(vendor_prefix, VendorPrefix(Span, Ident)),
|
||||
@ -322,6 +323,7 @@ impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
|
||||
.js_name()
|
||||
.map(|s| s.0.to_string())
|
||||
.unwrap_or(self.ident.to_string());
|
||||
let is_inspectable = attrs.inspectable().is_some();
|
||||
for (i, field) in self.fields.iter_mut().enumerate() {
|
||||
match field.vis {
|
||||
syn::Visibility::Public(..) => {}
|
||||
@ -361,6 +363,7 @@ impl<'a> ConvertToAst<BindgenAttrs> for &'a mut syn::ItemStruct {
|
||||
js_name,
|
||||
fields,
|
||||
comments,
|
||||
is_inspectable,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -116,6 +116,7 @@ macro_rules! shared_api {
|
||||
name: &'a str,
|
||||
fields: Vec<StructField<'a>>,
|
||||
comments: Vec<&'a str>,
|
||||
is_inspectable: bool,
|
||||
}
|
||||
|
||||
struct StructField<'a> {
|
||||
|
@ -0,0 +1,53 @@
|
||||
# `inspectable`
|
||||
|
||||
By default, structs exported from Rust become JavaScript classes with a single `ptr` property. All other properties are implemented as getters, which are not displayed when calling `toJSON`.
|
||||
|
||||
The `inspectable` attribute can be used on Rust structs to provide a `toJSON` and `toString` implementation that display all readable fields. For example:
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen(inspectable)]
|
||||
pub struct Baz {
|
||||
pub field: i32,
|
||||
private: i32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Baz {
|
||||
#[wasm_bindgen(constructor)]
|
||||
pub fn new(field: i32) -> Baz {
|
||||
Baz { field, private: 13 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Provides the following behavior as in this JavaScript snippet:
|
||||
|
||||
```js
|
||||
const obj = new Baz(3);
|
||||
assert.deepStrictEqual(obj.toJSON(), { field: 3 });
|
||||
obj.field = 4;
|
||||
assert.strictEqual(obj.toString(), '{"field":4}');
|
||||
```
|
||||
|
||||
One or both of these implementations can be overridden as desired. Note that the generated `toString` calls `toJSON` internally, so overriding `toJSON` will affect its output as a side effect.
|
||||
|
||||
```rust
|
||||
#[wasm_bindgen]
|
||||
impl Baz {
|
||||
#[wasm_bindgen(js_name = toJSON)]
|
||||
pub fn to_json(&self) -> i32 {
|
||||
self.field
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toString)]
|
||||
pub fn to_string(&self) -> String {
|
||||
format!("Baz: {}", self.field)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Note that the output of `console.log` will remain unchanged and display only the `ptr` field in browsers. It is recommended to call `toJSON` or `JSON.stringify` in these situations to aid with logging or debugging. Node.js does not suffer from this limitation, see the section below.
|
||||
|
||||
## `inspectable` Classes in Node.js
|
||||
|
||||
When the `nodejs` target is used, an additional `[util.inspect.custom]` implementation is provided which calls `toJSON` internally. This method is used for `console.log` and similar functions to display all readable fields of the Rust struct.
|
@ -170,3 +170,51 @@ exports.js_test_option_classes = () => {
|
||||
assert.ok(c instanceof wasm.OptionClass);
|
||||
wasm.option_class_assert_some(c);
|
||||
};
|
||||
|
||||
/**
|
||||
* Invokes `console.log`, but logs to a string rather than stdout
|
||||
* @param {any} data Data to pass to `console.log`
|
||||
* @returns {string} Output from `console.log`, without color or trailing newlines
|
||||
*/
|
||||
const console_log_to_string = data => {
|
||||
// Store the original stdout.write and create a console that logs without color
|
||||
const original_write = process.stdout.write;
|
||||
const colorless_console = new console.Console({
|
||||
stdout: process.stdout,
|
||||
colorMode: false
|
||||
});
|
||||
let output = '';
|
||||
|
||||
// Change stdout.write to append to our string, then restore the original function
|
||||
process.stdout.write = chunk => output += chunk.trim();
|
||||
colorless_console.log(data);
|
||||
process.stdout.write = original_write;
|
||||
|
||||
return output;
|
||||
};
|
||||
|
||||
exports.js_test_inspectable_classes = () => {
|
||||
const inspectable = wasm.Inspectable.new();
|
||||
const not_inspectable = wasm.NotInspectable.new();
|
||||
// Inspectable classes have a toJSON and toString implementation generated
|
||||
assert.deepStrictEqual(inspectable.toJSON(), { a: inspectable.a });
|
||||
assert.strictEqual(inspectable.toString(), `{"a":${inspectable.a}}`);
|
||||
// Inspectable classes in Node.js have improved console.log formatting as well
|
||||
assert.strictEqual(console_log_to_string(inspectable), `Inspectable { a: ${inspectable.a} }`);
|
||||
// Non-inspectable classes do not have a toJSON or toString generated
|
||||
assert.strictEqual(not_inspectable.toJSON, undefined);
|
||||
assert.strictEqual(not_inspectable.toString(), '[object Object]');
|
||||
// Non-inspectable classes in Node.js have no special console.log formatting
|
||||
assert.strictEqual(console_log_to_string(not_inspectable), `NotInspectable { ptr: ${not_inspectable.ptr} }`);
|
||||
inspectable.free();
|
||||
not_inspectable.free();
|
||||
};
|
||||
|
||||
exports.js_test_inspectable_classes_can_override_generated_methods = () => {
|
||||
const overridden_inspectable = wasm.OverriddenInspectable.new();
|
||||
// Inspectable classes can have the generated toJSON and toString overwritten
|
||||
assert.strictEqual(overridden_inspectable.a, 0);
|
||||
assert.deepStrictEqual(overridden_inspectable.toJSON(), 'JSON was overwritten');
|
||||
assert.strictEqual(overridden_inspectable.toString(), 'string was overwritten');
|
||||
overridden_inspectable.free();
|
||||
};
|
||||
|
@ -30,6 +30,8 @@ extern "C" {
|
||||
fn js_return_none2() -> Option<OptionClass>;
|
||||
fn js_return_some(a: OptionClass) -> Option<OptionClass>;
|
||||
fn js_test_option_classes();
|
||||
fn js_test_inspectable_classes();
|
||||
fn js_test_inspectable_classes_can_override_generated_methods();
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
@ -489,3 +491,65 @@ mod works_in_module {
|
||||
pub fn foo(&self) {}
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inspectable_classes() {
|
||||
js_test_inspectable_classes();
|
||||
}
|
||||
|
||||
#[wasm_bindgen(inspectable)]
|
||||
#[derive(Default)]
|
||||
pub struct Inspectable {
|
||||
pub a: u32,
|
||||
// This private field will not be exposed unless a getter is provided for it
|
||||
#[allow(dead_code)]
|
||||
private: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl Inspectable {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
#[derive(Default)]
|
||||
pub struct NotInspectable {
|
||||
pub a: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl NotInspectable {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[wasm_bindgen_test]
|
||||
fn inspectable_classes_can_override_generated_methods() {
|
||||
js_test_inspectable_classes_can_override_generated_methods();
|
||||
}
|
||||
|
||||
#[wasm_bindgen(inspectable)]
|
||||
#[derive(Default)]
|
||||
pub struct OverriddenInspectable {
|
||||
pub a: u32,
|
||||
}
|
||||
|
||||
#[wasm_bindgen]
|
||||
impl OverriddenInspectable {
|
||||
pub fn new() -> Self {
|
||||
Self::default()
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toJSON)]
|
||||
pub fn to_json(&self) -> String {
|
||||
String::from("JSON was overwritten")
|
||||
}
|
||||
|
||||
#[wasm_bindgen(js_name = toString)]
|
||||
pub fn to_string(&self) -> String {
|
||||
String::from("string was overwritten")
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user