use super::*;
use crate::autocomplete::json_navigator::DEFAULT_ARRAY_SAMPLE_SIZE;
use serde_json::Value;
use std::sync::Arc;
fn parse_json(json_str: &str) -> Arc<Value> {
if let Ok(value) = serde_json::from_str(json_str) {
return Arc::new(value);
}
let mut accumulated = String::new();
for line in json_str.lines() {
if line.trim().is_empty() {
continue;
}
accumulated.push_str(line);
accumulated.push('\n');
if let Ok(value) = serde_json::from_str::<Value>(&accumulated) {
return Arc::new(value);
}
}
Arc::new(Value::Null)
}
#[test]
fn test_analyze_simple_object() {
let result = r#"{"name": "test", "age": 30, "active": true}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true, false, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 3);
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(suggestions.iter().any(|s| s.text == ".age"));
assert!(suggestions.iter().any(|s| s.text == ".active"));
}
#[test]
fn test_analyze_nested_object() {
let result = r#"{"user": {"name": "Alice", "profile": {"city": "NYC"}}}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert!(suggestions.iter().any(|s| s.text == ".user"));
}
#[test]
fn test_analyze_array_of_objects_after_operator() {
let result = r#"[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text == ".[].id"));
assert!(suggestions.iter().any(|s| s.text == ".[].name"));
}
#[test]
fn test_analyze_array_of_objects_after_continuation() {
let result = r#"[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == "[]"));
assert!(suggestions.iter().any(|s| s.text == "[].id"));
assert!(suggestions.iter().any(|s| s.text == "[].name"));
}
#[test]
fn test_analyze_empty_array() {
let result = "[]";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Array,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_analyze_empty_object() {
let result = "{}";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_analyze_pretty_printed_object() {
let result = r#"{
"name": "Alice",
"age": 30
}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(suggestions.iter().any(|s| s.text == ".age"));
}
#[test]
fn test_multivalue_destructured_objects_after_bracket() {
let result = r#"{"name": "Alice", "age": 30}
{"name": "Bob", "age": 25}
{"name": "Charlie", "age": 35}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == "name"));
assert!(suggestions.iter().any(|s| s.text == "age"));
}
#[test]
fn test_multivalue_destructured_objects_after_operator() {
let result = r#"{"clusterArn": "arn1", "name": "svc1"}
{"clusterArn": "arn2", "name": "svc2"}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == ".clusterArn"));
assert!(suggestions.iter().any(|s| s.text == ".name"));
}
#[test]
fn test_multivalue_pretty_printed_destructured_after_bracket() {
let result = r#"{
"clusterArn": "arn1",
"name": "svc1"
}
{
"clusterArn": "arn2",
"name": "svc2"
}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == "clusterArn"));
assert!(suggestions.iter().any(|s| s.text == "name"));
}
#[test]
fn test_multivalue_mixed_types() {
let result = r#"42
"hello"
{"field": "value"}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Number,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_multivalue_with_whitespace() {
let result = r#"
{"key1": "val1"}
{"key2": "val2"}
"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".key1");
}
#[test]
fn test_object_constructor_suggestions_after_operator() {
let result = r#"{"name": "MyService", "cap": 10}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(suggestions.iter().any(|s| s.text == ".cap"));
assert!(!suggestions.iter().any(|s| s.text == ".serviceName"));
assert!(!suggestions.iter().any(|s| s.text == ".base"));
}
#[test]
fn test_array_constructor_suggestions() {
let result = r#"["value1", "value2"]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Array,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
}
#[test]
fn test_primitive_results() {
assert_eq!(
ResultAnalyzer::analyze_parsed_result(
&parse_json("42"),
ResultType::Number,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE
)
.len(),
0
);
assert_eq!(
ResultAnalyzer::analyze_parsed_result(
&parse_json(r#""hello""#),
ResultType::String,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
)
.len(),
0
);
assert_eq!(
ResultAnalyzer::analyze_parsed_result(
&parse_json("true"),
ResultType::Boolean,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
)
.len(),
0
);
}
#[test]
fn test_null_result() {
let parsed = parse_json("null");
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Null,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_empty_string_result() {
let parsed = parse_json("");
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Null,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_invalid_json_result() {
let result = "not valid json {]";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Null,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_very_large_result() {
let mut result = String::from("[");
for i in 0..1000 {
if i > 0 {
result.push(',');
}
result.push_str(&format!(
r#"{{"id": {}, "name": "item{}", "value": {}}}"#,
i,
i,
i * 2
));
}
result.push(']');
let parsed = parse_json(&result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text == ".[].id"));
assert!(suggestions.iter().any(|s| s.text == ".[].name"));
assert!(suggestions.iter().any(|s| s.text == ".[].value"));
}
#[test]
fn test_array_with_nulls_in_result() {
let result = r#"[null, null, {"field": "value"}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(
suggestions.iter().any(|s| s.text == ".[].field"),
"Should find 'field' from third element past nulls"
);
}
#[test]
fn test_bounded_scan_in_results() {
let result = r#"[{"a": 1}, {"b": 2}, {"c": 3}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text == ".[].a"));
assert!(suggestions.iter().any(|s| s.text == ".[].b"));
assert!(suggestions.iter().any(|s| s.text == ".[].c"));
}
#[test]
fn test_destructured_objects_after_bracket_no_prefix() {
let result = r#"{"serviceArn": "arn1", "config": {}}
{"serviceArn": "arn2", "config": {}}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == "serviceArn"));
assert!(suggestions.iter().any(|s| s.text == "config"));
assert!(!suggestions.iter().any(|s| s.text.starts_with('.')));
}
#[test]
fn test_destructured_objects_after_pipe_with_prefix() {
let result = r#"{"serviceArn": "arn1"}
{"serviceArn": "arn2"}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::DestructuredObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".serviceArn"));
assert!(suggestions.iter().all(|s| s.text.starts_with('.')));
}
#[test]
fn test_array_of_objects_after_dot_no_prefix() {
let result = r#"[{"id": 1}, {"id": 2}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == "[]"));
assert!(suggestions.iter().any(|s| s.text == "[].id"));
assert!(!suggestions.iter().any(|s| s.text.starts_with('.')));
}
#[test]
fn test_array_of_objects_after_pipe_with_prefix() {
let result = r#"[{"id": 1}, {"id": 2}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text == ".[].id"));
assert!(suggestions.iter().all(|s| s.text.starts_with('.')));
}
#[test]
fn test_single_object_after_bracket_no_prefix() {
let result = r#"{"name": "Alice", "age": 30}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == "name"));
assert!(suggestions.iter().any(|s| s.text == "age"));
assert!(!suggestions.iter().any(|s| s.text.starts_with('.')));
}
#[test]
fn test_single_object_after_operator_with_prefix() {
let result = r#"{"name": "Alice", "age": 30}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(suggestions.iter().any(|s| s.text == ".age"));
assert!(suggestions.iter().all(|s| s.text.starts_with('.')));
}
#[test]
fn test_primitive_array_after_operator() {
let result = "[1, 2, 3]";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Array,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_primitive_array_after_continuation() {
let result = "[1, 2, 3]";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Array,
false,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, "[]");
}
#[test]
fn test_field_type_detection() {
let result = r#"{
"str": "hello",
"num": 42,
"bool": true,
"null": null,
"obj": {"nested": "value"},
"arr": [1, 2, 3]
}"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
let str_field = suggestions.iter().find(|s| s.text == ".str").unwrap();
assert!(matches!(str_field.field_type, Some(JsonFieldType::String)));
let num_field = suggestions.iter().find(|s| s.text == ".num").unwrap();
assert!(matches!(num_field.field_type, Some(JsonFieldType::Number)));
let bool_field = suggestions.iter().find(|s| s.text == ".bool").unwrap();
assert!(matches!(
bool_field.field_type,
Some(JsonFieldType::Boolean)
));
let null_field = suggestions.iter().find(|s| s.text == ".null").unwrap();
assert!(matches!(null_field.field_type, Some(JsonFieldType::Null)));
let obj_field = suggestions.iter().find(|s| s.text == ".obj").unwrap();
assert!(matches!(obj_field.field_type, Some(JsonFieldType::Object)));
let arr_field = suggestions.iter().find(|s| s.text == ".arr").unwrap();
assert!(matches!(
arr_field.field_type,
Some(JsonFieldType::ArrayOf(_))
));
}
#[test]
fn test_array_suggestions_in_element_context() {
let result = r#"[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true, true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(
!suggestions.iter().any(|s| s.text == ".[]"),
"Should not suggest .[] in element context"
);
assert!(suggestions.iter().any(|s| s.text == ".id"));
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(!suggestions.iter().any(|s| s.text == ".[].id"));
assert!(!suggestions.iter().any(|s| s.text == ".[].name"));
}
#[test]
fn test_array_suggestions_outside_element_context() {
let result = r#"[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true, false, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == ".[].id"));
assert!(suggestions.iter().any(|s| s.text == ".[].name"));
assert!(!suggestions.iter().any(|s| s.text == ".id"));
assert!(!suggestions.iter().any(|s| s.text == ".name"));
}
#[test]
fn test_element_context_preserves_field_types() {
let result = r#"[{"str": "hello", "num": 42, "obj": {}}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
let str_field = suggestions.iter().find(|s| s.text == ".str").unwrap();
assert!(matches!(str_field.field_type, Some(JsonFieldType::String)));
let num_field = suggestions.iter().find(|s| s.text == ".num").unwrap();
assert!(matches!(num_field.field_type, Some(JsonFieldType::Number)));
let obj_field = suggestions.iter().find(|s| s.text == ".obj").unwrap();
assert!(matches!(obj_field.field_type, Some(JsonFieldType::Object)));
}
#[test]
fn test_element_context_with_needs_leading_dot_false() {
let result = r#"[{"id": 1}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
false, true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(suggestions.iter().any(|s| s.text == "id"));
assert!(!suggestions.iter().any(|s| s.text == ".id"));
assert!(!suggestions.iter().any(|s| s.text == "[].id"));
}
#[test]
fn test_element_context_does_not_suggest_iterator() {
let result = r#"[{"id": 1}]"#;
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert!(
!suggestions.iter().any(|s| s.text == ".[]"),
"Should not suggest .[] in element context"
);
assert!(suggestions.iter().any(|s| s.text == ".id"));
}
#[test]
fn test_element_context_does_not_affect_other_types() {
let result = r#"{"name": "test"}"#;
let parsed = parse_json(result);
let suggestions_in = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
let suggestions_out = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Object,
true,
false, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions_in.len(), suggestions_out.len());
assert!(suggestions_in.iter().any(|s| s.text == ".name"));
assert!(suggestions_out.iter().any(|s| s.text == ".name"));
}
#[test]
fn test_element_context_empty_array() {
let result = "[]";
let parsed = parse_json(result);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::Array,
true,
true, DEFAULT_ARRAY_SAMPLE_SIZE,
);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
mod analyze_value_tests {
use super::*;
#[test]
fn test_analyze_value_object() {
let json: Value = serde_json::from_str(r#"{"name": "test", "age": 30}"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == ".name"));
assert!(suggestions.iter().any(|s| s.text == ".age"));
}
#[test]
fn test_analyze_value_array_of_objects() {
let json: Value =
serde_json::from_str(r#"[{"id": 1, "name": "Alice"}, {"id": 2, "name": "Bob"}]"#)
.unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text == ".[].id"));
assert!(suggestions.iter().any(|s| s.text == ".[].name"));
}
#[test]
fn test_analyze_value_array_of_objects_suppressed() {
let json: Value = serde_json::from_str(r#"[{"id": 1}, {"id": 2}]"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(suggestions.iter().any(|s| s.text == ".id"));
assert!(!suggestions.iter().any(|s| s.text == ".[]"));
assert!(!suggestions.iter().any(|s| s.text == ".[].id"));
}
#[test]
fn test_analyze_value_empty_array() {
let json: Value = serde_json::from_str("[]").unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_analyze_value_scalar_returns_empty() {
let number: Value = serde_json::from_str("42").unwrap();
let string: Value = serde_json::from_str(r#""hello""#).unwrap();
let boolean: Value = serde_json::from_str("true").unwrap();
let null: Value = serde_json::from_str("null").unwrap();
assert!(
ResultAnalyzer::analyze_value(&number, true, false, DEFAULT_ARRAY_SAMPLE_SIZE)
.is_empty()
);
assert!(
ResultAnalyzer::analyze_value(&string, true, false, DEFAULT_ARRAY_SAMPLE_SIZE)
.is_empty()
);
assert!(
ResultAnalyzer::analyze_value(&boolean, true, false, DEFAULT_ARRAY_SAMPLE_SIZE)
.is_empty()
);
assert!(
ResultAnalyzer::analyze_value(&null, true, false, DEFAULT_ARRAY_SAMPLE_SIZE).is_empty()
);
}
#[test]
fn test_analyze_value_nested_object() {
let json: Value = serde_json::from_str(
r#"{"user": {"profile": {"name": "Alice"}}, "settings": {"theme": "dark"}}"#,
)
.unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 2);
assert!(suggestions.iter().any(|s| s.text == ".user"));
assert!(suggestions.iter().any(|s| s.text == ".settings"));
}
#[test]
fn test_analyze_value_without_leading_dot() {
let json: Value = serde_json::from_str(r#"{"name": "test"}"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, false, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, "name");
}
#[test]
fn test_analyze_value_array_of_primitives() {
let json: Value = serde_json::from_str("[1, 2, 3]").unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_analyze_value_accepts_reference() {
let json: Value = serde_json::from_str(r#"{"field": "value"}"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".field");
}
}
#[test]
fn test_field_starting_with_digit_gets_quoted() {
let json: Value = serde_json::from_str(r#"{"1numeric_key": "value"}"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, r#"."1numeric_key""#);
}
#[test]
fn test_field_with_hyphen_gets_quoted() {
let json: Value = serde_json::from_str(r#"{"my-field": "value"}"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, r#"."my-field""#);
}
#[test]
fn test_valid_field_name_not_quoted() {
let json: Value = serde_json::from_str(r#"{"simple_key": "value"}"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".simple_key");
}
#[test]
fn test_multiple_fields_with_mixed_identifier_types() {
let json: Value =
serde_json::from_str(r#"{"simple_key": 1, "1numeric_key": 2, "hyphen-key": 3}"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 3);
let suggestion_texts: Vec<_> = suggestions.iter().map(|s| s.text.as_str()).collect();
assert!(suggestion_texts.contains(&".simple_key"));
assert!(suggestion_texts.contains(&r#"."1numeric_key""#));
assert!(suggestion_texts.contains(&r#"."hyphen-key""#));
}
#[test]
fn test_array_of_objects_with_nonsimple_field_names() {
let json: Value = serde_json::from_str(
r#"[{"1numeric_key": "value1", "simple_key": "value2"}, {"1numeric_key": "value3", "simple_key": "value4"}]"#,
)
.unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 3); let suggestion_texts: Vec<_> = suggestions.iter().map(|s| s.text.as_str()).collect();
assert!(suggestion_texts.contains(&".[]"));
assert!(suggestion_texts.contains(&r#".[]."1numeric_key""#));
assert!(suggestion_texts.contains(&".[].simple_key"));
}
#[test]
fn test_no_leading_dot_with_nonsimple_field() {
let json: Value = serde_json::from_str(r#"{"1numeric_key": "value"}"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, false, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, r#""1numeric_key""#);
}
#[test]
fn test_nested_array_name_quoting_across_levels() {
let json: Value = serde_json::from_str(
r#"{
"hyphen-array": [
{
"nested-items": [
{"simple_key": "value"}
]
}
]
}"#,
)
.unwrap();
let top_level = ResultAnalyzer::analyze_value(&json, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(top_level.iter().any(|s| s.text == r#"."hyphen-array""#));
assert!(!top_level.iter().any(|s| s.text == ".hyphen-array"));
let outer_array = json
.get("hyphen-array")
.and_then(Value::as_array)
.expect("outer array should exist");
let outer_obj = outer_array
.first()
.and_then(Value::as_object)
.expect("outer array should contain object");
let outer_obj_value = Value::Object(outer_obj.clone());
let nested_level =
ResultAnalyzer::analyze_value(&outer_obj_value, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(nested_level.iter().any(|s| s.text == r#"."nested-items""#));
assert!(!nested_level.iter().any(|s| s.text == ".nested-items"));
}
#[test]
fn test_nested_field_quoting_with_iteration() {
let json: Value = serde_json::from_str(
r#"{
"outer": [
{
"inner-array": [
{"hyphen-key": "v1", "1numeric_key": "v2", "simple_key": "v3"}
]
}
]
}"#,
)
.unwrap();
let outer_array = json
.get("outer")
.and_then(Value::as_array)
.expect("outer array should exist");
let outer_obj = outer_array
.first()
.and_then(Value::as_object)
.expect("outer array should contain object");
let outer_obj_value = Value::Object(outer_obj.clone());
let outer_suggestions =
ResultAnalyzer::analyze_value(&outer_obj_value, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(
outer_suggestions
.iter()
.any(|s| s.text == r#"."inner-array""#)
);
let inner_array = outer_obj_value
.get("inner-array")
.and_then(Value::as_array)
.expect("inner array should exist");
let inner_array_value = Value::Array(inner_array.clone());
let inner_suggestions =
ResultAnalyzer::analyze_value(&inner_array_value, true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(inner_suggestions.iter().any(|s| s.text == ".[]"));
assert!(
inner_suggestions
.iter()
.any(|s| s.text == r#".[]."hyphen-key""#)
);
assert!(
inner_suggestions
.iter()
.any(|s| s.text == r#".[]."1numeric_key""#)
);
assert!(inner_suggestions.iter().any(|s| s.text == ".[].simple_key"));
assert!(!inner_suggestions.iter().any(|s| s.text == ".[].hyphen-key"));
assert!(
!inner_suggestions
.iter()
.any(|s| s.text == ".[].1numeric_key")
);
}
#[test]
fn test_heterogeneous_array_union_keys() {
let json: Value = serde_json::from_str(r#"[{"a": 1}, {"b": 2}, {"c": 3}]"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(suggestions.iter().any(|s| s.text == ".a"));
assert!(suggestions.iter().any(|s| s.text == ".b"));
assert!(suggestions.iter().any(|s| s.text == ".c"));
}
#[test]
fn test_overlapping_keys_no_duplicates() {
let json: Value = serde_json::from_str(r#"[{"a": 1, "b": 2}, {"a": 3, "c": 4}]"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(
suggestions.iter().filter(|s| s.text == ".a").count(),
1,
"Key 'a' should appear exactly once despite being in multiple elements"
);
assert!(suggestions.iter().any(|s| s.text == ".b"));
assert!(suggestions.iter().any(|s| s.text == ".c"));
}
#[test]
fn test_overlapping_keys_type_from_first_occurrence() {
let json: Value = serde_json::from_str(r#"[{"x": 42}, {"x": "string"}]"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
let x_field = suggestions.iter().find(|s| s.text == ".x").unwrap();
assert!(
matches!(x_field.field_type, Some(JsonFieldType::Number)),
"Type for 'x' should be Number from first occurrence, got {:?}",
x_field.field_type
);
}
#[test]
fn test_mixed_types_in_array() {
let json: Value = serde_json::from_str(r#"[{"a": 1}, "string", {"b": 2}]"#).unwrap();
let suggestions = ResultAnalyzer::analyze_value(&json, true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(suggestions.iter().any(|s| s.text == ".a"));
assert!(suggestions.iter().any(|s| s.text == ".b"));
}
#[test]
fn test_analyze_multi_values_dedup_across_objects() {
let v1: Value = serde_json::from_str(r#"{"name": "Alice", "age": 30}"#).unwrap();
let v2: Value = serde_json::from_str(r#"{"name": "Bob", "role": "admin"}"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_multi_values(&[&v1, &v2], true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(
suggestions.iter().filter(|s| s.text == ".name").count(),
1,
"Should deduplicate 'name' across multiple values"
);
assert!(suggestions.iter().any(|s| s.text == ".age"));
assert!(suggestions.iter().any(|s| s.text == ".role"));
}
#[test]
fn test_analyze_multi_values_mixed_object_and_array() {
let obj: Value = serde_json::from_str(r#"{"shared": 1}"#).unwrap();
let arr: Value = serde_json::from_str(r#"[{"shared": "str", "unique": 2}]"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_multi_values(&[&obj, &arr], true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert_eq!(
suggestions.iter().filter(|s| s.text == ".shared").count(),
1,
"Shared key should appear once, deduped across Object and Array values"
);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert!(suggestions.iter().any(|s| s.text.contains("unique")));
}
#[test]
fn test_analyze_multi_values_empty_slice() {
let suggestions =
ResultAnalyzer::analyze_multi_values(&[], true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(
suggestions.is_empty(),
"Empty values slice should produce no suggestions"
);
}
#[test]
fn test_analyze_multi_values_scalars_only() {
let n = serde_json::json!(42);
let s = serde_json::json!("hello");
let b = serde_json::json!(true);
let suggestions =
ResultAnalyzer::analyze_multi_values(&[&n, &s, &b], true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(
suggestions.is_empty(),
"Scalar-only values should produce no suggestions"
);
}
#[test]
fn test_analyze_multi_values_multiple_arrays_single_bracket_suggestion() {
let a1: Value = serde_json::from_str(r#"[{"x": 1}]"#).unwrap();
let a2: Value = serde_json::from_str(r#"[{"y": 2}]"#).unwrap();
let a3: Value = serde_json::from_str(r#"[{"z": 3}]"#).unwrap();
let suggestions = ResultAnalyzer::analyze_multi_values(
&[&a1, &a2, &a3],
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
let bracket_count = suggestions.iter().filter(|s| s.text == ".[]").count();
assert_eq!(
bracket_count, 1,
"Multiple arrays should produce only one .[] suggestion"
);
assert!(suggestions.iter().any(|s| s.text.contains("x")));
assert!(suggestions.iter().any(|s| s.text.contains("y")));
assert!(suggestions.iter().any(|s| s.text.contains("z")));
}
#[test]
fn test_analyze_multi_values_array_with_suppress_brackets() {
let arr: Value = serde_json::from_str(r#"[{"a": 1}, {"b": 2}]"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_multi_values(&[&arr], true, true, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(
!suggestions.iter().any(|s| s.text == ".[]"),
"Should NOT suggest .[] when suppress_array_brackets=true"
);
assert!(suggestions.iter().any(|s| s.text == ".a"));
assert!(suggestions.iter().any(|s| s.text == ".b"));
}
#[test]
fn test_analyze_multi_values_array_of_only_scalars() {
let arr: Value = serde_json::from_str(r#"[1, 2, 3]"#).unwrap();
let suggestions =
ResultAnalyzer::analyze_multi_values(&[&arr], true, false, DEFAULT_ARRAY_SAMPLE_SIZE);
assert!(
suggestions.iter().any(|s| s.text == ".[]"),
"Scalar array should still suggest .[]"
);
assert_eq!(
suggestions.len(),
1,
"Scalar array should only produce .[] with no field suggestions"
);
}
#[test]
fn test_custom_sample_size_limits_array_field_suggestions() {
let elements: Vec<String> = (0..10)
.map(|i| format!(r#"{{"key_{}": {}}}"#, i, i))
.collect();
let json_str = format!("[{}]", elements.join(", "));
let parsed = parse_json(&json_str);
let suggestions =
ResultAnalyzer::analyze_parsed_result(&parsed, ResultType::ArrayOfObjects, true, false, 5);
for i in 0..5 {
assert!(
suggestions
.iter()
.any(|s| s.text == format!(".[].key_{}", i)),
"Should suggest key_{} within custom sample limit of 5",
i
);
}
assert!(
!suggestions.iter().any(|s| s.text == ".[].key_5"),
"Should NOT suggest key_5 beyond custom sample limit of 5"
);
}
#[test]
fn test_default_sample_size_includes_first_10_elements() {
let elements: Vec<String> = (0..15)
.map(|i| format!(r#"{{"key_{}": {}}}"#, i, i))
.collect();
let json_str = format!("[{}]", elements.join(", "));
let parsed = parse_json(&json_str);
let suggestions = ResultAnalyzer::analyze_parsed_result(
&parsed,
ResultType::ArrayOfObjects,
true,
false,
DEFAULT_ARRAY_SAMPLE_SIZE,
);
for i in 0..10 {
assert!(
suggestions
.iter()
.any(|s| s.text == format!(".[].key_{}", i)),
"Should suggest key_{} within default sample limit of 10",
i
);
}
assert!(
!suggestions.iter().any(|s| s.text == ".[].key_10"),
"Should NOT suggest key_10 beyond default sample limit of 10"
);
}
#[test]
fn test_sample_size_one_suggests_only_first_element_fields() {
let json_str = r#"[{"a": 1}, {"b": 2}, {"c": 3}]"#;
let parsed = parse_json(json_str);
let suggestions =
ResultAnalyzer::analyze_parsed_result(&parsed, ResultType::ArrayOfObjects, true, false, 1);
assert!(
suggestions.iter().any(|s| s.text == ".[].a"),
"Should suggest 'a' from first element"
);
assert!(
!suggestions.iter().any(|s| s.text == ".[].b"),
"Should NOT suggest 'b' with sample size 1"
);
assert!(
!suggestions.iter().any(|s| s.text == ".[].c"),
"Should NOT suggest 'c' with sample size 1"
);
}