-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Allow for js property inspection #1876
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 2 commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -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,59 @@ 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 JavaScript object of all readable properties | ||
// This looks like { a: this.a, b: this.b } | ||
let readable_properties_js_object = format!( | ||
"{{{}}}", | ||
class | ||
.readable_properties | ||
.iter() | ||
.fold(String::from("\n"), |fields, field_name| { | ||
format!("{}{name}: this.{name},\n", fields, name = field_name) | ||
}) | ||
); | ||
|
||
dst.push_str(&format!( | ||
" | ||
toJSON() {{ | ||
return {readable_properties}; | ||
}} | ||
|
||
toString() {{ | ||
return JSON.stringify({readable_properties}); | ||
}} | ||
", | ||
readable_properties = readable_properties_js_object | ||
)); | ||
|
||
if self.config.mode.nodejs() { | ||
// `util.inspect` must be imported in Node.js to define [inspect.custom] | ||
self.import_name(&JsImport { | ||
name: JsImportName::Module { | ||
module: "util".to_string(), | ||
name: "inspect".to_string(), | ||
}, | ||
fields: Vec::new(), | ||
})?; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The return value of this is a There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I'm not sure if I can, this import gives me "inspect" back but the symbol name has to be "inspect.custom"… Do you know if there's another method I should try to get the symbol name in a variable? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh sorry what I mean is that below instead of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense! I've updated it |
||
|
||
// 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!(" | ||
[inspect.custom]() {{ | ||
return Object.assign(Object.create({{constructor: this.constructor}}), {readable_properties}); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Could this use |
||
}} | ||
", | ||
readable_properties = readable_properties_js_object | ||
)); | ||
} | ||
} | ||
|
||
dst.push_str(&format!( | ||
" | ||
free() {{ | ||
|
@@ -2723,6 +2780,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 +3033,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 | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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: | ||
|
||
```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. This method is used for `console.log` and similar functions to display all readable fields of the Rust struct. |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could this perhaps be
JSON.stringify(this.toJSON())
to avoid duplication?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I implemented it that way originally, but I thought it may be confusing if a struct providing its own
toJSON
had the side effect of changingtoString
and[util.custom.inspect]
. If it’s documented then it shouldn’t be an issue though. I’ll take your advice and mention it in the guide!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah sounds good to me!