use serde::Serialize;
use std::collections::HashMap;
use std::time::Duration;
use crate::confidence::ConfidenceMetadata;
#[derive(Debug, Clone, Serialize)]
pub struct QueryMeta {
#[serde(skip_serializing_if = "Option::is_none")]
pub pattern: Option<String>,
#[serde(skip_serializing_if = "Filters::is_empty")]
pub filters: Filters,
pub execution_time_ms: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub plan: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub confidence: Option<ConfidenceMetadata>,
}
impl QueryMeta {
#[must_use]
pub fn new(pattern: Option<String>, execution_time: Duration) -> Self {
Self {
pattern,
filters: Filters::default(),
execution_time_ms: execution_time.as_secs_f64() * 1000.0,
plan: None,
confidence: None,
}
}
#[must_use]
pub fn with_filters(mut self, filters: Filters) -> Self {
self.filters = filters;
self
}
#[must_use]
pub fn with_plan(mut self, plan: String) -> Self {
self.plan = Some(plan);
self
}
#[must_use]
pub fn with_confidence(mut self, confidence: ConfidenceMetadata) -> Self {
self.confidence = Some(confidence);
self
}
}
#[derive(Debug, Clone, Default, Serialize)]
pub struct Filters {
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub lang: Option<String>,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub ignore_case: bool,
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub exact: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub fuzzy: Option<FuzzyFilters>,
}
impl Filters {
#[must_use]
pub fn is_empty(&self) -> bool {
self.kind.is_none()
&& self.lang.is_none()
&& !self.ignore_case
&& !self.exact
&& self.fuzzy.is_none()
}
}
#[derive(Debug, Clone, Serialize)]
pub struct FuzzyFilters {
pub algorithm: String,
pub threshold: f64,
#[serde(skip_serializing_if = "Option::is_none")]
pub max_candidates: Option<usize>,
}
#[derive(Debug, Clone, Serialize)]
pub struct Stats {
pub total_matches: usize,
pub returned: usize,
#[serde(rename = "truncated")]
pub is_truncated: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub index_age_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub candidate_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filtered_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub used_ancestor_index: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub filtered_to: Option<String>,
}
impl Stats {
#[must_use]
pub fn new(total: usize, returned: usize) -> Self {
Self {
total_matches: total,
returned,
is_truncated: returned < total,
index_age_seconds: None,
candidate_count: None,
filtered_count: None,
used_ancestor_index: None,
filtered_to: None,
}
}
#[must_use]
pub fn with_index_age(mut self, age_seconds: u64) -> Self {
self.index_age_seconds = Some(age_seconds);
self
}
#[must_use]
pub fn with_candidates(mut self, total: usize, filtered: usize) -> Self {
self.candidate_count = Some(total);
self.filtered_count = Some(filtered);
self
}
#[must_use]
pub fn with_scope_info(mut self, is_ancestor: bool, filtered_to: Option<String>) -> Self {
self.used_ancestor_index = Some(is_ancestor);
self.filtered_to = filtered_to;
self
}
}
#[derive(Debug, Clone, Serialize)]
pub struct JsonResponse<T> {
pub query: QueryMeta,
pub stats: Stats,
pub results: Vec<T>,
}
impl<T> JsonResponse<T> {
#[must_use]
pub fn new(query: QueryMeta, stats: Stats, results: Vec<T>) -> Self {
Self {
query,
stats,
results,
}
}
}
#[derive(Debug, Clone, Serialize)]
#[serde(tag = "event", rename_all = "snake_case")]
pub enum StreamEvent<T> {
PartialResult {
result: T,
score: f64,
},
FinalSummary {
stats: Stats,
},
}
#[derive(Debug, Clone, Serialize)]
pub struct IndexStatus {
pub exists: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub created_at: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub age_seconds: Option<u64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub languages: Option<Vec<String>>,
pub supports_fuzzy: bool,
pub supports_relations: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub cross_language_relation_count: Option<usize>,
#[serde(skip_serializing_if = "Option::is_none")]
pub symbol_counts_by_kind: Option<HashMap<String, usize>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub file_counts_by_language: Option<HashMap<String, usize>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub relation_counts_by_pair: Option<HashMap<String, usize>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub stale: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub building: Option<bool>,
#[serde(skip_serializing_if = "Option::is_none")]
pub build_age_seconds: Option<u64>,
}
impl IndexStatus {
#[must_use]
pub fn not_found() -> Self {
Self {
exists: false,
path: None,
created_at: None,
age_seconds: None,
symbol_count: None,
file_count: None,
languages: None,
supports_fuzzy: false,
supports_relations: false,
cross_language_relation_count: None,
symbol_counts_by_kind: None,
file_counts_by_language: None,
relation_counts_by_pair: None,
stale: None,
building: None,
build_age_seconds: None,
}
}
#[must_use]
pub fn from_index(path: String, created_at: String, age_seconds: u64) -> IndexStatusBuilder {
IndexStatusBuilder {
path,
created_at,
age_seconds,
symbol_count: 0,
file_count: None,
languages: Vec::new(),
has_relations: false,
has_trigram: false,
cross_language_relation_count: 0,
symbol_counts_by_kind: None,
file_counts_by_language: None,
relation_counts_by_pair: None,
building: None,
build_age_seconds: None,
}
}
}
pub struct IndexStatusBuilder {
path: String,
created_at: String,
age_seconds: u64,
symbol_count: usize,
file_count: Option<usize>,
languages: Vec<String>,
has_relations: bool,
has_trigram: bool,
cross_language_relation_count: usize,
symbol_counts_by_kind: Option<HashMap<String, usize>>,
file_counts_by_language: Option<HashMap<String, usize>>,
relation_counts_by_pair: Option<HashMap<String, usize>>,
building: Option<bool>,
build_age_seconds: Option<u64>,
}
impl IndexStatusBuilder {
#[must_use]
pub fn symbol_count(mut self, count: usize) -> Self {
self.symbol_count = count;
self
}
#[must_use]
pub fn file_count(mut self, count: usize) -> Self {
self.file_count = Some(count);
self
}
#[must_use]
pub fn file_count_opt(mut self, count: Option<usize>) -> Self {
self.file_count = count;
self
}
#[must_use]
pub fn languages(mut self, langs: Vec<String>) -> Self {
self.languages = langs;
self
}
#[must_use]
pub fn has_relations(mut self, value: bool) -> Self {
self.has_relations = value;
self
}
#[must_use]
pub fn has_trigram(mut self, value: bool) -> Self {
self.has_trigram = value;
self
}
#[must_use]
pub fn cross_language_relation_count(mut self, count: usize) -> Self {
self.cross_language_relation_count = count;
self
}
#[must_use]
pub fn symbol_counts_by_kind(mut self, counts: HashMap<String, usize>) -> Self {
self.symbol_counts_by_kind = Some(counts);
self
}
#[must_use]
pub fn file_counts_by_language(mut self, counts: HashMap<String, usize>) -> Self {
self.file_counts_by_language = Some(counts);
self
}
#[must_use]
pub fn relation_counts_by_pair(mut self, counts: HashMap<String, usize>) -> Self {
self.relation_counts_by_pair = Some(counts);
self
}
#[must_use]
pub fn building(mut self, value: bool) -> Self {
self.building = Some(value);
self
}
#[must_use]
pub fn build_age_seconds(mut self, value: u64) -> Self {
self.build_age_seconds = Some(value);
self
}
#[must_use]
pub fn build(self) -> IndexStatus {
let stale = self.age_seconds > 86400; IndexStatus {
exists: true,
path: Some(self.path),
created_at: Some(self.created_at),
age_seconds: Some(self.age_seconds),
symbol_count: Some(self.symbol_count),
file_count: self.file_count,
languages: Some(self.languages),
supports_fuzzy: self.has_trigram,
supports_relations: self.has_relations,
cross_language_relation_count: if self.cross_language_relation_count > 0 {
Some(self.cross_language_relation_count)
} else {
None
},
symbol_counts_by_kind: self.symbol_counts_by_kind,
file_counts_by_language: self.file_counts_by_language,
relation_counts_by_pair: self.relation_counts_by_pair,
stale: Some(stale),
building: self.building,
build_age_seconds: self.build_age_seconds,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
#[test]
fn test_query_meta_serialization() {
let meta = QueryMeta::new(Some("handler".to_string()), Duration::from_millis(5));
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"pattern\":\"handler\""));
assert!(json.contains("\"execution_time_ms\":5"));
}
#[test]
fn test_filters_empty() {
let filters = Filters::default();
assert!(filters.is_empty());
let json = serde_json::to_value(&filters).unwrap();
assert_eq!(json.as_object().unwrap().len(), 0);
}
#[test]
fn test_filters_with_values() {
let filters = Filters {
kind: Some("function".to_string()),
lang: Some("rust".to_string()),
ignore_case: true,
..Default::default()
};
assert!(!filters.is_empty());
let json = serde_json::to_string(&filters).unwrap();
assert!(json.contains("\"kind\":\"function\""));
assert!(json.contains("\"lang\":\"rust\""));
assert!(json.contains("\"ignore_case\":true"));
}
#[test]
fn test_stats_truncation() {
let stats = Stats::new(100, 50);
assert_eq!(stats.total_matches, 100);
assert_eq!(stats.returned, 50);
assert!(stats.is_truncated);
let stats_not_truncated = Stats::new(50, 50);
assert!(!stats_not_truncated.is_truncated);
}
#[test]
fn test_json_response() {
let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(10));
let stats = Stats::new(5, 5);
let results = vec!["result1", "result2"];
let response = JsonResponse::new(meta, stats, results);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"query\""));
assert!(json.contains("\"stats\""));
assert!(json.contains("\"results\""));
}
#[test]
fn test_stream_event_partial() {
let event = StreamEvent::PartialResult {
result: "test_symbol",
score: 0.95,
};
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"event\":\"partial_result\""));
assert!(json.contains("\"score\":0.95"));
}
#[test]
fn test_stream_event_final() {
let stats = Stats::new(42, 20);
let event: StreamEvent<()> = StreamEvent::FinalSummary { stats };
let json = serde_json::to_string(&event).unwrap();
assert!(json.contains("\"event\":\"final_summary\""));
assert!(json.contains("\"total_matches\":42"));
}
#[test]
fn test_query_meta_with_confidence() {
use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
let confidence = ConfidenceMetadata {
level: ConfidenceLevel::AstOnly,
limitations: vec!["Missing rust-analyzer".to_string()],
unavailable_features: vec!["Type inference".to_string()],
};
let meta = QueryMeta::new(Some("test_pattern".to_string()), Duration::from_millis(10))
.with_confidence(confidence.clone());
assert!(meta.confidence.is_some());
let meta_confidence = meta.confidence.unwrap();
assert_eq!(meta_confidence.level, ConfidenceLevel::AstOnly);
assert_eq!(meta_confidence.limitations.len(), 1);
assert_eq!(meta_confidence.unavailable_features.len(), 1);
}
#[test]
fn test_query_meta_confidence_serialization() {
use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
let confidence = ConfidenceMetadata {
level: ConfidenceLevel::Partial,
limitations: vec!["Limited accuracy".to_string()],
unavailable_features: vec![],
};
let meta = QueryMeta::new(Some("search_pattern".to_string()), Duration::from_millis(5))
.with_confidence(confidence);
let json = serde_json::to_string(&meta).unwrap();
assert!(json.contains("\"confidence\""));
assert!(json.contains("\"partial\""));
assert!(json.contains("\"limitations\""));
assert!(json.contains("\"Limited accuracy\""));
}
#[test]
fn test_query_meta_without_confidence() {
let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(5));
assert!(meta.confidence.is_none());
let json = serde_json::to_string(&meta).unwrap();
assert!(!json.contains("\"confidence\""));
}
#[test]
fn test_json_response_with_confidence() {
use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
let confidence = ConfidenceMetadata {
level: ConfidenceLevel::Verified,
limitations: vec![],
unavailable_features: vec![],
};
let meta = QueryMeta::new(Some("pattern".to_string()), Duration::from_millis(8))
.with_confidence(confidence);
let stats = Stats::new(10, 10);
let results = vec!["result1", "result2"];
let response = JsonResponse::new(meta, stats, results);
let json = serde_json::to_string(&response).unwrap();
assert!(json.contains("\"confidence\""));
assert!(json.contains("\"verified\""));
}
}