Skip to main content

robinpath_modules/modules/
log_mod.rs

1use robinpath::{RobinPath, Value};
2use std::sync::{Arc, Mutex};
3
4struct LogConfig {
5    level: u8,
6    file_path: Option<String>,
7    format: String,
8    indent: usize,
9    timers: std::collections::HashMap<String, u64>,
10}
11
12impl LogConfig {
13    fn new() -> Self {
14        Self {
15            level: 0, // 0=debug, 1=info, 2=warn, 3=error, 4=fatal
16            file_path: None,
17            format: "text".to_string(),
18            indent: 0,
19            timers: std::collections::HashMap::new(),
20        }
21    }
22}
23
24fn now_ms() -> u64 {
25    std::time::SystemTime::now()
26        .duration_since(std::time::UNIX_EPOCH)
27        .unwrap_or_default()
28        .as_millis() as u64
29}
30
31fn level_num(s: &str) -> u8 {
32    match s.to_lowercase().as_str() {
33        "debug" => 0, "info" => 1, "warn" | "warning" => 2, "error" => 3, "fatal" => 4, _ => 0,
34    }
35}
36
37fn level_name(n: u8) -> &'static str {
38    match n { 0 => "DEBUG", 1 => "INFO", 2 => "WARN", 3 => "ERROR", 4 => "FATAL", _ => "DEBUG" }
39}
40
41fn format_log_line(level: u8, messages: &[Value], config: &LogConfig) -> String {
42    let parts: Vec<String> = messages.iter().map(|v| v.to_display_string()).collect();
43    let msg = parts.join(" ");
44    let indent = " ".repeat(config.indent * 2);
45    if config.format == "json" {
46        let now = now_ms();
47        format!("{{\"level\":\"{}\",\"message\":\"{}\",\"timestamp\":{}}}", level_name(level), msg.replace('"', "\\\""), now)
48    } else {
49        let now = chrono_like_now();
50        format!("{} [{}] {}{}", now, level_name(level), indent, msg)
51    }
52}
53
54fn chrono_like_now() -> String {
55    let ts = now_ms() / 1000;
56    let days = (ts as i64).div_euclid(86400);
57    let time_of_day = (ts as i64).rem_euclid(86400);
58    let hour = (time_of_day / 3600) as u32;
59    let min = ((time_of_day % 3600) / 60) as u32;
60    let sec = (time_of_day % 60) as u32;
61    let z = days + 719468;
62    let era = z.div_euclid(146097);
63    let doe = z.rem_euclid(146097);
64    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
65    let y = yoe + era * 400;
66    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
67    let mp = (5 * doy + 2) / 153;
68    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
69    let m = if mp < 10 { mp + 3 } else { mp - 9 } as u32;
70    let y = if m <= 2 { y + 1 } else { y };
71    format!("{:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z", y, m, d, hour, min, sec)
72}
73
74fn do_log(level: u8, messages: &[Value], state: &Mutex<LogConfig>) -> Result<Value, String> {
75    let config = state.lock().unwrap();
76    if level < config.level {
77        return Ok(Value::Null);
78    }
79    let line = format_log_line(level, messages, &config);
80    // Write to file if configured
81    if let Some(ref path) = config.file_path {
82        use std::io::Write;
83        if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(path) {
84            let _ = writeln!(f, "{}", line);
85        }
86    }
87    Ok(Value::String(line))
88}
89
90pub fn register(rp: &mut RobinPath) {
91    let state = Arc::new(Mutex::new(LogConfig::new()));
92
93    let s = state.clone();
94    rp.register_builtin("log.debug", move |args, _| do_log(0, args, &s));
95    let s = state.clone();
96    rp.register_builtin("log.info", move |args, _| do_log(1, args, &s));
97    let s = state.clone();
98    rp.register_builtin("log.warn", move |args, _| do_log(2, args, &s));
99    let s = state.clone();
100    rp.register_builtin("log.error", move |args, _| do_log(3, args, &s));
101    let s = state.clone();
102    rp.register_builtin("log.fatal", move |args, _| do_log(4, args, &s));
103
104    let s = state.clone();
105    rp.register_builtin("log.setLevel", move |args, _| {
106        let level = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "debug".to_string());
107        s.lock().unwrap().level = level_num(&level);
108        Ok(Value::Bool(true))
109    });
110
111    let s = state.clone();
112    rp.register_builtin("log.getLevel", move |_args, _| {
113        let config = s.lock().unwrap();
114        Ok(Value::String(level_name(config.level).to_lowercase()))
115    });
116
117    let s = state.clone();
118    rp.register_builtin("log.setFile", move |args, _| {
119        let path = args.first().map(|v| v.to_display_string());
120        s.lock().unwrap().file_path = path;
121        Ok(Value::Bool(true))
122    });
123
124    let s = state.clone();
125    rp.register_builtin("log.setFormat", move |args, _| {
126        let fmt = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "text".to_string());
127        s.lock().unwrap().format = fmt;
128        Ok(Value::Bool(true))
129    });
130
131    let s = state.clone();
132    rp.register_builtin("log.clear", move |_args, _| {
133        let mut config = s.lock().unwrap();
134        config.level = 0;
135        config.file_path = None;
136        config.format = "text".to_string();
137        config.indent = 0;
138        config.timers.clear();
139        Ok(Value::Bool(true))
140    });
141
142    let s = state.clone();
143    rp.register_builtin("log.group", move |args, _| {
144        let label = args.first().map(|v| v.to_display_string()).unwrap_or_default();
145        let mut config = s.lock().unwrap();
146        let indent = " ".repeat(config.indent * 2);
147        config.indent += 1;
148        Ok(Value::String(format!("{}--- {} ---", indent, label)))
149    });
150
151    let s = state.clone();
152    rp.register_builtin("log.groupEnd", move |_args, _| {
153        let mut config = s.lock().unwrap();
154        if config.indent > 0 { config.indent -= 1; }
155        Ok(Value::Bool(true))
156    });
157
158    let s = state.clone();
159    rp.register_builtin("log.time", move |args, _| {
160        let label = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
161        s.lock().unwrap().timers.insert(label, now_ms());
162        Ok(Value::Bool(true))
163    });
164
165    let s = state.clone();
166    rp.register_builtin("log.timeEnd", move |args, _| {
167        let label = args.first().map(|v| v.to_display_string()).unwrap_or_else(|| "default".to_string());
168        let mut config = s.lock().unwrap();
169        if let Some(start) = config.timers.remove(&label) {
170            let elapsed = now_ms() - start;
171            Ok(Value::String(format!("{}: {}ms", label, elapsed)))
172        } else {
173            Err(format!("Timer \"{}\" not found", label))
174        }
175    });
176
177    rp.register_builtin("log.table", |args, _| {
178        let data = args.first().cloned().unwrap_or(Value::Null);
179        if let Value::Array(arr) = &data {
180            if arr.is_empty() {
181                return Ok(Value::String("(empty)".to_string()));
182            }
183            // Collect column names from first object
184            let columns: Vec<String> = if let Some(Value::Object(first)) = arr.first() {
185                first.keys().cloned().collect()
186            } else {
187                vec!["value".to_string()]
188            };
189            let mut rows: Vec<Vec<String>> = vec![columns.clone()];
190            for item in arr {
191                if let Value::Object(obj) = item {
192                    rows.push(columns.iter().map(|c| obj.get(c).map(|v| v.to_display_string()).unwrap_or_default()).collect());
193                } else {
194                    rows.push(vec![item.to_display_string()]);
195                }
196            }
197            // Calculate column widths
198            let widths: Vec<usize> = (0..columns.len()).map(|i| {
199                rows.iter().map(|r| r.get(i).map(|s| s.len()).unwrap_or(0)).max().unwrap_or(0)
200            }).collect();
201            let mut lines = Vec::new();
202            for (idx, row) in rows.iter().enumerate() {
203                let cells: Vec<String> = row.iter().enumerate()
204                    .map(|(i, s)| format!("{:width$}", s, width = widths.get(i).copied().unwrap_or(0)))
205                    .collect();
206                lines.push(format!("| {} |", cells.join(" | ")));
207                if idx == 0 {
208                    let sep: Vec<String> = widths.iter().map(|&w| "-".repeat(w)).collect();
209                    lines.push(format!("|-{}-|", sep.join("-|-")));
210                }
211            }
212            Ok(Value::String(lines.join("\n")))
213        } else {
214            Ok(Value::String(data.to_display_string()))
215        }
216    });
217}