use jsonc_parser::ast::{Object, Value as Node};
use jsonc_parser::{CollectOptions, ParseOptions, parse_to_ast};
use super::policy::{Coercion, Value, collect};
fn strict() -> ParseOptions {
ParseOptions {
allow_comments: false,
allow_loose_object_property_names: false,
allow_trailing_commas: false,
allow_missing_commas: false,
allow_single_quoted_strings: false,
allow_hexadecimal_numbers: false,
allow_unary_plus_numbers: false,
}
}
pub(crate) fn extract(text: &str) -> Vec<f64> {
collect(&parsed(text).unwrap_or(Value::Other), Coercion::Typed)
}
pub(crate) fn extract_spanned(text: &str) -> Vec<(f64, usize)> {
let Ok(result) = parse_to_ast(text, &CollectOptions::default(), &strict()) else {
return Vec::new();
};
let Some(root) = result.value else {
return Vec::new();
};
let mut out = Vec::new();
visit_spanned(&root, &mut out);
out
}
fn visit_spanned(node: &Node, out: &mut Vec<(f64, usize)>) {
match node {
Node::NumberLit(literal) => {
if let Ok(value) = literal.value.parse::<f64>()
&& value.is_finite()
{
out.push((value, literal.range.start));
}
}
Node::Array(array) => {
for element in &array.elements {
visit_spanned(element, out);
}
}
Node::Object(object) => {
for property in &object.properties {
visit_spanned(&property.value, out);
}
}
_ => {}
}
}
fn parsed(text: &str) -> Option<Value> {
let result = parse_to_ast(text, &CollectOptions::default(), &strict()).ok()?;
result.value.as_ref().map(convert)
}
fn convert(node: &Node) -> Value {
match node {
Node::NumberLit(literal) => literal
.value
.parse::<f64>()
.map_or(Value::Other, Value::Number),
Node::StringLit(literal) => Value::Text(literal.value.to_string()),
Node::Array(array) => Value::Seq(array.elements.iter().map(convert).collect()),
Node::Object(object) => convert_object(object),
_ => Value::Other,
}
}
fn convert_object(object: &Object) -> Value {
Value::Map(
object
.properties
.iter()
.map(|p| convert(&p.value))
.collect(),
)
}
pub(crate) fn parse_error(text: &str) -> Option<String> {
match parse_to_ast(text, &CollectOptions::default(), &strict()) {
Err(error) => Some(format!("Failed to parse JSON: {error}")),
Ok(result) if result.value.is_none() => {
Some("Failed to parse JSON: unexpected end of input".to_string())
}
Ok(_) => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::extract::render::js_number;
#[test]
fn numbers_are_extracted_and_keys_are_not() {
assert_eq!(extract(r#"{"port":8080}"#), [8080.0]);
}
#[test]
fn a_quoted_number_is_not_a_number() {
assert_eq!(extract(r#"{"a":42,"b":"42"}"#), [42.0]);
}
#[test]
fn nesting_is_followed_in_document_order() {
assert_eq!(
extract(r#"{"a":1,"b":{"c":2,"d":[3,4]}}"#),
[1.0, 2.0, 3.0, 4.0]
);
}
#[test]
fn booleans_and_null_are_not_numbers() {
assert_eq!(extract(r#"{"a":true,"b":null,"c":1}"#), [1.0]);
}
#[test]
fn a_large_integer_keeps_the_double_javascript_would_give_it() {
let extracted = extract(r#"{"a":123456789012345680000}"#);
assert_eq!(js_number(extracted[0]), "123456789012345680000");
}
#[test]
fn a_span_points_at_the_token() {
let document = r#"{"a":8080}"#;
let (value, offset) = extract_spanned(document)[0];
assert_eq!(value, 8080.0);
assert_eq!(&document[offset..offset + 4], "8080");
}
#[test]
fn the_spanned_walk_yields_the_same_numbers_in_the_same_order() {
let document = r#"{"a":1,"b":{"c":2,"d":[3,4]}}"#;
let spanned: Vec<f64> = extract_spanned(document)
.into_iter()
.map(|(v, _)| v)
.collect();
assert_eq!(spanned, extract(document));
}
#[test]
fn a_broken_document_yields_nothing_and_says_why() {
assert!(extract("{not json").is_empty());
assert!(parse_error("{not json").is_some());
assert!(parse_error(r#"{"a":1}"#).is_none());
}
#[test]
fn an_empty_document_is_a_parse_failure() {
assert!(parse_error("").is_some());
assert!(parse_error(" \n ").is_some());
assert!(parse_error("{}").is_none());
}
#[test]
fn the_loosenings_are_off() {
assert!(parse_error(r#"{"a":1,}"#).is_some());
assert!(parse_error(r#"{"a":0x1A}"#).is_some());
}
}