Skip to main content

json_eval_rs/utils/
mod.rs

1//! Crate-wide utility helpers.
2//!
3//! Includes cross-platform debug logging, optional timing instrumentation, and
4//! JSON number cleanup helpers used by schema and logic evaluation.
5
6use serde_json::Value;
7use std::cell::RefCell;
8
9#[cfg(feature = "wasm")]
10use wasm_bindgen::prelude::*;
11
12#[cfg(feature = "wasm")]
13#[wasm_bindgen]
14extern "C" {
15    #[wasm_bindgen(js_namespace = console)]
16    pub fn log(s: &str);
17}
18
19/// Cross-platform debug logging macro.
20/// Prints natively to console.log in WASM environments, and falls back to println! elsewhere.
21#[macro_export]
22macro_rules! debug_log {
23    ($($t:tt)*) => {
24        let msg = format!($($t)*);
25        #[cfg(feature = "wasm")]
26        {
27            $crate::utils::log(&format!("[WASM DEBUG] {}", msg));
28        }
29        #[cfg(not(feature = "wasm"))]
30        {
31            println!("[WASM DEBUG] {}", msg);
32        }
33    }
34}
35
36// Timing and Debug infrastructure
37thread_local! {
38    static TIMING_ENABLED: RefCell<bool> = RefCell::new(std::env::var("JSONEVAL_TIMING").is_ok());
39    static DEBUG_CACHE_ENABLED: RefCell<bool> = RefCell::new(std::env::var("JSONEVAL_DEBUG_CACHE").is_ok());
40    static TIMING_DATA: RefCell<Vec<(String, std::time::Duration)>> = RefCell::new(Vec::new());
41}
42
43/// Check if timing is enabled
44#[inline]
45pub fn is_timing_enabled() -> bool {
46    TIMING_ENABLED.with(|enabled| *enabled.borrow())
47}
48
49/// Check if cache debugging is enabled
50#[inline]
51pub fn is_debug_cache_enabled() -> bool {
52    DEBUG_CACHE_ENABLED.with(|enabled| *enabled.borrow())
53}
54
55/// Enable timing programmatically (in addition to JSONEVAL_TIMING environment variable)
56pub fn enable_timing() {
57    TIMING_ENABLED.with(|enabled| {
58        *enabled.borrow_mut() = true;
59    });
60}
61
62pub fn enable_debug_cache() {
63    DEBUG_CACHE_ENABLED.with(|enabled| {
64        *enabled.borrow_mut() = true;
65    });
66}
67
68/// Disable timing
69pub fn disable_timing() {
70    TIMING_ENABLED.with(|enabled| {
71        *enabled.borrow_mut() = false;
72    });
73}
74
75/// Record timing data
76#[inline]
77pub fn record_timing(label: &str, duration: std::time::Duration) {
78    if is_timing_enabled() {
79        TIMING_DATA.with(|data| {
80            data.borrow_mut().push((label.to_string(), duration));
81        });
82    }
83}
84
85/// Print timing summary
86pub fn print_timing_summary() {
87    if !is_timing_enabled() {
88        return;
89    }
90
91    TIMING_DATA.with(|data| {
92        let timings = data.borrow();
93        if timings.is_empty() {
94            return;
95        }
96
97        eprintln!("\nšŸ“Š Timing Summary (JSONEVAL_TIMING enabled)");
98        eprintln!("{}", "=".repeat(60));
99
100        let mut total = std::time::Duration::ZERO;
101        for (label, duration) in timings.iter() {
102            eprintln!("{:40} {:>12?}", label, duration);
103            total += *duration;
104        }
105
106        eprintln!("{}", "=".repeat(60));
107        eprintln!("{:40} {:>12?}", "TOTAL", total);
108        eprintln!();
109    });
110}
111
112/// Clear timing data
113pub fn clear_timing_data() {
114    TIMING_DATA.with(|data| {
115        data.borrow_mut().clear();
116    });
117}
118
119/// Macro for timing a block of code
120#[macro_export]
121macro_rules! time_block {
122    ($label:expr, $block:block) => {{
123        let _start = if $crate::utils::is_timing_enabled() {
124            Some(std::time::Instant::now())
125        } else {
126            None
127        };
128        let result = $block;
129        if let Some(start) = _start {
130            $crate::utils::record_timing($label, start.elapsed());
131        }
132        result
133    }};
134}
135
136/// Clean floating point noise from JSON values
137/// Converts values very close to zero (< 1e-10) to exactly 0
138pub fn clean_float_noise(value: Value) -> Value {
139    const EPSILON: f64 = 1e-10;
140
141    match value {
142        Value::Number(n) => {
143            if let Some(f) = n.as_f64() {
144                if f.abs() < EPSILON {
145                    Value::Number(serde_json::Number::from(0))
146                } else if f.fract().abs() < EPSILON {
147                    Value::Number(serde_json::Number::from(f.round() as i64))
148                } else {
149                    Value::Number(n)
150                }
151            } else {
152                Value::Number(n)
153            }
154        }
155        Value::Array(arr) => Value::Array(arr.into_iter().map(clean_float_noise).collect()),
156        Value::Object(obj) => Value::Object(
157            obj.into_iter()
158                .map(|(k, v)| (k, clean_float_noise(v)))
159                .collect(),
160        ),
161        _ => value,
162    }
163}
164
165#[inline(always)]
166pub fn clean_float_noise_scalar(value: Value) -> Value {
167    const EPSILON: f64 = 1e-10;
168
169    match value {
170        Value::Number(ref n) => {
171            if let Some(f) = n.as_f64() {
172                if f.abs() < EPSILON {
173                    Value::Number(serde_json::Number::from(0))
174                } else if f.fract().abs() < EPSILON {
175                    Value::Number(serde_json::Number::from(f.round() as i64))
176                } else {
177                    value
178                }
179            } else {
180                value
181            }
182        }
183        Value::Array(_) | Value::Object(_) => clean_float_noise(value),
184        _ => value,
185    }
186}