use crate::autocomplete::autocomplete_state::{JsonFieldType, Suggestion, SuggestionType};
use crate::query::ResultType;
use serde_json::Value;
pub struct ResultAnalyzer;
#[inline]
fn dot_prefix(needs_leading_dot: bool) -> &'static str {
if needs_leading_dot { "." } else { "" }
}
impl ResultAnalyzer {
fn extract_object_fields(
map: &serde_json::Map<String, Value>,
prefix: &str,
suggestions: &mut Vec<Suggestion>,
) {
for (key, val) in map {
let field_type = Self::detect_json_type(val);
suggestions.push(Suggestion::new_with_type(
format!("{}{}", prefix, key),
SuggestionType::Field,
Some(field_type),
));
}
}
pub fn analyze_result(
result: &str,
result_type: ResultType,
needs_leading_dot: bool,
) -> Vec<Suggestion> {
if result.trim().is_empty() {
return Vec::new();
}
let value = match Self::parse_first_json_value(result) {
Some(v) => v,
None => return Vec::new(),
};
Self::extract_suggestions_for_type(&value, result_type, needs_leading_dot)
}
fn parse_first_json_value(text: &str) -> Option<Value> {
let text = text.trim();
if text.is_empty() {
return None;
}
if let Ok(value) = serde_json::from_str(text) {
return Some(value);
}
let mut deserializer = serde_json::Deserializer::from_str(text).into_iter();
if let Some(Ok(value)) = deserializer.next() {
return Some(value);
}
None
}
fn extract_suggestions_for_type(
value: &Value,
result_type: ResultType,
needs_leading_dot: bool,
) -> Vec<Suggestion> {
match result_type {
ResultType::ArrayOfObjects => {
let prefix = dot_prefix(needs_leading_dot);
let mut suggestions = vec![Suggestion::new_with_type(
format!("{}[]", prefix),
SuggestionType::Pattern,
None,
)];
if let Value::Array(arr) = value
&& let Some(Value::Object(map)) = arr.first()
{
for (key, val) in map {
let field_type = Self::detect_json_type(val);
suggestions.push(Suggestion::new_with_type(
format!("{}[].{}", prefix, key),
SuggestionType::Field,
Some(field_type),
));
}
}
suggestions
}
ResultType::DestructuredObjects => {
let prefix = dot_prefix(needs_leading_dot);
let mut suggestions = Vec::new();
if let Value::Object(map) = value {
Self::extract_object_fields(map, prefix, &mut suggestions);
}
suggestions
}
ResultType::Object => {
let prefix = dot_prefix(needs_leading_dot);
let mut suggestions = Vec::new();
if let Value::Object(map) = value {
Self::extract_object_fields(map, prefix, &mut suggestions);
}
suggestions
}
ResultType::Array => {
let prefix = dot_prefix(needs_leading_dot);
vec![Suggestion::new_with_type(
format!("{}[]", prefix),
SuggestionType::Pattern,
None,
)]
}
_ => Vec::new(), }
}
fn detect_json_type(value: &Value) -> JsonFieldType {
match value {
Value::Null => JsonFieldType::Null,
Value::Bool(_) => JsonFieldType::Boolean,
Value::Number(_) => JsonFieldType::Number,
Value::String(_) => JsonFieldType::String,
Value::Array(arr) => {
if arr.is_empty() {
JsonFieldType::Array
} else {
let inner_type = Self::detect_json_type(&arr[0]);
JsonFieldType::ArrayOf(Box::new(inner_type))
}
}
Value::Object(_) => JsonFieldType::Object,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_analyze_simple_object() {
let result = r#"{"name": "test", "age": 30, "active": true}"#;
let suggestions = ResultAnalyzer::analyze_result(
result,
ResultType::Object,
true, );
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, false);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Array, true);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_analyze_empty_object() {
let result = "{}";
let suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_analyze_pretty_printed_object() {
let result = r#"{
"name": "Alice",
"age": 30
}"#;
let suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
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 suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, false);
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 suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, true);
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 suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, false);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Number, true);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_multivalue_with_whitespace() {
let result = r#"
{"key1": "val1"}
{"key2": "val2"}
"#;
let suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Array, true);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
}
#[test]
fn test_primitive_results() {
assert_eq!(
ResultAnalyzer::analyze_result("42", ResultType::Number, true).len(),
0
);
assert_eq!(
ResultAnalyzer::analyze_result(r#""hello""#, ResultType::String, true).len(),
0
);
assert_eq!(
ResultAnalyzer::analyze_result("true", ResultType::Boolean, true).len(),
0
);
}
#[test]
fn test_null_result() {
let suggestions = ResultAnalyzer::analyze_result("null", ResultType::Null, true);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_empty_string_result() {
let suggestions = ResultAnalyzer::analyze_result("", ResultType::Null, true);
assert_eq!(suggestions.len(), 0);
}
#[test]
fn test_invalid_json_result() {
let result = "not valid json {]";
let suggestions = ResultAnalyzer::analyze_result(result, ResultType::Null, true);
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 suggestions = ResultAnalyzer::analyze_result(&result, ResultType::ArrayOfObjects, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, true);
assert!(suggestions.iter().any(|s| s.text == ".[]"));
assert_eq!(suggestions.len(), 1); }
#[test]
fn test_bounded_scan_in_results() {
let result = r#"[{"a": 1}, {"b": 2}, {"c": 3}]"#;
let suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, true);
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 suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, false);
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 suggestions =
ResultAnalyzer::analyze_result(result, ResultType::DestructuredObjects, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, false);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::ArrayOfObjects, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, false);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Array, true);
assert_eq!(suggestions.len(), 1);
assert_eq!(suggestions[0].text, ".[]");
}
#[test]
fn test_primitive_array_after_continuation() {
let result = "[1, 2, 3]";
let suggestions = ResultAnalyzer::analyze_result(result, ResultType::Array, false);
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 suggestions = ResultAnalyzer::analyze_result(result, ResultType::Object, true);
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(_))
));
}
}