reactive_state/middleware/
web_logger.rs

1//! Logging [Middleware](crate::middleware::Middleware) for
2//! applications running in the browser using `wasm-bindgen`.
3//! Publishes actions/events that occur within the
4//! [Store](crate::Store).
5
6use super::{Middleware, ReduceMiddlewareResult};
7use serde::Serialize;
8use std::{fmt::Display, hash::Hash};
9use wasm_bindgen::JsValue;
10use web_sys::console;
11
12pub enum LogLevel {
13    Trace,
14    Debug,
15    Warn,
16    Info,
17    Log,
18}
19
20impl LogLevel {
21    pub fn log_1(&self, message: &JsValue) {
22        #[allow(unused_unsafe)]
23        unsafe {
24            match self {
25                LogLevel::Trace => console::trace_1(message),
26                LogLevel::Debug => console::debug_1(message),
27                LogLevel::Warn => console::warn_1(message),
28                LogLevel::Info => console::info_1(message),
29                LogLevel::Log => console::log_1(message),
30            }
31        }
32    }
33
34    pub fn log(&self, messages: Vec<JsValue>) {
35        let messages_array = js_sys::Array::new_with_length(messages.len() as u32);
36
37        for (i, m) in messages.into_iter().enumerate() {
38            messages_array.set(i as u32, m);
39        }
40
41        #[allow(unused_unsafe)]
42        unsafe {
43            match self {
44                LogLevel::Trace => console::trace(&messages_array),
45                LogLevel::Debug => console::debug(&messages_array),
46                LogLevel::Warn => console::warn(&messages_array),
47                LogLevel::Info => console::info(&messages_array),
48                LogLevel::Log => console::log(&messages_array),
49            }
50        }
51    }
52}
53
54pub enum DisplayType {
55    /// Print using the browser's log groups. Unfortunately this isn't
56    /// always very consistent, especially with
57    /// asynchronous/concurrent events.
58    Groups,
59    /// Print the data in a single javascript object tree.
60    SingleObject,
61}
62
63impl Default for DisplayType {
64    fn default() -> Self {
65        Self::Groups
66    }
67}
68
69#[derive(Serialize)]
70struct OnReduceLog<'a, State, Action, Effect> {
71    action: &'a Option<Action>,
72    prev_state: &'a State,
73    next_state: &'a State,
74    effects: &'a [Effect],
75}
76
77#[derive(Serialize)]
78struct OnNotifyLog<'a, State, Event> {
79    state: &'a State,
80    events: &'a [Event],
81}
82
83impl Default for LogLevel {
84    fn default() -> Self {
85        LogLevel::Log
86    }
87}
88
89/// Logging middleware for applications running in the browser.
90///
91/// See [web_logger](super::web_logger) for more details.
92pub struct WebLoggerMiddleware {
93    log_level: LogLevel,
94    display_type: DisplayType,
95}
96
97impl WebLoggerMiddleware {
98    pub fn new() -> Self {
99        Self {
100            log_level: LogLevel::default(),
101            display_type: DisplayType::default(),
102        }
103    }
104
105    /// Set the level at which the data from this middleware will be
106    /// logged to.
107    pub fn log_level(mut self, log_level: LogLevel) -> Self {
108        self.log_level = log_level;
109        self
110    }
111
112    /// What type of display to use when printing the data from this
113    /// middleware.
114    pub fn display_type(mut self, display_type: DisplayType) -> Self {
115        self.display_type = display_type;
116        self
117    }
118
119    fn on_reduce_groups<State, Action, Event, Effect>(
120        &self,
121        store: &crate::Store<State, Action, Event, Effect>,
122        action: Option<&Action>,
123        reduce: super::ReduceFn<State, Action, Event, Effect>,
124    ) -> ReduceMiddlewareResult<Event, Effect>
125    where
126        State: Serialize,
127        Action: Serialize + Display,
128        Event: Clone + Hash + Eq + Serialize,
129        Effect: Serialize,
130    {
131        let prev_state_js = JsValue::from_serde(&(*store.state())).unwrap();
132
133        let action_js = JsValue::from_serde(&action).unwrap();
134        let action_display = match &action {
135            Some(action) => format!("{}", action),
136            None => "None".to_string(),
137        };
138
139        let result = reduce(store, action);
140        let next_state_js = JsValue::from_serde(&(*store.state())).unwrap();
141
142        let effects_js = JsValue::from_serde(&result.effects).unwrap();
143        let effects_display = match &result.effects.len() {
144            0 => "None".to_string(),
145            _ => format!("({})", result.effects.len()),
146        };
147
148        #[allow(unused_unsafe)]
149        unsafe {
150            console::group_collapsed_3(
151                &JsValue::from_serde(&format!("%caction %c{}", action_display)).unwrap(),
152                &JsValue::from_str("color: gray; font-weight: lighter;"),
153                &JsValue::from_str("inherit"),
154            );
155            console::group_collapsed_2(
156                &JsValue::from_str("%cprev state"),
157                &JsValue::from_str("color: #9E9E9E; font-weight: bold;"),
158            );
159        }
160
161        self.log_level.log_1(&prev_state_js);
162
163        #[allow(unused_unsafe)]
164        unsafe {
165            console::group_end();
166
167            console::group_collapsed_3(
168                &JsValue::from_str(&format!("%caction: %c{}", action_display)),
169                &JsValue::from_str("color: #03A9F4; font-weight: bold;"),
170                &JsValue::from_str("color: gray; font-weight: lighter;"),
171            );
172        }
173
174        self.log_level.log_1(&action_js);
175
176        #[allow(unused_unsafe)]
177        unsafe {
178            console::group_end();
179
180            console::group_collapsed_2(
181                &JsValue::from_str("%cnext state"),
182                &JsValue::from_str("color: #4CAF50; font-weight: bold;"),
183            );
184        }
185
186        self.log_level.log_1(&next_state_js);
187
188        #[allow(unused_unsafe)]
189        unsafe {
190            console::group_end();
191
192            console::group_collapsed_3(
193                &JsValue::from_str(&format!("%ceffects: %c{}", effects_display)),
194                &JsValue::from_str("color: #C210C2; font-weight: bold;"),
195                &JsValue::from_str("color: gray; font-weight: lighter;"),
196            );
197        }
198        self.log_level.log_1(&effects_js);
199
200        #[allow(unused_unsafe)]
201        unsafe {
202            console::group_end();
203        }
204
205        result
206    }
207
208    fn on_reduce_no_groups<State, Action, Event, Effect>(
209        &self,
210        store: &crate::Store<State, Action, Event, Effect>,
211        action: Option<&Action>,
212        reduce: super::ReduceFn<State, Action, Event, Effect>,
213    ) -> ReduceMiddlewareResult<Event, Effect>
214    where
215        State: Serialize,
216        Action: Serialize + Display,
217        Event: Clone + Hash + Eq + Serialize,
218        Effect: Serialize,
219    {
220        let action_display = format!(
221            "on_reduce(), action: {}",
222            match &action {
223                Some(action) => format!("{}", action),
224                None => "None".to_string(),
225            }
226        );
227
228        let action_display_js = JsValue::from_str(&action_display);
229
230        let prev_state = store.state();
231
232        let result = reduce(store, action);
233        let next_state = store.state();
234
235        let log_object = OnReduceLog {
236            action: &action,
237            prev_state: &*prev_state,
238            next_state: &*next_state,
239            effects: &result.effects,
240        };
241
242        let log_object_js = JsValue::from_serde(&log_object).unwrap();
243        self.log_level.log(vec![action_display_js, log_object_js]);
244
245        result
246    }
247
248    fn on_notify_groups<State, Action, Event, Effect>(
249        &self,
250        store: &crate::Store<State, Action, Event, Effect>,
251        events: Vec<Event>,
252        notify: super::NotifyFn<State, Action, Event, Effect>,
253    ) -> Vec<Event>
254    where
255        Event: Serialize,
256    {
257        let events_js = JsValue::from_serde(&events).unwrap();
258        let events_display = match events.len() {
259            0 => "None".to_string(),
260            _ => format!("({})", events.len()),
261        };
262
263        #[allow(unused_unsafe)]
264        unsafe {
265            console::group_collapsed_3(
266                &JsValue::from_str(&format!("%cevents: %c{}", events_display)),
267                &JsValue::from_str("color: #FCBA03; font-weight: bold;"),
268                &JsValue::from_str("color: gray; font-weight: lighter;"),
269            );
270        }
271
272        self.log_level.log_1(&events_js);
273
274        #[allow(unused_unsafe)]
275        unsafe {
276            console::group_end();
277            console::group_end();
278        }
279
280        notify(store, events)
281    }
282
283    fn on_notify_no_groups<State, Action, Event, Effect>(
284        &self,
285        store: &crate::Store<State, Action, Event, Effect>,
286        events: Vec<Event>,
287        notify: super::NotifyFn<State, Action, Event, Effect>,
288    ) -> Vec<Event>
289    where
290        Event: Serialize + Clone + Hash + Eq,
291        State: Serialize,
292    {
293        let log_object = OnNotifyLog {
294            state: &*store.state(),
295            events: &events,
296        };
297
298        let log_object_js = JsValue::from_serde(&log_object).unwrap();
299
300        let display = JsValue::from_str("on_notify(): ");
301
302        self.log_level.log(vec![display, log_object_js]);
303
304        notify(store, events)
305    }
306}
307
308impl Default for WebLoggerMiddleware {
309    fn default() -> Self {
310        WebLoggerMiddleware::new()
311    }
312}
313
314impl<State, Action, Event, Effect> Middleware<State, Action, Event, Effect> for WebLoggerMiddleware
315where
316    State: Serialize,
317    Action: Serialize + Display,
318    Event: Clone + Hash + Eq + Serialize,
319    Effect: Serialize,
320{
321    fn on_reduce(
322        &self,
323        store: &crate::Store<State, Action, Event, Effect>,
324        action: Option<&Action>,
325        reduce: super::ReduceFn<State, Action, Event, Effect>,
326    ) -> ReduceMiddlewareResult<Event, Effect> {
327        match self.display_type {
328            DisplayType::Groups => self.on_reduce_groups(store, action, reduce),
329            DisplayType::SingleObject => self.on_reduce_no_groups(store, action, reduce),
330        }
331    }
332
333    fn process_effect(
334        &self,
335        _store: &crate::Store<State, Action, Event, Effect>,
336        effect: Effect,
337    ) -> Option<Effect> {
338        Some(effect)
339    }
340
341    fn on_notify(
342        &self,
343        store: &crate::Store<State, Action, Event, Effect>,
344        events: Vec<Event>,
345        notify: super::NotifyFn<State, Action, Event, Effect>,
346    ) -> Vec<Event> {
347        match self.display_type {
348            DisplayType::Groups => self.on_notify_groups(store, events, notify),
349            DisplayType::SingleObject => self.on_notify_no_groups(store, events, notify),
350        }
351    }
352}