use jsonc_parser::ast::{Object, Value as Node};
use jsonc_parser::{CollectOptions, ParseOptions, parse_to_ast};
pub(crate) fn extract_spanned(text: &str, comments: bool) -> Vec<(String, usize)> {
let Ok(result) = parse_to_ast(text, &CollectOptions::default(), &options(comments)) else {
return Vec::new();
};
let Some(root) = result.value else {
return Vec::new();
};
let mut values = Vec::new();
visit_spanned(&root, &mut values);
values
}
fn visit_spanned(node: &Node, values: &mut Vec<(String, usize)>) {
match node {
Node::StringLit(literal) => {
let trimmed = super::text::trim(&literal.value);
if !trimmed.is_empty() {
values.push((trimmed.to_string(), literal.range.start + 1));
}
}
Node::Array(array) => {
for element in &array.elements {
visit_spanned(element, values);
}
}
Node::Object(object) => {
for property in &object.properties {
visit_spanned(&property.value, values);
}
}
_ => {}
}
}
pub(crate) fn extract(text: &str, comments: bool) -> Vec<String> {
let Ok(result) = parse_to_ast(text, &CollectOptions::default(), &options(comments)) else {
return Vec::new();
};
let Some(root) = result.value else {
return Vec::new();
};
let mut values = Vec::new();
visit(&root, &mut values);
values
}
fn options(comments: bool) -> ParseOptions {
ParseOptions {
allow_comments: comments,
allow_loose_object_property_names: false,
allow_trailing_commas: comments,
allow_missing_commas: false,
allow_single_quoted_strings: false,
allow_hexadecimal_numbers: false,
allow_unary_plus_numbers: false,
}
}
fn visit(node: &Node, values: &mut Vec<String>) {
match node {
Node::StringLit(literal) => {
let trimmed = super::text::trim(&literal.value);
if !trimmed.is_empty() {
values.push(trimmed.to_string());
}
}
Node::Array(array) => {
for element in &array.elements {
visit(element, values);
}
}
Node::Object(object) => visit_object(object, values),
_ => {}
}
}
fn visit_object(object: &Object, values: &mut Vec<String>) {
for property in &object.properties {
visit(&property.value, values);
}
}
pub(crate) fn parse_error(text: &str, comments: bool) -> Option<String> {
parse_to_ast(text, &CollectOptions::default(), &options(comments))
.err()
.map(|error| format!("Invalid JSON: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn string_values_are_extracted_and_keys_are_not() {
assert_eq!(extract(r#"{"key":"value"}"#, false), ["value"]);
}
#[test]
fn non_string_primitives_are_dropped() {
assert_eq!(
extract(r#"{"a":42,"b":true,"c":null,"d":"kept"}"#, false),
["kept"]
);
}
#[test]
fn nesting_is_followed_in_document_order() {
let document = r#"{"a":"one","b":{"c":"two","d":["three","four"]}}"#;
assert_eq!(extract(document, false), ["one", "two", "three", "four"]);
}
#[test]
fn many_keys_still_come_back_in_document_order() {
let document = format!(
"{{{}}}",
(0..64)
.map(|n| format!(r#""k{n}":"v{n}""#))
.collect::<Vec<_>>()
.join(",")
);
let expected: Vec<String> = (0..64).map(|n| format!("v{n}")).collect();
assert_eq!(extract(&document, false), expected);
}
#[test]
fn values_are_trimmed_and_empty_ones_dropped() {
assert_eq!(
extract(r#"{"a":" padded ","b":"","c":" "}"#, false),
["padded"]
);
}
#[test]
fn escapes_are_resolved() {
assert_eq!(
extract(r#"{"a":"first\nsecond"}"#, false),
["first\nsecond"]
);
assert_eq!(
extract(r#"{"a":"she said \"hi\""}"#, false),
[r#"she said "hi""#]
);
}
#[test]
fn the_spanned_walk_yields_the_same_values_in_the_same_order() {
let document = r#"{"a":"one","b":{"c":"two","d":["three","four"]}}"#;
let spanned: Vec<String> = extract_spanned(document, false)
.into_iter()
.map(|(value, _)| value)
.collect();
assert_eq!(spanned, extract(document, false));
}
#[test]
fn a_span_starts_inside_the_quotes() {
let document = r#"{"a":"one"}"#;
let (value, offset) = extract_spanned(document, false)[0].clone();
assert_eq!(value, "one");
assert_eq!(&document[offset..offset + value.len()], "one");
}
#[test]
fn a_resolved_escape_still_has_a_span() {
let spanned = extract_spanned(r#"{"a":"first\nsecond"}"#, false);
assert_eq!(spanned[0].0, "first\nsecond");
assert_eq!(spanned[0].1, 6);
}
#[test]
fn a_broken_document_yields_nothing_and_says_why() {
assert!(extract("{not json", false).is_empty());
assert!(
parse_error("{not json", false)
.expect("a message")
.contains("Invalid JSON")
);
assert!(parse_error(r#"{"a":"b"}"#, false).is_none());
}
#[test]
fn comments_and_trailing_commas_are_not_accepted() {
assert!(parse_error(r#"{"a":"b"} // trailing comment"#, false).is_some());
assert!(parse_error(r#"{"a":"b",}"#, false).is_some());
assert!(parse_error(r"{'a':'b'}", false).is_some());
}
#[test]
fn an_empty_object_is_not_an_error() {
assert!(extract("{}", false).is_empty());
assert!(parse_error("{}", false).is_none());
}
}