json-pluck 0.1.0

Pluck a single value out of a serde_json::Value by dotted path or simple JSONPath. Lossy, forgiving, intended for LLM-emitted JSON.
Documentation
//! # json-pluck
//!
//! Pluck a single value out of a `serde_json::Value` by dotted path or
//! a tiny subset of JSONPath. Lossy and forgiving — built for the kind
//! of LLM-emitted JSON where the answer is buried under unpredictable
//! wrappers.
//!
//! Supported path syntax:
//!
//! - `a.b.c` — nested object access
//! - `a.b[0]` — array indexing
//! - `a.b[-1]` — negative array indexing (-1 = last)
//! - `**.answer` — first match anywhere under the root (BFS)
//!
//! ## Example
//!
//! ```
//! use json_pluck::pluck;
//! use serde_json::json;
//!
//! let v = json!({
//!     "result": {
//!         "items": [
//!             { "value": 1 },
//!             { "value": 42 }
//!         ]
//!     }
//! });
//! assert_eq!(pluck(&v, "result.items[1].value"), Some(&json!(42)));
//! assert_eq!(pluck(&v, "result.items[-1].value"), Some(&json!(42)));
//! assert_eq!(pluck(&v, "**.value"), Some(&json!(1)));
//! ```

#![deny(missing_docs)]

use serde_json::Value;

/// Pluck a single value by `path`.
pub fn pluck<'a>(root: &'a Value, path: &str) -> Option<&'a Value> {
    // BFS path: `**.field` — find the first field named `field` anywhere
    // under `root`.
    if let Some(name) = path.strip_prefix("**.") {
        return bfs_first(root, name);
    }
    let mut cur = root;
    for seg in split_path(path) {
        cur = step(cur, &seg)?;
    }
    Some(cur)
}

/// Mutable variant.
pub fn pluck_mut<'a>(root: &'a mut Value, path: &str) -> Option<&'a mut Value> {
    if path.starts_with("**.") {
        // BFS-mut is awkward in safe Rust; do an immutable lookup, then
        // re-descend by the discovered path. For simplicity we only
        // support deterministic paths in the mutable variant.
        return None;
    }
    let mut cur = root;
    for seg in split_path(path) {
        cur = step_mut(cur, &seg)?;
    }
    Some(cur)
}

#[derive(Debug, Clone, PartialEq)]
enum Seg {
    Key(String),
    Index(i64),
}

fn split_path(path: &str) -> Vec<Seg> {
    // Tokenize on `.` and `[N]`. Robust enough for the dotted+bracket
    // subset documented above.
    let mut out = Vec::new();
    let mut buf = String::new();
    let mut chars = path.chars().peekable();
    while let Some(c) = chars.next() {
        match c {
            '.' => {
                if !buf.is_empty() {
                    out.push(Seg::Key(std::mem::take(&mut buf)));
                }
            }
            '[' => {
                if !buf.is_empty() {
                    out.push(Seg::Key(std::mem::take(&mut buf)));
                }
                let mut num = String::new();
                while let Some(&next) = chars.peek() {
                    if next == ']' {
                        chars.next();
                        break;
                    }
                    num.push(next);
                    chars.next();
                }
                if let Ok(n) = num.parse::<i64>() {
                    out.push(Seg::Index(n));
                }
            }
            _ => buf.push(c),
        }
    }
    if !buf.is_empty() {
        out.push(Seg::Key(buf));
    }
    out
}

fn step<'a>(v: &'a Value, seg: &Seg) -> Option<&'a Value> {
    match seg {
        Seg::Key(k) => v.as_object()?.get(k),
        Seg::Index(i) => {
            let arr = v.as_array()?;
            let idx = if *i < 0 {
                (arr.len() as i64 + *i) as usize
            } else {
                *i as usize
            };
            arr.get(idx)
        }
    }
}

fn step_mut<'a>(v: &'a mut Value, seg: &Seg) -> Option<&'a mut Value> {
    match seg {
        Seg::Key(k) => v.as_object_mut()?.get_mut(k),
        Seg::Index(i) => {
            let arr = v.as_array_mut()?;
            let idx = if *i < 0 {
                (arr.len() as i64 + *i) as usize
            } else {
                *i as usize
            };
            arr.get_mut(idx)
        }
    }
}

fn bfs_first<'a>(root: &'a Value, name: &str) -> Option<&'a Value> {
    let mut queue: std::collections::VecDeque<&Value> = std::collections::VecDeque::new();
    queue.push_back(root);
    while let Some(v) = queue.pop_front() {
        match v {
            Value::Object(map) => {
                if let Some(hit) = map.get(name) {
                    return Some(hit);
                }
                for (_, child) in map {
                    queue.push_back(child);
                }
            }
            Value::Array(arr) => {
                for child in arr {
                    queue.push_back(child);
                }
            }
            _ => {}
        }
    }
    None
}