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
62/// Disable timing
63pub fn disable_timing() {
64    TIMING_ENABLED.with(|enabled| {
65        *enabled.borrow_mut() = false;
66    });
67}
68
69/// Record timing data
70#[inline]
71pub fn record_timing(label: &str, duration: std::time::Duration) {
72    if is_timing_enabled() {
73        TIMING_DATA.with(|data| {
74            data.borrow_mut().push((label.to_string(), duration));
75        });
76    }
77}
78
79/// Print timing summary
80pub fn print_timing_summary() {
81    if !is_timing_enabled() {
82        return;
83    }
84
85    TIMING_DATA.with(|data| {
86        let timings = data.borrow();
87        if timings.is_empty() {
88            return;
89        }
90
91        eprintln!("\nšŸ“Š Timing Summary (JSONEVAL_TIMING enabled)");
92        eprintln!("{}", "=".repeat(60));
93
94        let mut total = std::time::Duration::ZERO;
95        for (label, duration) in timings.iter() {
96            eprintln!("{:40} {:>12?}", label, duration);
97            total += *duration;
98        }
99
100        eprintln!("{}", "=".repeat(60));
101        eprintln!("{:40} {:>12?}", "TOTAL", total);
102        eprintln!();
103    });
104}
105
106/// Clear timing data
107pub fn clear_timing_data() {
108    TIMING_DATA.with(|data| {
109        data.borrow_mut().clear();
110    });
111}
112
113/// Macro for timing a block of code
114#[macro_export]
115macro_rules! time_block {
116    ($label:expr, $block:block) => {{
117        let _start = if $crate::utils::is_timing_enabled() {
118            Some(std::time::Instant::now())
119        } else {
120            None
121        };
122        let result = $block;
123        if let Some(start) = _start {
124            $crate::utils::record_timing($label, start.elapsed());
125        }
126        result
127    }};
128}
129
130/// Clean floating point noise from JSON values
131/// Converts values very close to zero (< 1e-10) to exactly 0
132pub fn clean_float_noise(value: Value) -> Value {
133    const EPSILON: f64 = 1e-10;
134
135    match value {
136        Value::Number(n) => {
137            if let Some(f) = n.as_f64() {
138                if f.abs() < EPSILON {
139                    Value::Number(serde_json::Number::from(0))
140                } else if f.fract().abs() < EPSILON {
141                    Value::Number(serde_json::Number::from(f.round() as i64))
142                } else {
143                    Value::Number(n)
144                }
145            } else {
146                Value::Number(n)
147            }
148        }
149        Value::Array(arr) => Value::Array(arr.into_iter().map(clean_float_noise).collect()),
150        Value::Object(obj) => Value::Object(
151            obj.into_iter()
152                .map(|(k, v)| (k, clean_float_noise(v)))
153                .collect(),
154        ),
155        _ => value,
156    }
157}
158
159#[inline(always)]
160pub fn clean_float_noise_scalar(value: Value) -> Value {
161    const EPSILON: f64 = 1e-10;
162
163    match value {
164        Value::Number(ref n) => {
165            if let Some(f) = n.as_f64() {
166                if f.abs() < EPSILON {
167                    Value::Number(serde_json::Number::from(0))
168                } else if f.fract().abs() < EPSILON {
169                    Value::Number(serde_json::Number::from(f.round() as i64))
170                } else {
171                    value
172                }
173            } else {
174                value
175            }
176        }
177        Value::Array(_) | Value::Object(_) => clean_float_noise(value),
178        _ => value,
179    }
180}