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}