use super::locate::{KeySpan, join};
#[derive(Debug, Clone, Copy)]
struct Frame {
array: bool,
index: usize,
expect_key: bool,
}
pub(crate) fn key_spans(text: &str) -> Vec<KeySpan> {
let bytes = text.as_bytes();
let mut frames: Vec<Frame> = Vec::new();
let mut path: Vec<String> = Vec::new();
let mut spans = Vec::new();
let mut at = 0;
while at < bytes.len() {
match bytes[at] {
b'{' | b'[' => {
let array = bytes[at] == b'[';
frames.push(Frame {
array,
index: 0,
expect_key: !array,
});
path.push(if array {
"[0]".to_string()
} else {
String::new()
});
at += 1;
}
b'}' | b']' => {
frames.pop();
path.pop();
at += 1;
}
b':' => {
if let Some(frame) = frames.last_mut() {
frame.expect_key = false;
}
at += 1;
}
b',' => {
advance(&mut frames, &mut path);
at += 1;
}
b'"' => {
let (content, end) = read_string(bytes, at);
at = end;
if frames.last().is_some_and(|frame| frame.expect_key) {
rename(&mut path, &text[content]);
continue;
}
spans.push(KeySpan {
start: content.start,
end: content.end,
path: join(path.iter().map(String::as_str)),
});
}
b'/' => at = skip_comment(bytes, at),
byte if is_scalar_byte(byte) => {
let start = at;
while at < bytes.len() && is_scalar_byte(bytes[at]) {
at += 1;
}
spans.push(KeySpan {
start,
end: at,
path: join(path.iter().map(String::as_str)),
});
}
_ => at += 1,
}
}
spans
}
fn advance(frames: &mut [Frame], path: &mut [String]) {
let Some(frame) = frames.last_mut() else {
return;
};
frame.expect_key = !frame.array;
if !frame.array {
return;
}
frame.index += 1;
rename(path, &format!("[{}]", frame.index));
}
fn rename(path: &mut [String], segment: &str) {
let Some(last) = path.last_mut() else {
return;
};
last.clear();
last.push_str(segment);
}
fn read_string(bytes: &[u8], open: usize) -> (std::ops::Range<usize>, usize) {
let mut at = open + 1;
while at < bytes.len() {
if bytes[at] == b'\\' {
at += 2;
continue;
}
if bytes[at] == b'"' {
return (open + 1..at, at + 1);
}
at += 1;
}
(open + 1..bytes.len(), bytes.len())
}
fn is_scalar_byte(byte: u8) -> bool {
byte.is_ascii_alphanumeric() || matches!(byte, b'-' | b'+' | b'.' | b'_')
}
fn skip_comment(bytes: &[u8], at: usize) -> usize {
match bytes.get(at + 1) {
Some(b'/') => bytes[at..]
.iter()
.position(|byte| *byte == b'\n')
.map_or(bytes.len(), |offset| at + offset),
Some(b'*') => bytes
.windows(2)
.skip(at + 2)
.position(|pair| pair == b"*/")
.map_or(bytes.len(), |offset| at + offset + 4),
_ => at + 1,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::locate::key_at;
fn keyed(text: &str, expected: &[(&str, &str)]) {
let spans = key_spans(text);
let found: Vec<(&str, &str)> = spans
.iter()
.map(|span| (&text[span.start..span.end], span.path.as_str()))
.collect::<Vec<_>>();
assert_eq!(found, expected, "{text}");
}
#[test]
fn a_string_value_is_named_by_its_key() {
keyed(r#"{"id":"abc"}"#, &[("abc", "id")]);
}
#[test]
fn nested_objects_build_a_dotted_path() {
keyed(r#"{"a":{"b":{"c":"x"}}}"#, &[("x", "a.b.c")]);
}
#[test]
fn array_elements_are_indexed() {
keyed(
r#"{"ids":["x","y","z"]}"#,
&[("x", "ids.[0]"), ("y", "ids.[1]"), ("z", "ids.[2]")],
);
}
#[test]
fn an_array_of_objects_keeps_both_the_index_and_the_key() {
keyed(
r#"{"rows":[{"id":"x"},{"id":"y"}]}"#,
&[("x", "rows.[0].id"), ("y", "rows.[1].id")],
);
}
#[test]
fn a_key_is_not_a_value_region() {
let text = r#"{"550e8400-e29b-41d4-a716-446655440000":"x"}"#;
let spans = key_spans(text);
assert_eq!(spans.len(), 1);
assert_eq!(&text[spans[0].start..spans[0].end], "x");
assert_eq!(key_at(&spans, 2), None, "the key text has no key path");
}
#[test]
fn a_bare_token_is_a_value_region() {
keyed(
r#"{"id":1536886938009600000,"ok":true,"n":null}"#,
&[("1536886938009600000", "id"), ("true", "ok"), ("null", "n")],
);
}
#[test]
fn an_escaped_quote_does_not_end_a_string() {
keyed(r#"{"a":"x\"y","b":"z"}"#, &[(r#"x\"y"#, "a"), ("z", "b")]);
}
#[test]
fn comments_and_trailing_commas_are_tolerated() {
keyed("{\n// a comment\n\"a\": \"x\",\n}", &[("x", "a")]);
keyed("{/* a comment */\"a\": \"x\"}", &[("x", "a")]);
}
#[test]
fn a_truncated_document_still_yields_what_it_read() {
keyed(r#"{"a":"x","b":"#, &[("x", "a")]);
}
#[test]
fn a_root_scalar_has_no_key_path() {
keyed(r#""x""#, &[("x", "")]);
}
#[test]
fn the_spans_are_ordered_and_do_not_overlap() {
let spans = key_spans(r#"{"a":"x","b":["y","z"]}"#);
for pair in spans.windows(2) {
assert!(pair[0].end <= pair[1].start, "{pair:?}");
}
}
}