prettiest/
lib.rs

1//! Pretty printing for Javascript values from [wasm-bindgen](https://docs.rs/wasm-bindgen).
2
3#![forbid(unsafe_code)]
4
5use js_sys::{
6    Array, Date, Error, Function, JsString, Map, Object, Promise, Reflect, RegExp, Set, Symbol,
7    WeakSet,
8};
9use std::{
10    collections::{BTreeMap, BTreeSet, HashSet},
11    fmt::{Debug, Display, Formatter, Result as FmtResult},
12    rc::Rc,
13};
14use wasm_bindgen::{JsCast, JsValue};
15use web_sys::{Document, Element, Window};
16
17pub trait Pretty {
18    fn pretty(&self) -> Prettified;
19}
20
21impl<T> Pretty for T
22where
23    T: AsRef<JsValue>,
24{
25    fn pretty(&self) -> Prettified {
26        Prettified {
27            value: self.as_ref().to_owned(),
28            seen: WeakSet::new(),
29            skip: Default::default(),
30        }
31    }
32}
33
34/// A pretty-printable value from Javascript.
35pub struct Prettified {
36    /// The current value we're visiting.
37    value: JsValue,
38    /// We just use a JS array here to avoid relying on wasm-bindgen's unstable
39    /// ABI.
40    seen: WeakSet,
41    /// Properties we don't want serialized.
42    skip: Rc<HashSet<String>>,
43}
44
45impl Prettified {
46    /// Skip printing the property with `name` if it exists on any object
47    /// visited (transitively).
48    pub fn skip_property(&mut self, name: &str) -> &mut Self {
49        let mut with_name = HashSet::to_owned(&self.skip);
50        with_name.insert(name.to_owned());
51        self.skip = Rc::new(with_name);
52        self
53    }
54
55    fn child(&self, v: &JsValue) -> Self {
56        Self { seen: self.seen.clone(), skip: self.skip.clone(), value: v.as_ref().clone() }
57    }
58
59    // TODO get a serde_json::Value from this too
60}
61
62impl Debug for Prettified {
63    fn fmt(&self, f: &mut Formatter) -> FmtResult {
64        // detect and break cycles before trying to figure out Object subclass
65        // keeps a single path here rather than separately in each branch below
66        let mut _reset = None;
67        if let Some(obj) = self.value.dyn_ref::<Object>() {
68            if self.seen.has(obj) {
69                return write!(f, "[Cycle]");
70            }
71
72            self.seen.add(obj);
73            _reset = Some(scopeguard::guard(obj.to_owned(), |obj| {
74                self.seen.delete(&obj);
75            }));
76        }
77
78        if self.value.is_null() {
79            write!(f, "null")
80        } else if self.value.is_undefined() {
81            write!(f, "undefined")
82        } else if self.value.dyn_ref::<Function>().is_some() {
83            JsFunction.fmt(f)
84        } else if self.value.dyn_ref::<Promise>().is_some() {
85            write!(f, "[Promise]")
86        } else if self.value.dyn_ref::<Document>().is_some() {
87            write!(f, "[Document]")
88        } else if self.value.dyn_ref::<Window>().is_some() {
89            write!(f, "[Window]")
90        } else if let Some(s) = self.value.dyn_ref::<JsString>() {
91            write!(f, "{:?}", s.as_string().unwrap())
92        } else if let Some(n) = self.value.as_f64() {
93            write!(f, "{}", n)
94        } else if let Some(b) = self.value.as_bool() {
95            write!(f, "{:?}", b)
96        } else if let Some(d) = self.value.dyn_ref::<Date>() {
97            write!(f, "{}", d.to_iso_string().as_string().unwrap())
98        } else if let Some(d) = self.value.dyn_ref::<Element>() {
99            let name = d.tag_name().to_ascii_lowercase();
100            let (mut class, mut id) = (d.class_name(), d.id());
101            if !class.is_empty() {
102                class.insert_str(0, " .");
103            }
104            if !id.is_empty() {
105                id.insert_str(0, " #");
106            }
107            write!(f, "<{}{}{}/>", name, id, class)
108        } else if let Some(e) = self.value.dyn_ref::<Error>() {
109            write!(f, "Error: {}", e.to_string().as_string().unwrap())
110        } else if let Some(r) = self.value.dyn_ref::<RegExp>() {
111            write!(f, "/{}/", r.to_string().as_string().unwrap())
112        } else if let Some(s) = self.value.dyn_ref::<Symbol>() {
113            write!(f, "{}", s.to_string().as_string().unwrap())
114        } else if let Some(a) = self.value.dyn_ref::<Array>() {
115            let mut f = f.debug_list();
116            for val in a.iter() {
117                f.entry(&self.child(&val));
118            }
119            f.finish()
120        } else if let Some(s) = self.value.dyn_ref::<Set>() {
121            let mut f = f.debug_set();
122            let entries = s.entries();
123            while let Ok(next) = entries.next() {
124                if next.done() {
125                    break;
126                }
127                f.entry(&self.child(&next.value()));
128            }
129            f.finish()
130        } else if let Some(m) = self.value.dyn_ref::<Map>() {
131            let mut f = f.debug_map();
132            let keys = m.keys();
133            while let Ok(next) = keys.next() {
134                if next.done() {
135                    break;
136                }
137                let key = next.value();
138                let value = m.get(&key);
139
140                f.entry(&self.child(&key), &self.child(&value));
141            }
142
143            f.finish()
144        } else if let Some(obj) = self.value.dyn_ref::<Object>() {
145            let mut proto = obj.clone();
146            let mut props_seen = HashSet::new();
147            let name = obj.constructor().name().as_string().unwrap();
148            let mut f = f.debug_struct(&name);
149
150            loop {
151                let mut functions = BTreeSet::new();
152                let mut props = BTreeMap::new();
153
154                for raw_key in Object::get_own_property_names(&proto).iter() {
155                    let key = raw_key.as_string().expect("object keys are always strings");
156                    if (key.starts_with("__") && key.ends_with("__"))
157                        || props_seen.contains(&key)
158                        || functions.contains(&key)
159                        || self.skip.contains(&key)
160                    {
161                        continue;
162                    }
163
164                    if let Ok(value) = Reflect::get(&obj, &raw_key) {
165                        props_seen.insert(key.clone());
166                        if value.is_function() {
167                            functions.insert(key);
168                        } else {
169                            props.insert(key, self.child(&value));
170                        }
171                    }
172                }
173
174                for (key, value) in props {
175                    f.field(&key, &value);
176                }
177
178                for key in functions {
179                    f.field(&key, &JsFunction);
180                }
181
182                proto = Object::get_prototype_of(proto.as_ref());
183                if proto.is_falsy() || proto.constructor().name().as_string().unwrap() == "Object" {
184                    // we've reached the end of the prototype chain
185                    break;
186                }
187            }
188
189            f.finish()
190        } else {
191            write!(f, "unknown ({:?})", &self.value)
192        }
193    }
194}
195
196impl Display for Prettified {
197    fn fmt(&self, f: &mut Formatter) -> FmtResult {
198        write!(f, "{:#?}", self)
199    }
200}
201
202struct JsFunction;
203impl Debug for JsFunction {
204    fn fmt(&self, f: &mut Formatter) -> FmtResult {
205        write!(f, "[Function]")
206    }
207}
208
209#[cfg(test)]
210mod tests {
211    use super::*;
212    use futures::channel::oneshot::channel;
213    use wasm_bindgen::closure::Closure;
214    use wasm_bindgen_test::{wasm_bindgen_test, wasm_bindgen_test_configure};
215    use web_sys::{Event, EventTarget};
216
217    wasm_bindgen_test_configure!(run_in_browser);
218
219    #[wasm_bindgen_test]
220    fn cycle_is_broken() {
221        let with_cycles = js_sys::Function::new_no_args(
222            r#"
223            let root = { child: { nested: [] } };
224            root.child.nested.push(root);
225            return root;
226        "#,
227        )
228        .call0(&JsValue::null())
229        .unwrap();
230
231        assert_eq!(
232            with_cycles.pretty().to_string(),
233            r#"Object {
234    child: Object {
235        nested: [
236            [Cycle],
237        ],
238    },
239}"#
240        );
241    }
242
243    #[wasm_bindgen_test]
244    fn repeated_siblings_are_not_cycles() {
245        let with_siblings = js_sys::Function::new_no_args(
246            r#"
247            let root = { child: { nested: [] } };
248            let repeated_child = { foo: "bar" };
249            root.child.nested.push(repeated_child);
250            root.child.nested.push(repeated_child);
251            return root;
252        "#,
253        )
254        .call0(&JsValue::null())
255        .unwrap();
256
257        assert_eq!(
258            with_siblings.pretty().to_string(),
259            r#"Object {
260    child: Object {
261        nested: [
262            Object {
263                foo: "bar",
264            },
265            Object {
266                foo: "bar",
267            },
268        ],
269    },
270}"#
271        );
272    }
273
274    #[wasm_bindgen_test]
275    async fn live_keyboard_event() {
276        // create an input element and bind it to the document
277        let window = web_sys::window().unwrap();
278        let document = window.document().unwrap();
279        let input = document.create_element("input").unwrap();
280        // input.set_attribute("type", "text").unwrap();
281        document.body().unwrap().append_child(input.as_ref()).unwrap();
282
283        // create & add an event listener that will send the event back the test
284        let (send, recv) = channel();
285        let callback = Closure::once_into_js(move |ev: Event| {
286            send.send(ev).unwrap();
287        });
288        let target: &EventTarget = input.as_ref();
289        let event_type = "keydown";
290        target.add_event_listener_with_callback(event_type, callback.dyn_ref().unwrap()).unwrap();
291
292        // create & dispatch an event to the input element
293        let sent_event = web_sys::KeyboardEvent::new_with_keyboard_event_init_dict(
294            event_type,
295            web_sys::KeyboardEventInit::new()
296                .char_code(b'F' as u32)
297                .bubbles(true)
298                .cancelable(true)
299                .view(Some(&window)),
300        )
301        .unwrap();
302        let sent: &Event = sent_event.as_ref();
303        assert!(target.dispatch_event(sent).unwrap());
304
305        // wait for the event to come back
306        let received_event: Event = recv.await.unwrap();
307        // make sure we can print it without exploding due to nesting
308        assert_eq!(
309            received_event.pretty().skip_property("timeStamp").to_string(),
310            r#"KeyboardEvent {
311    isTrusted: false,
312    DOM_KEY_LOCATION_LEFT: 1,
313    DOM_KEY_LOCATION_NUMPAD: 3,
314    DOM_KEY_LOCATION_RIGHT: 2,
315    DOM_KEY_LOCATION_STANDARD: 0,
316    altKey: false,
317    charCode: 70,
318    code: "",
319    ctrlKey: false,
320    isComposing: false,
321    key: "",
322    keyCode: 0,
323    location: 0,
324    metaKey: false,
325    repeat: false,
326    shiftKey: false,
327    constructor: [Function],
328    getModifierState: [Function],
329    initKeyboardEvent: [Function],
330    detail: 0,
331    sourceCapabilities: null,
332    view: [Window],
333    which: 0,
334    initUIEvent: [Function],
335    AT_TARGET: 2,
336    BUBBLING_PHASE: 3,
337    CAPTURING_PHASE: 1,
338    NONE: 0,
339    bubbles: true,
340    cancelBubble: false,
341    cancelable: true,
342    composed: false,
343    currentTarget: null,
344    defaultPrevented: false,
345    eventPhase: 0,
346    path: [
347        <input/>,
348        <body/>,
349        <html/>,
350        [Document],
351        [Window],
352    ],
353    returnValue: true,
354    srcElement: <input/>,
355    target: <input/>,
356    type: "keydown",
357    composedPath: [Function],
358    initEvent: [Function],
359    preventDefault: [Function],
360    stopImmediatePropagation: [Function],
361    stopPropagation: [Function],
362}"#,
363        );
364    }
365}