Skip to main content

robinpath_modules/modules/
log_mod.rs

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