yoke/src/json.rs

116 lines
3.5 KiB
Rust

/// Extract the string value for a given key from a flat JSON line.
/// Looks for `"key": "value"` and returns the value (unescaped basic sequences).
/// Returns `None` if the key is not found or the value is not a string.
pub fn extract_str<'a>(line: &'a str, key: &str) -> Option<&'a str> {
let needle = {
let mut pat = String::with_capacity(key.len() + 3);
pat.push('"');
pat.push_str(key);
pat.push('"');
pat
};
let mut search_from = 0;
loop {
let rel_pos = line[search_from..].find(&needle)?;
let key_start = search_from + rel_pos;
let after_key = key_start + needle.len();
let rest = &line[after_key..];
// Skip whitespace and colon
let rest = rest.trim_start();
let rest = match rest.strip_prefix(':') {
Some(r) => r,
None => {
// This was a value, not a key — advance and retry
search_from = after_key;
continue;
}
};
let rest = rest.trim_start();
// Expect opening quote
let rest = match rest.strip_prefix('"') {
Some(r) => r,
None => {
search_from = after_key;
continue;
}
};
// Find closing quote (handle escaped quotes)
let mut end = 0;
let bytes = rest.as_bytes();
while end < bytes.len() {
if bytes[end] == b'\\' {
end += 2;
} else if bytes[end] == b'"' {
return Some(&rest[..end]);
} else {
end += 1;
}
}
return None;
}
}
/// Extract a numeric value for a given key from a flat JSON line.
/// Looks for `"key": 123.45` and returns the number.
/// Returns `None` if the key is not found or the value is not a number.
pub fn extract_num(line: &str, key: &str) -> Option<f64> {
let needle = {
let mut pat = String::with_capacity(key.len() + 3);
pat.push('"');
pat.push_str(key);
pat.push('"');
pat
};
let key_start = line.find(&needle)?;
let after_key = key_start + needle.len();
let rest = &line[after_key..];
// Skip whitespace and colon
let rest = rest.trim_start();
let rest = rest.strip_prefix(':')?;
let rest = rest.trim_start();
// Collect numeric chars: digits, '.', '-', '+', 'e', 'E'
let num_end = rest
.find(|c: char| !c.is_ascii_digit() && c != '.' && c != '-' && c != '+' && c != 'e' && c != 'E')
.unwrap_or(rest.len());
if num_end == 0 {
return None;
}
rest[..num_end].parse::<f64>().ok()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_extract_str_basic() {
let line = r#"{"type": "assistant", "subtype": "text"}"#;
assert_eq!(extract_str(line, "type"), Some("assistant"));
assert_eq!(extract_str(line, "subtype"), Some("text"));
assert_eq!(extract_str(line, "missing"), None);
}
#[test]
fn test_extract_str_escaped() {
let line = r#"{"path": "foo\"bar"}"#;
assert_eq!(extract_str(line, "path"), Some(r#"foo\"bar"#));
}
#[test]
fn test_extract_num_basic() {
let line = r#"{"cost_usd": 0.42, "num_turns": 5}"#;
assert!((extract_num(line, "cost_usd").unwrap() - 0.42).abs() < 1e-10);
assert!((extract_num(line, "num_turns").unwrap() - 5.0).abs() < 1e-10);
assert_eq!(extract_num(line, "missing"), None);
}
}