Skip to main content

gobby_code/
savings.rs

1//! Daemon-based savings tracking for gcode.
2//!
3//! Reports token savings to the Gobby daemon via HTTP POST when gcode returns
4//! compact symbol/outline data instead of full file contents.
5//!
6//! Display output follows the gsqz pattern: stderr prefix showing savings.
7
8/// Calculate savings percentage.
9pub fn savings_pct(original_chars: usize, actual_chars: usize) -> f64 {
10    if original_chars == 0 {
11        return 0.0;
12    }
13    (1.0 - actual_chars as f64 / original_chars as f64) * 100.0
14}
15
16/// Report a savings event to the Gobby daemon via HTTP POST.
17///
18/// Best-effort: all errors are silently ignored. The daemon being down
19/// should never break gcode functionality.
20pub fn report_savings(base_url: &str, original_chars: usize, actual_chars: usize) {
21    let url = format!("{}/api/admin/savings/record", base_url);
22    let payload = serde_json::json!({
23        "category": "code_index",
24        "original_chars": original_chars,
25        "actual_chars": actual_chars,
26        "metadata": { "strategy": "outline" }
27    });
28    let _ = ureq::post(&url)
29        .timeout(std::time::Duration::from_secs(1))
30        .send_json(payload);
31}
32
33/// Resolve the daemon URL from config or environment.
34///
35/// Resolution order: config `daemon_url` → `GOBBY_PORT` env → default port 60887
36pub fn resolve_daemon_url(config_url: Option<&str>) -> Option<String> {
37    if let Some(url) = config_url {
38        // Expand ${GOBBY_PORT} if present
39        if url.contains("${GOBBY_PORT}") {
40            if let Ok(port) = std::env::var("GOBBY_PORT") {
41                return Some(url.replace("${GOBBY_PORT}", &port));
42            }
43            // Fall through to defaults if GOBBY_PORT not set
44        } else {
45            return Some(url.to_string());
46        }
47    }
48
49    // Fall back to GOBBY_PORT env var
50    if let Ok(port) = std::env::var("GOBBY_PORT") {
51        return Some(format!("http://localhost:{}", port));
52    }
53
54    // Default to well-known Gobby daemon (matches bootstrap.yaml defaults)
55    Some("http://localhost:60887".to_string())
56}
57
58/// Print savings info to stderr in gsqz-style format.
59pub fn print_savings(label: &str, original_chars: usize, actual_chars: usize) {
60    if actual_chars == 0 || original_chars <= actual_chars {
61        return;
62    }
63    let pct = savings_pct(original_chars, actual_chars);
64    eprintln!("[gcode \u{2014} {label}, saved {pct:.0}% ({actual_chars}B vs {original_chars}B)]");
65}
66
67#[cfg(test)]
68mod tests {
69    use super::*;
70
71    #[test]
72    fn test_savings_pct_basic() {
73        let pct = savings_pct(1000, 200);
74        assert!((pct - 80.0).abs() < 0.01);
75    }
76
77    #[test]
78    fn test_savings_pct_zero_original() {
79        assert_eq!(savings_pct(0, 0), 0.0);
80    }
81
82    #[test]
83    fn test_savings_pct_no_savings() {
84        assert!((savings_pct(100, 100)).abs() < 0.01);
85    }
86
87    #[test]
88    fn test_resolve_daemon_url_config_value() {
89        let url = resolve_daemon_url(Some("http://custom:9999"));
90        assert_eq!(url, Some("http://custom:9999".to_string()));
91    }
92
93    #[test]
94    #[serial_test::serial]
95    fn test_resolve_daemon_url_env_var() {
96        unsafe { std::env::set_var("GOBBY_PORT", "12345") };
97        let url = resolve_daemon_url(None);
98        assert_eq!(url, Some("http://localhost:12345".to_string()));
99        unsafe { std::env::remove_var("GOBBY_PORT") };
100    }
101
102    #[test]
103    #[serial_test::serial]
104    fn test_resolve_daemon_url_default() {
105        unsafe { std::env::remove_var("GOBBY_PORT") };
106        let url = resolve_daemon_url(None);
107        assert_eq!(url, Some("http://localhost:60887".to_string()));
108    }
109
110    #[test]
111    #[serial_test::serial]
112    fn test_resolve_daemon_url_expand_port() {
113        unsafe { std::env::set_var("GOBBY_PORT", "54321") };
114        let url = resolve_daemon_url(Some("http://myhost:${GOBBY_PORT}"));
115        assert_eq!(url, Some("http://myhost:54321".to_string()));
116        unsafe { std::env::remove_var("GOBBY_PORT") };
117    }
118}