mik_sdk/json/
value.rs

1//! JsonValue struct and all its methods.
2
3use super::lazy;
4use miniserde::json::{Array, Number, Object, Value};
5use std::rc::Rc;
6
7/// Internal representation of a JSON value.
8/// Supports both lazy (byte scanning) and parsed (tree) modes.
9#[derive(Clone)]
10#[allow(clippy::redundant_pub_crate)]
11pub(crate) enum JsonInner {
12    /// Lazy mode: stores raw bytes, uses scanning for path_* methods.
13    Lazy { bytes: Rc<[u8]> },
14    /// Parsed mode: fully parsed tree (used for builder APIs and tree traversal).
15    Parsed(Rc<Value>),
16}
17
18/// A JSON value with fluent builder API and lazy parsing.
19///
20/// # Lazy Parsing
21///
22/// When created via `json::try_parse()`, the value starts in lazy mode.
23/// The `path_*` methods scan the raw bytes without building a full tree,
24/// which is **10-40x faster** when you only need a few fields.
25///
26/// Operations that require the full tree (`get()`, `at()`, `keys()`, etc.)
27/// trigger a full parse on first access, which is then cached.
28///
29/// # Thread Safety
30///
31/// `JsonValue` uses `Rc<Value>` internally and is **not** `Send` or `Sync`.
32/// It cannot be shared across threads. This is intentional for WASM targets
33/// where single-threaded execution is the norm and `Rc` provides cheaper
34/// reference counting than `Arc`.
35///
36/// If you need thread-safe JSON values, consider using a different JSON
37/// library like `serde_json` with its thread-safe `Value` type.
38#[derive(Clone)]
39pub struct JsonValue {
40    pub(crate) inner: JsonInner,
41}
42
43impl JsonValue {
44    /// Create a JsonValue from a parsed Value (eager mode).
45    pub(crate) fn new(v: Value) -> Self {
46        Self {
47            inner: JsonInner::Parsed(Rc::new(v)),
48        }
49    }
50
51    /// Create a JsonValue from raw bytes (lazy mode).
52    pub(crate) fn from_bytes(bytes: &[u8]) -> Self {
53        Self {
54            inner: JsonInner::Lazy {
55                bytes: Rc::from(bytes),
56            },
57        }
58    }
59
60    pub(crate) fn null() -> Self {
61        Self::new(Value::Null)
62    }
63
64    /// Get the raw bytes if in lazy mode.
65    pub(crate) fn bytes(&self) -> Option<&[u8]> {
66        match &self.inner {
67            JsonInner::Lazy { bytes } => Some(bytes),
68            JsonInner::Parsed(_) => None,
69        }
70    }
71
72    /// Parse the bytes and return the Value. Used for tree operations.
73    pub(crate) fn parse_bytes(bytes: &[u8]) -> Option<Value> {
74        let s = std::str::from_utf8(bytes).ok()?;
75        miniserde::json::from_str(s).ok()
76    }
77
78    /// Get the Value reference, parsing if needed.
79    /// For methods that need the full tree.
80    pub(crate) fn value(&self) -> &Value {
81        // Static null for returning when parse fails
82        static NULL: Value = Value::Null;
83
84        match &self.inner {
85            JsonInner::Parsed(v) => v,
86            JsonInner::Lazy { .. } => &NULL,
87        }
88    }
89
90    // === Reading (chainable) ===
91
92    /// Get the Value for tree operations, parsing if in lazy mode.
93    fn get_value_for_tree(&self) -> Value {
94        match &self.inner {
95            JsonInner::Parsed(v) => (**v).clone(),
96            JsonInner::Lazy { bytes } => Self::parse_bytes(bytes).unwrap_or(Value::Null),
97        }
98    }
99
100    /// Get object field (returns null if missing or not an object).
101    ///
102    /// Note: This triggers a full parse if in lazy mode. For extracting
103    /// specific fields, prefer `path_str()`, `path_int()`, etc. which use
104    /// lazy scanning.
105    #[must_use]
106    pub fn get(&self, key: &str) -> Self {
107        match self.get_value_for_tree() {
108            Value::Object(obj) => obj.get(key).cloned().map_or_else(Self::null, Self::new),
109            _ => Self::null(),
110        }
111    }
112
113    /// Get array element (returns null if out of bounds or not an array).
114    ///
115    /// Note: This triggers a full parse if in lazy mode and clones the
116    /// underlying Value. For parsing large arrays, use `map_array()` or
117    /// `try_map_array()` instead for better performance.
118    #[must_use]
119    pub fn at(&self, index: usize) -> Self {
120        match self.get_value_for_tree() {
121            Value::Array(arr) => arr.get(index).cloned().map_or_else(Self::null, Self::new),
122            _ => Self::null(),
123        }
124    }
125
126    /// Process array elements without per-element cloning.
127    ///
128    /// This is more efficient than calling `at(i)` in a loop because it
129    /// avoids cloning each element's Value. Returns `None` if not an array.
130    ///
131    /// Note: This triggers a full parse if in lazy mode.
132    ///
133    /// # Example
134    ///
135    /// ```
136    /// # use mik_sdk::json::{self, RawValue};
137    /// let value = json::arr()
138    ///     .push(json::str("hello"))
139    ///     .push(json::str("world"));
140    /// let strings: Option<Vec<String>> = value.map_array(|v| {
141    ///     match v {
142    ///         RawValue::String(s) => Some(s.clone()),
143    ///         _ => None,
144    ///     }
145    /// });
146    /// assert_eq!(strings, Some(vec!["hello".to_string(), "world".to_string()]));
147    /// ```
148    #[must_use]
149    pub fn map_array<T, F>(&self, f: F) -> Option<Vec<T>>
150    where
151        F: Fn(&Value) -> Option<T>,
152    {
153        match self.get_value_for_tree() {
154            Value::Array(arr) => {
155                let mut result = Vec::with_capacity(arr.len());
156                for elem in &arr {
157                    result.push(f(elem)?);
158                }
159                Some(result)
160            },
161            _ => None,
162        }
163    }
164
165    /// Process array elements with error handling, without per-element cloning.
166    ///
167    /// Like `map_array()`, but the function can return errors.
168    /// Returns `None` if not an array, `Some(Err(_))` if parsing fails.
169    ///
170    /// Note: This triggers a full parse if in lazy mode.
171    #[must_use]
172    pub fn try_map_array<T, E, F>(&self, f: F) -> Option<Result<Vec<T>, E>>
173    where
174        F: Fn(&Value) -> Result<T, E>,
175    {
176        match self.get_value_for_tree() {
177            Value::Array(arr) => {
178                let mut result = Vec::with_capacity(arr.len());
179                for elem in &arr {
180                    match f(elem) {
181                        Ok(v) => result.push(v),
182                        Err(e) => return Some(Err(e)),
183                    }
184                }
185                Some(Ok(result))
186            },
187            _ => None,
188        }
189    }
190
191    /// Wrap a raw Value reference in a temporary JsonValue for parsing.
192    ///
193    /// This is useful inside `map_array`/`try_map_array` callbacks when you
194    /// need to use JsonValue methods like `get()` or `str()`.
195    ///
196    /// Note: The returned JsonValue clones the Value, so use sparingly.
197    #[must_use]
198    pub fn from_raw(value: &Value) -> Self {
199        Self::new(value.clone())
200    }
201
202    /// As string, None if not a string.
203    #[must_use]
204    pub fn str(&self) -> Option<String> {
205        match self.get_value_for_tree() {
206            Value::String(s) => Some(s),
207            _ => None,
208        }
209    }
210
211    /// As string, or default if not a string.
212    #[must_use]
213    pub fn str_or(&self, default: &str) -> String {
214        self.str().unwrap_or_else(|| default.to_string())
215    }
216
217    /// As integer, None if not a number.
218    #[must_use]
219    pub fn int(&self) -> Option<i64> {
220        match self.get_value_for_tree() {
221            Value::Number(n) => match n {
222                Number::I64(i) => Some(i),
223                Number::U64(u) => u.try_into().ok(),
224                Number::F64(f) => {
225                    const MAX_SAFE_INT: f64 = 9007199254740992.0; // 2^53
226                    if f.is_finite() && f.abs() <= MAX_SAFE_INT {
227                        Some(f as i64)
228                    } else {
229                        None
230                    }
231                },
232            },
233            _ => None,
234        }
235    }
236
237    /// As integer, or default if not a number.
238    #[must_use]
239    pub fn int_or(&self, default: i64) -> i64 {
240        self.int().unwrap_or(default)
241    }
242
243    /// As float, None if not a number.
244    ///
245    /// # Precision Warning
246    ///
247    /// Converting large integers to f64 may lose precision. Integers with
248    /// absolute value > 2^53 (9,007,199,254,740,992) cannot be represented
249    /// exactly in f64. For large integers, use [`int()`](Self::int) instead.
250    ///
251    /// Non-finite values (NaN, Infinity) return `None`.
252    #[must_use]
253    #[allow(clippy::cast_precision_loss)] // Documented: large i64/u64 may lose precision
254    pub fn float(&self) -> Option<f64> {
255        match self.get_value_for_tree() {
256            Value::Number(n) => match n {
257                Number::F64(f) if f.is_finite() => Some(f),
258                Number::I64(i) => Some(i as f64),
259                Number::U64(u) => Some(u as f64),
260                Number::F64(_) => None, // Non-finite f64
261            },
262            _ => None,
263        }
264    }
265
266    /// As float, or default if not a number.
267    ///
268    /// See [`float()`](Self::float) for precision warnings.
269    #[must_use]
270    pub fn float_or(&self, default: f64) -> f64 {
271        self.float().unwrap_or(default)
272    }
273
274    /// As boolean, None if not a boolean.
275    #[must_use]
276    pub fn bool(&self) -> Option<bool> {
277        match self.get_value_for_tree() {
278            Value::Bool(b) => Some(b),
279            _ => None,
280        }
281    }
282
283    /// As boolean, or default if not a boolean.
284    #[must_use]
285    pub fn bool_or(&self, default: bool) -> bool {
286        self.bool().unwrap_or(default)
287    }
288
289    /// Is this value null?
290    #[must_use]
291    pub fn is_null(&self) -> bool {
292        matches!(self.get_value_for_tree(), Value::Null)
293    }
294
295    /// Get object keys (empty if not an object).
296    ///
297    /// Note: This triggers a full parse if in lazy mode.
298    #[must_use]
299    pub fn keys(&self) -> Vec<String> {
300        match self.get_value_for_tree() {
301            Value::Object(obj) => obj.keys().cloned().collect(),
302            _ => Vec::new(),
303        }
304    }
305
306    /// Get array/object length.
307    ///
308    /// Note: This triggers a full parse if in lazy mode.
309    #[must_use]
310    pub fn len(&self) -> Option<usize> {
311        match self.get_value_for_tree() {
312            Value::Array(arr) => Some(arr.len()),
313            Value::Object(obj) => Some(obj.len()),
314            _ => None,
315        }
316    }
317
318    /// Is this an empty array/object?
319    ///
320    /// Note: This triggers a full parse if in lazy mode.
321    #[must_use]
322    pub fn is_empty(&self) -> bool {
323        self.len().is_some_and(|l| l == 0)
324    }
325
326    // === Path-based accessors (lazy scanning when possible) ===
327
328    /// Navigate to a nested value by path, returning a reference to the raw Value.
329    ///
330    /// This requires a full parse. For lazy scanning, use `path_str`, `path_int`, etc.
331    fn get_path(&self, path: &[&str]) -> Option<&Value> {
332        let mut current = self.value();
333        for key in path {
334            match current {
335                Value::Object(obj) => {
336                    current = obj.get(*key)?;
337                },
338                _ => return None,
339            }
340        }
341        Some(current)
342    }
343
344    /// Get string at path.
345    ///
346    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
347    /// This is **10-40x faster** than full parsing when you only need a few fields.
348    ///
349    /// # Example
350    ///
351    /// ```
352    /// # use mik_sdk::json;
353    /// let body = br#"{"user":{"name":"Alice"}}"#;
354    /// let parsed = json::try_parse(body).unwrap();
355    /// let name = parsed.path_str(&["user", "name"]);  // Lazy scan: ~500ns
356    /// assert_eq!(name, Some("Alice".to_string()));
357    /// ```
358    #[must_use]
359    pub fn path_str(&self, path: &[&str]) -> Option<String> {
360        // Fast path: lazy scanning
361        if let Some(bytes) = self.bytes() {
362            return lazy::path_str(bytes, path);
363        }
364
365        // Fallback: tree traversal
366        match self.get_path(path)? {
367            Value::String(s) => Some(s.clone()),
368            _ => None,
369        }
370    }
371
372    /// Get string at path, or default.
373    #[must_use]
374    pub fn path_str_or(&self, path: &[&str], default: &str) -> String {
375        self.path_str(path).unwrap_or_else(|| default.to_string())
376    }
377
378    /// Get integer at path.
379    ///
380    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
381    #[must_use]
382    pub fn path_int(&self, path: &[&str]) -> Option<i64> {
383        // Fast path: lazy scanning
384        if let Some(bytes) = self.bytes() {
385            return lazy::path_int(bytes, path);
386        }
387
388        // Fallback: tree traversal
389        match self.get_path(path)? {
390            Value::Number(n) => match n {
391                Number::I64(i) => Some(*i),
392                Number::U64(u) => (*u).try_into().ok(),
393                Number::F64(f) => {
394                    const MAX_SAFE_INT: f64 = 9007199254740992.0;
395                    if f.is_finite() && f.abs() <= MAX_SAFE_INT {
396                        Some(*f as i64)
397                    } else {
398                        None
399                    }
400                },
401            },
402            _ => None,
403        }
404    }
405
406    /// Get integer at path, or default.
407    #[must_use]
408    pub fn path_int_or(&self, path: &[&str], default: i64) -> i64 {
409        self.path_int(path).unwrap_or(default)
410    }
411
412    /// Get float at path.
413    ///
414    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
415    #[must_use]
416    #[allow(clippy::cast_precision_loss)] // Documented: large i64/u64 may lose precision
417    pub fn path_float(&self, path: &[&str]) -> Option<f64> {
418        // Fast path: lazy scanning
419        if let Some(bytes) = self.bytes() {
420            return lazy::path_float(bytes, path);
421        }
422
423        // Fallback: tree traversal
424        match self.get_path(path)? {
425            Value::Number(n) => match n {
426                Number::F64(f) if f.is_finite() => Some(*f),
427                Number::I64(i) => Some(*i as f64),
428                Number::U64(u) => Some(*u as f64),
429                Number::F64(_) => None, // Non-finite f64
430            },
431            _ => None,
432        }
433    }
434
435    /// Get float at path, or default.
436    #[must_use]
437    pub fn path_float_or(&self, path: &[&str], default: f64) -> f64 {
438        self.path_float(path).unwrap_or(default)
439    }
440
441    /// Get boolean at path.
442    ///
443    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
444    #[must_use]
445    pub fn path_bool(&self, path: &[&str]) -> Option<bool> {
446        // Fast path: lazy scanning
447        if let Some(bytes) = self.bytes() {
448            return lazy::path_bool(bytes, path);
449        }
450
451        // Fallback: tree traversal
452        match self.get_path(path)? {
453            Value::Bool(b) => Some(*b),
454            _ => None,
455        }
456    }
457
458    /// Get boolean at path, or default.
459    #[must_use]
460    pub fn path_bool_or(&self, path: &[&str], default: bool) -> bool {
461        self.path_bool(path).unwrap_or(default)
462    }
463
464    /// Check if value at path is null.
465    ///
466    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
467    #[must_use]
468    pub fn path_is_null(&self, path: &[&str]) -> bool {
469        // Fast path: lazy scanning
470        if let Some(bytes) = self.bytes() {
471            return lazy::path_is_null(bytes, path);
472        }
473
474        // Fallback: tree traversal
475        matches!(self.get_path(path), Some(Value::Null))
476    }
477
478    /// Check if path exists (even if null).
479    ///
480    /// When in lazy mode, this scans the raw bytes without parsing the full tree.
481    #[must_use]
482    pub fn path_exists(&self, path: &[&str]) -> bool {
483        // Fast path: lazy scanning
484        if let Some(bytes) = self.bytes() {
485            return lazy::path_exists(bytes, path);
486        }
487
488        // Fallback: tree traversal
489        self.get_path(path).is_some()
490    }
491
492    // === Building (fluent) ===
493
494    /// Get mutable access to the parsed value, converting from lazy if needed.
495    fn get_parsed_mut(&mut self) -> &mut Rc<Value> {
496        // First, ensure we're in parsed mode
497        if let JsonInner::Lazy { bytes } = &self.inner {
498            let value = Self::parse_bytes(bytes).unwrap_or(Value::Null);
499            self.inner = JsonInner::Parsed(Rc::new(value));
500        }
501
502        // Now we're guaranteed to be in Parsed mode
503        match &mut self.inner {
504            JsonInner::Parsed(rc) => rc,
505            JsonInner::Lazy { .. } => unreachable!(),
506        }
507    }
508
509    /// Set object field (creates object if needed).
510    ///
511    /// Uses copy-on-write via `Rc::make_mut` - only clones the object if
512    /// there are multiple references. For typical builder patterns like
513    /// `obj().set("a", v1).set("b", v2)`, this is O(1) per set, not O(n).
514    #[must_use]
515    #[allow(clippy::needless_pass_by_value)] // API design: take ownership for builder pattern
516    pub fn set(mut self, key: &str, value: Self) -> Self {
517        let inner_val = value.value().clone();
518        let rc = self.get_parsed_mut();
519        let val_mut = Rc::make_mut(rc);
520
521        if let Value::Object(obj) = val_mut {
522            obj.insert(key.to_string(), inner_val);
523        } else {
524            // Not an object, create new one
525            let mut obj = Object::new();
526            obj.insert(key.to_string(), inner_val);
527            *val_mut = Value::Object(obj);
528        }
529
530        self
531    }
532
533    /// Push to array (creates array if needed).
534    ///
535    /// Uses copy-on-write via `Rc::make_mut` - only clones the array if
536    /// there are multiple references. For typical builder patterns like
537    /// `arr().push(v1).push(v2)`, this is O(1) per push, not O(n).
538    #[must_use]
539    #[allow(clippy::needless_pass_by_value)] // API design: take ownership for builder pattern
540    pub fn push(mut self, value: Self) -> Self {
541        let inner_val = value.value().clone();
542        let rc = self.get_parsed_mut();
543        let val_mut = Rc::make_mut(rc);
544
545        if let Value::Array(arr) = val_mut {
546            arr.push(inner_val);
547        } else {
548            // Not an array, create new one
549            let mut arr = Array::new();
550            arr.push(inner_val);
551            *val_mut = Value::Array(arr);
552        }
553
554        self
555    }
556
557    // === Output ===
558
559    /// Serialize to JSON bytes.
560    #[must_use]
561    pub fn to_bytes(&self) -> Vec<u8> {
562        self.to_string().into_bytes()
563    }
564}
565
566impl std::fmt::Display for JsonValue {
567    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
568        match &self.inner {
569            // Lazy mode: bytes are already valid JSON, write directly
570            JsonInner::Lazy { bytes } => {
571                // Safe: parse() validated UTF-8 before creating Lazy
572                let s = std::str::from_utf8(bytes).unwrap_or("null");
573                f.write_str(s)
574            },
575            // Parsed mode: serialize the value
576            JsonInner::Parsed(v) => {
577                write!(f, "{}", miniserde::json::to_string(&**v))
578            },
579        }
580    }
581}
582
583impl std::fmt::Debug for JsonValue {
584    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
585        std::fmt::Display::fmt(self, f)
586    }
587}