1use std::sync::{Arc, Mutex};
2use std::fmt;
3
4#[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))]
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
7pub enum DebugLevel {
8 Off = 0,
9 Error = 1,
10 Warn = 2,
11 Info = 3,
12 Trace = 4,
13 Verbose = 5,
14}
15
16impl Default for DebugLevel {
17 fn default() -> Self {
18 #[cfg(feature = "debug")]
19 {
20 DebugLevel::Trace
21 }
22 #[cfg(not(feature = "debug"))]
23 {
24 DebugLevel::Off
25 }
26 }
27}
28
29impl fmt::Display for DebugLevel {
30 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
31 match self {
32 Self::Off => write!(f, "off"),
33 Self::Error => write!(f, "error"),
34 Self::Warn => write!(f, "warn"),
35 Self::Info => write!(f, "info"),
36 Self::Trace => write!(f, "trace"),
37 Self::Verbose => write!(f, "verbose"),
38 }
39 }
40}
41
42impl std::str::FromStr for DebugLevel {
43 type Err = String;
44
45 fn from_str(s: &str) -> Result<Self, Self::Err> {
46 match s.to_lowercase().as_str() {
47 "off" | "0" => Ok(DebugLevel::Off),
48 "error" | "1" => Ok(DebugLevel::Error),
49 "warn" | "warning" | "2" => Ok(DebugLevel::Warn),
50 "info" | "3" => Ok(DebugLevel::Info),
51 "trace" | "4" => Ok(DebugLevel::Trace),
52 "verbose" | "5" => Ok(DebugLevel::Verbose),
53 _ => Err(format!("Invalid debug level: {}", s)),
54 }
55 }
56}
57
58#[cfg_attr(feature = "wasm", derive(serde::Serialize, serde::Deserialize))]
60#[derive(Debug, Clone, PartialEq)]
61pub struct DebugEntry {
62 pub level: DebugLevel,
63 pub message: String,
64 pub line: Option<usize>,
65 pub column: Option<usize>,
66}
67
68pub struct DebugContext {
70 level: DebugLevel,
71 logs: Arc<Mutex<Vec<DebugEntry>>>,
72}
73
74impl DebugContext {
75 pub fn new(level: DebugLevel) -> Self {
76 Self {
77 level,
78 logs: Arc::new(Mutex::new(Vec::new())),
79 }
80 }
81
82 pub fn set_level(&mut self, level: DebugLevel) {
83 self.level = level;
84 }
85
86 pub fn log(&self, entry_level: DebugLevel, message: impl Into<String>) {
87 self.log_at(entry_level, message, None, None);
88 }
89
90 pub fn log_at(&self, entry_level: DebugLevel, message: impl Into<String>, line: Option<usize>, column: Option<usize>) {
91 if entry_level <= self.level {
92 let entry = DebugEntry {
93 level: entry_level,
94 message: message.into(),
95 line,
96 column,
97 };
98
99 if let Ok(mut logs) = self.logs.lock() {
100 logs.push(entry);
101 }
102 }
103 }
104
105 pub fn error(&self, msg: impl Into<String>) {
106 self.log(DebugLevel::Error, msg);
107 }
108
109 pub fn warn(&self, msg: impl Into<String>) {
110 self.log(DebugLevel::Warn, msg);
111 }
112
113 pub fn info(&self, msg: impl Into<String>) {
114 self.log(DebugLevel::Info, msg);
115 }
116
117 pub fn trace(&self, msg: impl Into<String>) {
118 self.log(DebugLevel::Trace, msg);
119 }
120
121 pub fn verbose(&self, msg: impl Into<String>) {
122 self.log(DebugLevel::Verbose, msg);
123 }
124
125 pub fn get_logs(&self) -> Vec<DebugEntry> {
126 self.logs.lock()
127 .map(|logs| logs.clone())
128 .unwrap_or_default()
129 }
130
131 pub fn clear_logs(&self) {
132 if let Ok(mut logs) = self.logs.lock() {
133 logs.clear();
134 }
135 }
136
137 pub fn format_logs(&self) -> String {
138 let logs = self.get_logs();
139 logs.iter()
140 .map(|entry| {
141 let location = match (entry.line, entry.column) {
142 (Some(line), Some(col)) => format!(" [{}:{}]", line, col),
143 (Some(line), None) => format!(" [line {}]", line),
144 _ => String::new(),
145 };
146 format!("[{}]{}: {}", entry.level, location, entry.message)
147 })
148 .collect::<Vec<_>>()
149 .join("\n")
150 }
151}
152
153impl Default for DebugContext {
154 fn default() -> Self {
155 Self::new(DebugLevel::default())
156 }
157}
158
159impl Clone for DebugContext {
160 fn clone(&self) -> Self {
161 Self {
162 level: self.level,
163 logs: self.logs.clone(),
164 }
165 }
166}
167
168#[cfg(test)]
169mod tests {
170 use super::*;
171
172 #[test]
173 fn test_debug_level_ordering() {
174 assert!(DebugLevel::Off < DebugLevel::Error);
175 assert!(DebugLevel::Error < DebugLevel::Warn);
176 assert!(DebugLevel::Trace < DebugLevel::Verbose);
177 }
178
179 #[test]
180 fn test_debug_level_from_str() {
181 assert_eq!("trace".parse::<DebugLevel>().unwrap(), DebugLevel::Trace);
182 assert_eq!("off".parse::<DebugLevel>().unwrap(), DebugLevel::Off);
183 assert_eq!("5".parse::<DebugLevel>().unwrap(), DebugLevel::Verbose);
184 }
185
186 #[test]
187 fn test_debug_context_filtering() {
188 let ctx = DebugContext::new(DebugLevel::Warn);
189 ctx.trace("should not be logged");
190 ctx.warn("should be logged");
191 ctx.error("should be logged");
192
193 let logs = ctx.get_logs();
194 assert_eq!(logs.len(), 2);
195 assert_eq!(logs[0].level, DebugLevel::Warn);
196 assert_eq!(logs[1].level, DebugLevel::Error);
197 }
198
199 #[test]
200 fn test_debug_entry_with_position() {
201 let ctx = DebugContext::new(DebugLevel::Trace);
202 ctx.log_at(DebugLevel::Error, "syntax error", Some(42), Some(10));
203
204 let logs = ctx.get_logs();
205 assert_eq!(logs[0].line, Some(42));
206 assert_eq!(logs[0].column, Some(10));
207 }
208}