use crate::query::executor::JqExecutor;
use serde_json::Value;
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ResultType {
ArrayOfObjects,
DestructuredObjects,
Object,
Array,
String,
Number,
Boolean,
Null,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CharType {
PipeOperator, Semicolon, Comma, Colon, OpenParen, OpenBracket, OpenBrace, CloseBracket, CloseBrace, CloseParen, QuestionMark, Dot, NoOp, }
pub struct QueryState {
pub executor: JqExecutor,
pub result: Result<String, String>,
pub last_successful_result: Option<String>,
pub last_successful_result_unformatted: Option<String>,
pub base_query_for_suggestions: Option<String>,
pub base_type_for_suggestions: Option<ResultType>,
}
impl QueryState {
pub fn new(json_input: String) -> Self {
let executor = JqExecutor::new(json_input);
let result = executor.execute(".");
let last_successful_result = result.as_ref().ok().cloned();
let last_successful_result_unformatted = last_successful_result
.as_ref()
.map(|s| Self::strip_ansi_codes(s));
let base_query_for_suggestions = Some(".".to_string());
let base_type_for_suggestions = last_successful_result_unformatted
.as_ref()
.map(|s| Self::detect_result_type(s));
Self {
executor,
result,
last_successful_result,
last_successful_result_unformatted,
base_query_for_suggestions,
base_type_for_suggestions,
}
}
pub fn execute(&mut self, query: &str) {
self.result = self.executor.execute(query);
if let Ok(result) = &self.result {
let unformatted = Self::strip_ansi_codes(result);
let is_only_nulls = unformatted
.lines()
.filter(|line| !line.trim().is_empty())
.all(|line| line.trim() == "null");
if !is_only_nulls {
self.last_successful_result = Some(result.clone());
self.last_successful_result_unformatted = Some(unformatted.clone());
let base_query = Self::normalize_base_query(query);
self.base_query_for_suggestions = Some(base_query);
self.base_type_for_suggestions = Some(Self::detect_result_type(&unformatted));
}
}
}
fn normalize_base_query(query: &str) -> String {
let mut base = query.trim_end().to_string();
if base.ends_with(" | .") {
base = base[..base.len() - 4].trim_end().to_string();
}
else if base.ends_with(" |") {
base = base[..base.len() - 2].trim_end().to_string();
}
else if base.ends_with('.') && base.len() > 1 {
base = base[..base.len() - 1].to_string();
}
base
}
fn detect_result_type(result: &str) -> ResultType {
use serde_json::Deserializer;
let mut deserializer = Deserializer::from_str(result).into_iter();
let first_value = match deserializer.next() {
Some(Ok(v)) => v,
_ => return ResultType::Null,
};
let has_multiple_values = deserializer.next().is_some();
match first_value {
Value::Object(_) if has_multiple_values => ResultType::DestructuredObjects,
Value::Object(_) => ResultType::Object,
Value::Array(ref arr) => {
if arr.is_empty() {
ResultType::Array
} else if matches!(arr[0], Value::Object(_)) {
ResultType::ArrayOfObjects
} else {
ResultType::Array
}
}
Value::String(_) => ResultType::String,
Value::Number(_) => ResultType::Number,
Value::Bool(_) => ResultType::Boolean,
Value::Null => ResultType::Null,
}
}
pub fn classify_char(ch: Option<char>) -> CharType {
match ch {
Some('|') => CharType::PipeOperator,
Some(';') => CharType::Semicolon,
Some(',') => CharType::Comma,
Some(':') => CharType::Colon,
Some('(') => CharType::OpenParen,
Some('[') => CharType::OpenBracket,
Some('{') => CharType::OpenBrace,
Some(']') => CharType::CloseBracket,
Some('}') => CharType::CloseBrace,
Some(')') => CharType::CloseParen,
Some('?') => CharType::QuestionMark,
Some('.') => CharType::Dot,
Some(_) => CharType::NoOp,
None => CharType::NoOp,
}
}
fn strip_ansi_codes(s: &str) -> String {
let mut result = String::with_capacity(s.len());
let mut chars = s.chars().peekable();
while let Some(ch) = chars.next() {
if ch == '\x1b' {
if chars.peek() == Some(&'[') {
chars.next(); for c in chars.by_ref() {
if c == 'm' {
break;
}
}
}
} else {
result.push(ch);
}
}
result
}
pub fn line_count(&self) -> u32 {
match &self.result {
Ok(result) => result.lines().count() as u32,
Err(_) => self
.last_successful_result
.as_ref()
.map(|r| r.lines().count() as u32)
.unwrap_or(0),
}
}
pub fn max_line_width(&self) -> u16 {
let content = match &self.result {
Ok(result) => result,
Err(_) => self.last_successful_result.as_deref().unwrap_or(""),
};
content
.lines()
.map(|l| l.len())
.max()
.unwrap_or(0)
.min(u16::MAX as usize) as u16
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_query_state() {
let json = r#"{"name": "test"}"#;
let state = QueryState::new(json.to_string());
assert!(state.result.is_ok());
assert!(state.last_successful_result.is_some());
}
#[test]
fn test_execute_updates_result() {
let json = r#"{"name": "test", "age": 30}"#;
let mut state = QueryState::new(json.to_string());
state.execute(".name");
assert!(state.result.is_ok());
assert!(state.last_successful_result.is_some());
}
#[test]
fn test_execute_caches_successful_result() {
let json = r#"{"value": 42}"#;
let mut state = QueryState::new(json.to_string());
state.execute(".value");
let cached = state.last_successful_result.clone();
assert!(cached.is_some());
state.execute(".[invalid syntax");
assert!(state.result.is_err());
assert_eq!(state.last_successful_result, cached);
}
#[test]
fn test_line_count_with_ok_result() {
let json = r#"{"test": true}"#;
let mut state = QueryState::new(json.to_string());
let content: String = (0..50).map(|i| format!("line{}\n", i)).collect();
state.result = Ok(content);
assert_eq!(state.line_count(), 50);
}
#[test]
fn test_line_count_uses_cached_on_error() {
let json = r#"{"test": true}"#;
let mut state = QueryState::new(json.to_string());
let valid_result: String = (0..30).map(|i| format!("line{}\n", i)).collect();
state.result = Ok(valid_result.clone());
state.last_successful_result = Some(valid_result);
state.result = Err("syntax error".to_string());
assert_eq!(state.line_count(), 30);
}
#[test]
fn test_line_count_zero_on_error_without_cache() {
let json = r#"{"test": true}"#;
let mut state = QueryState::new(json.to_string());
state.result = Err("error".to_string());
state.last_successful_result = None;
assert_eq!(state.line_count(), 0);
}
#[test]
fn test_null_results_dont_overwrite_cache() {
let json = r#"{"name": "test", "age": 30}"#;
let mut state = QueryState::new(json.to_string());
let initial_cache = state.last_successful_result.clone();
let initial_unformatted = state.last_successful_result_unformatted.clone();
assert!(initial_cache.is_some());
assert!(initial_unformatted.is_some());
state.execute(".nonexistent");
assert!(state.result.is_ok());
assert!(state.result.as_ref().unwrap().contains("null"));
assert_eq!(state.last_successful_result, initial_cache);
assert_eq!(
state.last_successful_result_unformatted,
initial_unformatted
);
state.execute(".name");
assert!(state.result.as_ref().unwrap().contains("test"));
assert_ne!(state.last_successful_result, initial_cache);
assert_ne!(
state.last_successful_result_unformatted,
initial_unformatted
);
assert!(
state
.last_successful_result
.as_ref()
.unwrap()
.contains("test")
);
let unformatted = state.last_successful_result_unformatted.as_ref().unwrap();
assert!(!unformatted.contains("\x1b"));
assert!(unformatted.contains("test"));
}
#[test]
fn test_strip_ansi_codes_simple() {
let input = "\x1b[0m{\x1b[1;39m\"name\"\x1b[0m: \x1b[0;32m\"test\"\x1b[0m}";
let output = QueryState::strip_ansi_codes(input);
assert_eq!(output, r#"{"name": "test"}"#);
assert!(!output.contains("\x1b"));
}
#[test]
fn test_strip_ansi_codes_complex() {
let input = "\x1b[1;39m{\x1b[0m\n \x1b[0;34m\"key\"\x1b[0m: \x1b[0;32m\"value\"\x1b[0m\n\x1b[1;39m}\x1b[0m";
let output = QueryState::strip_ansi_codes(input);
assert!(output.contains(r#""key""#));
assert!(output.contains(r#""value""#));
assert!(!output.contains("\x1b"));
}
#[test]
fn test_strip_ansi_codes_no_codes() {
let input = r#"{"name": "plain"}"#;
let output = QueryState::strip_ansi_codes(input);
assert_eq!(output, input);
}
#[test]
fn test_strip_ansi_codes_null_with_color() {
let input = "\x1b[0;90mnull\x1b[0m";
let output = QueryState::strip_ansi_codes(input);
assert_eq!(output, "null");
}
#[test]
fn test_unformatted_result_stored_on_execute() {
let json = r#"{"name": "test"}"#;
let mut state = QueryState::new(json.to_string());
state.execute(".name");
assert!(state.last_successful_result.is_some());
assert!(state.last_successful_result_unformatted.is_some());
let unformatted = state.last_successful_result_unformatted.as_ref().unwrap();
assert!(!unformatted.contains("\x1b"));
}
#[test]
fn test_unformatted_result_handles_multiline_objects() {
let json = r#"{"items": [{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]}"#;
let mut state = QueryState::new(json.to_string());
state.execute(".items[]");
let unformatted = state.last_successful_result_unformatted.as_ref().unwrap();
assert!(!unformatted.contains("\x1b"));
assert!(unformatted.contains("id"));
assert!(unformatted.contains("name"));
}
#[test]
fn test_multiple_nulls_dont_overwrite_cache() {
let json = r#"{"items": [{"id": 1}, {"id": 2}, {"id": 3}]}"#;
let mut state = QueryState::new(json.to_string());
state.execute(".items[]");
let cached_after_items = state.last_successful_result_unformatted.clone();
assert!(cached_after_items.is_some());
assert!(cached_after_items.as_ref().unwrap().contains("id"));
state.execute(".items[].nonexistent");
assert!(state.result.as_ref().unwrap().contains("null"));
assert_eq!(state.last_successful_result_unformatted, cached_after_items);
}
#[test]
fn test_detect_array_of_objects() {
let result = r#"[{"id": 1, "name": "a"}, {"id": 2, "name": "b"}]"#;
assert_eq!(
QueryState::detect_result_type(result),
ResultType::ArrayOfObjects
);
}
#[test]
fn test_detect_empty_array() {
let result = "[]";
assert_eq!(QueryState::detect_result_type(result), ResultType::Array);
}
#[test]
fn test_detect_array_of_primitives() {
let result = "[1, 2, 3, 4, 5]";
assert_eq!(QueryState::detect_result_type(result), ResultType::Array);
let result = r#"["a", "b", "c"]"#;
assert_eq!(QueryState::detect_result_type(result), ResultType::Array);
let result = "[true, false, true]";
assert_eq!(QueryState::detect_result_type(result), ResultType::Array);
}
#[test]
fn test_detect_destructured_objects() {
let result = r#"{"id": 1, "name": "a"}
{"id": 2, "name": "b"}
{"id": 3, "name": "c"}"#;
assert_eq!(
QueryState::detect_result_type(result),
ResultType::DestructuredObjects
);
}
#[test]
fn test_detect_destructured_objects_pretty_printed() {
let result = r#"{
"id": 1,
"name": "a"
}
{
"id": 2,
"name": "b"
}"#;
assert_eq!(
QueryState::detect_result_type(result),
ResultType::DestructuredObjects
);
}
#[test]
fn test_detect_single_object() {
let result = r#"{"name": "test", "age": 30}"#;
assert_eq!(QueryState::detect_result_type(result), ResultType::Object);
}
#[test]
fn test_detect_single_object_pretty_printed() {
let result = r#"{
"name": "test",
"age": 30
}"#;
assert_eq!(QueryState::detect_result_type(result), ResultType::Object);
}
#[test]
fn test_detect_string() {
let result = r#""hello world""#;
assert_eq!(QueryState::detect_result_type(result), ResultType::String);
}
#[test]
fn test_detect_number() {
let result = "42";
assert_eq!(QueryState::detect_result_type(result), ResultType::Number);
let result = "3.14159";
assert_eq!(QueryState::detect_result_type(result), ResultType::Number);
let result = "-100";
assert_eq!(QueryState::detect_result_type(result), ResultType::Number);
}
#[test]
fn test_detect_boolean() {
let result = "true";
assert_eq!(QueryState::detect_result_type(result), ResultType::Boolean);
let result = "false";
assert_eq!(QueryState::detect_result_type(result), ResultType::Boolean);
}
#[test]
fn test_detect_null() {
let result = "null";
assert_eq!(QueryState::detect_result_type(result), ResultType::Null);
}
#[test]
fn test_detect_invalid_json_returns_null() {
let result = "not valid json";
assert_eq!(QueryState::detect_result_type(result), ResultType::Null);
}
#[test]
fn test_detect_multiple_primitives() {
let result = r#""value1"
"value2"
"value3""#;
assert_eq!(QueryState::detect_result_type(result), ResultType::String);
}
#[test]
fn test_classify_pipe_operator() {
assert_eq!(QueryState::classify_char(Some('|')), CharType::PipeOperator);
}
#[test]
fn test_classify_semicolon() {
assert_eq!(QueryState::classify_char(Some(';')), CharType::Semicolon);
}
#[test]
fn test_classify_comma() {
assert_eq!(QueryState::classify_char(Some(',')), CharType::Comma);
}
#[test]
fn test_classify_colon() {
assert_eq!(QueryState::classify_char(Some(':')), CharType::Colon);
}
#[test]
fn test_classify_open_paren() {
assert_eq!(QueryState::classify_char(Some('(')), CharType::OpenParen);
}
#[test]
fn test_classify_close_paren() {
assert_eq!(QueryState::classify_char(Some(')')), CharType::CloseParen);
}
#[test]
fn test_classify_open_bracket() {
assert_eq!(QueryState::classify_char(Some('[')), CharType::OpenBracket);
}
#[test]
fn test_classify_close_bracket() {
assert_eq!(QueryState::classify_char(Some(']')), CharType::CloseBracket);
}
#[test]
fn test_classify_open_brace() {
assert_eq!(QueryState::classify_char(Some('{')), CharType::OpenBrace);
}
#[test]
fn test_classify_close_brace() {
assert_eq!(QueryState::classify_char(Some('}')), CharType::CloseBrace);
}
#[test]
fn test_classify_question_mark() {
assert_eq!(QueryState::classify_char(Some('?')), CharType::QuestionMark);
}
#[test]
fn test_classify_dot() {
assert_eq!(QueryState::classify_char(Some('.')), CharType::Dot);
}
#[test]
fn test_classify_no_op_characters() {
assert_eq!(QueryState::classify_char(Some('a')), CharType::NoOp);
assert_eq!(QueryState::classify_char(Some('Z')), CharType::NoOp);
assert_eq!(QueryState::classify_char(Some('5')), CharType::NoOp);
assert_eq!(QueryState::classify_char(Some('_')), CharType::NoOp);
}
#[test]
fn test_classify_none() {
assert_eq!(QueryState::classify_char(None), CharType::NoOp);
}
#[test]
fn test_normalize_strips_pipe_with_identity() {
assert_eq!(
QueryState::normalize_base_query(".services | ."),
".services"
);
assert_eq!(QueryState::normalize_base_query(".items[] | ."), ".items[]");
}
#[test]
fn test_normalize_strips_incomplete_pipe() {
assert_eq!(QueryState::normalize_base_query(".services |"), ".services");
assert_eq!(QueryState::normalize_base_query(".items[] | "), ".items[]");
}
#[test]
fn test_normalize_strips_trailing_dot() {
assert_eq!(QueryState::normalize_base_query(".services."), ".services");
assert_eq!(
QueryState::normalize_base_query(".services[]."),
".services[]"
);
assert_eq!(
QueryState::normalize_base_query(".user.profile."),
".user.profile"
);
}
#[test]
fn test_normalize_strips_trailing_whitespace() {
assert_eq!(QueryState::normalize_base_query(".services "), ".services");
assert_eq!(QueryState::normalize_base_query(".services "), ".services");
assert_eq!(QueryState::normalize_base_query(".services\t"), ".services");
}
#[test]
fn test_normalize_preserves_root() {
assert_eq!(QueryState::normalize_base_query("."), ".");
}
#[test]
fn test_normalize_preserves_complete_queries() {
assert_eq!(QueryState::normalize_base_query(".services"), ".services");
assert_eq!(
QueryState::normalize_base_query(".services[]"),
".services[]"
);
assert_eq!(QueryState::normalize_base_query(".user.name"), ".user.name");
}
#[test]
fn test_normalize_handles_complex_patterns() {
assert_eq!(QueryState::normalize_base_query(".a | .b | ."), ".a | .b");
assert_eq!(
QueryState::normalize_base_query(".services[].config | ."),
".services[].config"
);
}
}