web_log/
lib.rs

1//! Minimal wrapper over browser console to provide printing facilities
2//!
3//! ## Features:
4//!
5//! - `std` - Enables `std::io::Write` implementation.
6//!
7//! ## Usage
8//!
9//! ```rust,no_run
10//! use web_log::{ConsoleType, Console};
11//!
12//! use core::fmt::Write;
13//!
14//! let mut writer = Console::new(ConsoleType::Info);
15//! let _ = write!(writer, "Hellow World!");
16//! drop(writer); //or writer.flush();
17//!
18//! web_log::println!("Hello via macro!");
19//! web_log::eprintln!("Error via macro!");
20//! ```
21
22#![cfg_attr(not(test), no_std)]
23#![warn(missing_docs)]
24
25#[cfg(feature = "std")]
26extern crate std;
27
28#[cfg(not(test))]
29use wasm_bindgen::prelude::wasm_bindgen;
30
31use core::{cmp, ptr, mem, fmt};
32
33#[cfg(not(test))]
34#[wasm_bindgen]
35extern "C" {
36    #[wasm_bindgen(js_namespace = console)]
37    fn error(s: &str);
38    #[wasm_bindgen(js_namespace = console)]
39    fn warn(s: &str);
40    #[wasm_bindgen(js_namespace = console)]
41    fn info(s: &str);
42    #[wasm_bindgen(js_namespace = console)]
43    fn debug(s: &str);
44}
45
46#[cfg(test)]
47fn error(_: &str) {
48}
49
50#[cfg(test)]
51fn warn(_: &str) {
52}
53
54#[cfg(test)]
55fn info(_: &str) {
56}
57
58#[cfg(test)]
59fn debug(_: &str) {
60}
61
62const BUFFER_CAPACITY: usize = 4096;
63
64#[derive(Debug, PartialEq, Eq, Copy, Clone)]
65///Specifies method of writing into console.
66pub enum ConsoleType {
67    ///Uses `console.error`
68    Error,
69    ///Uses `console.warn`
70    Warn,
71    ///Uses `console.info`
72    Info,
73    ///Uses `console.debug`
74    Debug,
75}
76
77///Wrapper over browser's console
78///
79///On `Drop` performs `flush` or requires manual `flush` for written to be printed in the console.
80///Buffer capacity is 4096 bytes.
81///In case of overflow it dumps existing data to the console and overwrites with rest of it.
82pub struct Console {
83    typ: ConsoleType,
84    buffer: mem::MaybeUninit<[u8; BUFFER_CAPACITY]>,
85    len: usize,
86}
87
88impl Console {
89    ///Creates new instance
90    pub const fn new(typ: ConsoleType) -> Self {
91        Self {
92            typ,
93            buffer: mem::MaybeUninit::uninit(),
94            len: 0,
95        }
96    }
97
98    #[inline(always)]
99    ///Returns content of written buffer.
100    pub fn buffer(&self) -> &[u8] {
101        unsafe {
102            core::slice::from_raw_parts(self.buffer.as_ptr() as *const u8, self.len)
103        }
104    }
105
106    #[inline(always)]
107    fn as_mut_ptr(&mut self) -> *mut u8 {
108        self.buffer.as_mut_ptr() as _
109    }
110
111    #[inline(always)]
112    ///Flushes internal buffer, if any data is available.
113    ///
114    ///Namely it dumps stored data in buffer via Console.
115    ///And resets buffered length to 0.
116    pub fn flush(&mut self) {
117        if self.len > 0 {
118            self.inner_flush();
119        }
120    }
121
122    fn inner_flush(&mut self) {
123        let text = unsafe {
124            core::str::from_utf8_unchecked(self.buffer())
125        };
126        match self.typ {
127            ConsoleType::Error => error(text),
128            ConsoleType::Warn => warn(text),
129            ConsoleType::Info => info(text),
130            ConsoleType::Debug => debug(text),
131        }
132
133        self.len = 0;
134    }
135
136    #[inline]
137    fn copy_data<'a>(&mut self, text: &'a [u8]) -> &'a [u8] {
138        let mut write_len = cmp::min(BUFFER_CAPACITY.saturating_sub(self.len), text.len());
139
140        #[inline(always)]
141        fn is_char_boundary(text: &[u8], idx: usize) -> bool {
142            if idx == 0 {
143                return true;
144            }
145
146            match text.get(idx) {
147                None => idx == text.len(),
148                Some(&byte) => (byte as i8) >= -0x40
149            }
150        }
151
152        #[inline(never)]
153        #[cold]
154        fn shift_by_char_boundary(text: &[u8], mut size: usize) -> usize {
155            while !is_char_boundary(text, size) {
156                size -= 1;
157            }
158            size
159        }
160
161        if !is_char_boundary(text, write_len) {
162            //0 is always char boundary so 0 - 1 is impossible
163            write_len = shift_by_char_boundary(text, write_len - 1);
164        }
165
166        unsafe {
167            ptr::copy_nonoverlapping(text.as_ptr(), self.as_mut_ptr().add(self.len), write_len);
168        }
169        self.len += write_len;
170        &text[write_len..]
171    }
172
173    ///Writes supplied text to the buffer.
174    ///
175    ///On buffer overflow, data is logged via `Console`
176    ///and buffer is filled with the rest of `data`
177    pub fn write_data(&mut self, mut data: &[u8]) {
178        loop {
179            data = self.copy_data(data);
180
181            if data.is_empty() {
182                break;
183            } else {
184                self.flush();
185            }
186        }
187    }
188}
189
190impl fmt::Write for Console {
191    #[inline]
192    fn write_str(&mut self, text: &str) -> fmt::Result {
193        self.write_data(text.as_bytes());
194
195        Ok(())
196    }
197}
198
199#[cfg(feature = "std")]
200impl std::io::Write for Console {
201    fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
202        self.write_data(buf);
203        Ok(buf.len())
204    }
205
206    #[inline(always)]
207    fn flush(&mut self) -> std::io::Result<()> {
208        self.flush();
209        Ok(())
210    }
211}
212
213impl Drop for Console {
214    #[inline]
215    fn drop(&mut self) {
216        self.flush();
217    }
218}
219
220#[macro_export]
221///`println` alternative to write message with INFO priority.
222macro_rules! println {
223    () => {{
224        $crate::println!(" ");
225    }};
226    ($($arg:tt)*) => {{
227        use core::fmt::Write;
228        let mut writer = $crate::Console::new($crate::ConsoleType::Info);
229        let _ = write!(writer, $($arg)*);
230        drop(writer);
231    }}
232}
233
234#[macro_export]
235///`eprintln` alternative to write message with ERROR priority.
236macro_rules! eprintln {
237    () => {{
238        $crate::println!(" ");
239    }};
240    ($($arg:tt)*) => {{
241        use core::fmt::Write;
242        let mut writer = $crate::Console::new($crate::ConsoleType::Error);
243        let _ = write!(writer, $($arg)*);
244        drop(writer);
245    }}
246}
247
248#[cfg(test)]
249mod tests {
250    use super::{Console, ConsoleType};
251    const DATA: &str = "1234567891";
252
253    #[test]
254    fn should_normal_write() {
255        let mut writer = Console::new(ConsoleType::Warn);
256
257        assert_eq!(writer.typ, ConsoleType::Warn);
258
259        let data = DATA.as_bytes();
260
261        writer.write_data(data);
262        assert_eq!(writer.len, data.len());
263        assert_eq!(writer.buffer(), data);
264
265        writer.write_data(b" ");
266        writer.write_data(data);
267        let expected = format!("{} {}", DATA, DATA);
268        assert_eq!(writer.len, expected.len());
269        assert_eq!(writer.buffer(), expected.as_bytes());
270    }
271
272    #[test]
273    fn should_handle_write_overflow() {
274        let mut writer = Console::new(ConsoleType::Warn);
275        let data = DATA.as_bytes();
276
277        //BUFFER_CAPACITY / DATA.len() = 148.xxx
278        for idx in 1..=409 {
279            writer.write_data(data);
280            assert_eq!(writer.len, data.len() * idx);
281        }
282
283        writer.write_data(data);
284        assert_eq!(writer.len, 4);
285        writer.flush();
286        assert_eq!(writer.len, 0);
287    }
288
289    #[test]
290    fn should_handle_write_overflow_outside_of_char_boundary() {
291        let mut writer = Console::new(ConsoleType::Warn);
292        let data = DATA.as_bytes();
293
294        for idx in 1..=409 {
295            writer.write_data(data);
296            assert_eq!(writer.len, data.len() * idx);
297        }
298
299        writer.write_data(b"1234");
300        assert_eq!(4094, writer.len);
301        let unicode = "ロリ";
302        writer.write_data(unicode.as_bytes());
303        assert_eq!(writer.len, unicode.len());
304        assert_eq!(writer.buffer(), unicode.as_bytes());
305    }
306}