1use serde::Serialize;
7use std::collections::HashMap;
8use std::time::Duration;
9
10use crate::confidence::ConfidenceMetadata;
11
12#[derive(Debug, Clone, Serialize)]
14pub struct QueryMeta {
15 #[serde(skip_serializing_if = "Option::is_none")]
17 pub pattern: Option<String>,
18
19 #[serde(skip_serializing_if = "Filters::is_empty")]
21 pub filters: Filters,
22
23 pub execution_time_ms: f64,
25
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub plan: Option<String>,
29
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub confidence: Option<ConfidenceMetadata>,
33}
34
35impl QueryMeta {
36 #[must_use]
38 pub fn new(pattern: Option<String>, execution_time: Duration) -> Self {
39 Self {
40 pattern,
41 filters: Filters::default(),
42 execution_time_ms: execution_time.as_secs_f64() * 1000.0,
43 plan: None,
44 confidence: None,
45 }
46 }
47
48 #[must_use]
50 pub fn with_filters(mut self, filters: Filters) -> Self {
51 self.filters = filters;
52 self
53 }
54
55 #[must_use]
57 pub fn with_plan(mut self, plan: String) -> Self {
58 self.plan = Some(plan);
59 self
60 }
61
62 #[must_use]
64 pub fn with_confidence(mut self, confidence: ConfidenceMetadata) -> Self {
65 self.confidence = Some(confidence);
66 self
67 }
68}
69
70#[derive(Debug, Clone, Default, Serialize)]
72pub struct Filters {
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub kind: Option<String>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub lang: Option<String>,
80
81 #[serde(skip_serializing_if = "std::ops::Not::not")]
83 pub ignore_case: bool,
84
85 #[serde(skip_serializing_if = "std::ops::Not::not")]
87 pub exact: bool,
88
89 #[serde(skip_serializing_if = "Option::is_none")]
91 pub fuzzy: Option<FuzzyFilters>,
92}
93
94impl Filters {
95 #[must_use]
97 pub fn is_empty(&self) -> bool {
98 self.kind.is_none()
99 && self.lang.is_none()
100 && !self.ignore_case
101 && !self.exact
102 && self.fuzzy.is_none()
103 }
104}
105
106#[derive(Debug, Clone, Serialize)]
108pub struct FuzzyFilters {
109 pub algorithm: String,
111
112 pub threshold: f64,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub max_candidates: Option<usize>,
118}
119
120#[derive(Debug, Clone, Serialize)]
122pub struct Stats {
123 pub total_matches: usize,
125
126 pub returned: usize,
128
129 #[serde(rename = "truncated")]
131 pub is_truncated: bool,
132
133 #[serde(skip_serializing_if = "Option::is_none")]
135 pub index_age_seconds: Option<u64>,
136
137 #[serde(skip_serializing_if = "Option::is_none")]
139 pub candidate_count: Option<usize>,
140
141 #[serde(skip_serializing_if = "Option::is_none")]
143 pub filtered_count: Option<usize>,
144
145 #[serde(skip_serializing_if = "Option::is_none")]
147 pub used_ancestor_index: Option<bool>,
148
149 #[serde(skip_serializing_if = "Option::is_none")]
151 pub filtered_to: Option<String>,
152}
153
154impl Stats {
155 #[must_use]
157 pub fn new(total: usize, returned: usize) -> Self {
158 Self {
159 total_matches: total,
160 returned,
161 is_truncated: returned < total,
162 index_age_seconds: None,
163 candidate_count: None,
164 filtered_count: None,
165 used_ancestor_index: None,
166 filtered_to: None,
167 }
168 }
169
170 #[must_use]
172 pub fn with_index_age(mut self, age_seconds: u64) -> Self {
173 self.index_age_seconds = Some(age_seconds);
174 self
175 }
176
177 #[must_use]
179 pub fn with_candidates(mut self, total: usize, filtered: usize) -> Self {
180 self.candidate_count = Some(total);
181 self.filtered_count = Some(filtered);
182 self
183 }
184
185 #[must_use]
187 pub fn with_scope_info(mut self, is_ancestor: bool, filtered_to: Option<String>) -> Self {
188 self.used_ancestor_index = Some(is_ancestor);
189 self.filtered_to = filtered_to;
190 self
191 }
192}
193
194#[derive(Debug, Clone, Serialize)]
196pub struct JsonResponse<T> {
197 pub query: QueryMeta,
199
200 pub stats: Stats,
202
203 pub results: Vec<T>,
205}
206
207impl<T> JsonResponse<T> {
208 #[must_use]
210 pub fn new(query: QueryMeta, stats: Stats, results: Vec<T>) -> Self {
211 Self {
212 query,
213 stats,
214 results,
215 }
216 }
217}
218
219#[derive(Debug, Clone, Serialize)]
221#[serde(tag = "event", rename_all = "snake_case")]
222pub enum StreamEvent<T> {
223 PartialResult {
225 result: T,
227 score: f64,
229 },
230
231 FinalSummary {
233 stats: Stats,
235 },
236}
237
238#[derive(Debug, Clone, Serialize)]
240pub struct IndexStatus {
241 pub exists: bool,
243
244 #[serde(skip_serializing_if = "Option::is_none")]
246 pub path: Option<String>,
247
248 #[serde(skip_serializing_if = "Option::is_none")]
250 pub created_at: Option<String>,
251
252 #[serde(skip_serializing_if = "Option::is_none")]
254 pub age_seconds: Option<u64>,
255
256 #[serde(skip_serializing_if = "Option::is_none")]
258 pub symbol_count: Option<usize>,
259
260 #[serde(skip_serializing_if = "Option::is_none")]
262 pub file_count: Option<usize>,
263
264 #[serde(skip_serializing_if = "Option::is_none")]
266 pub languages: Option<Vec<String>>,
267
268 pub supports_fuzzy: bool,
270
271 pub supports_relations: bool,
273
274 #[serde(skip_serializing_if = "Option::is_none")]
276 pub cross_language_relation_count: Option<usize>,
277
278 #[serde(skip_serializing_if = "Option::is_none")]
281 pub symbol_counts_by_kind: Option<HashMap<String, usize>>,
282
283 #[serde(skip_serializing_if = "Option::is_none")]
286 pub file_counts_by_language: Option<HashMap<String, usize>>,
287
288 #[serde(skip_serializing_if = "Option::is_none")]
291 pub relation_counts_by_pair: Option<HashMap<String, usize>>,
292
293 #[serde(skip_serializing_if = "Option::is_none")]
295 pub stale: Option<bool>,
296
297 #[serde(skip_serializing_if = "Option::is_none")]
299 pub building: Option<bool>,
300
301 #[serde(skip_serializing_if = "Option::is_none")]
303 pub build_age_seconds: Option<u64>,
304
305 #[serde(skip_serializing_if = "Option::is_none")]
317 pub aggregate: Option<crate::workspace::WorkspaceIndexStatus>,
318
319 #[serde(skip_serializing_if = "Option::is_none")]
328 pub partial: Option<bool>,
329}
330
331impl IndexStatus {
332 #[must_use]
334 pub fn not_found() -> Self {
335 Self {
336 exists: false,
337 path: None,
338 created_at: None,
339 age_seconds: None,
340 symbol_count: None,
341 file_count: None,
342 languages: None,
343 supports_fuzzy: false,
344 supports_relations: false,
345 cross_language_relation_count: None,
346 symbol_counts_by_kind: None,
347 file_counts_by_language: None,
348 relation_counts_by_pair: None,
349 stale: None,
350 building: None,
351 build_age_seconds: None,
352 aggregate: None,
353 partial: None,
354 }
355 }
356
357 #[must_use]
374 pub fn excluded() -> Self {
375 Self {
376 exists: false,
377 path: None,
378 created_at: None,
379 age_seconds: None,
380 symbol_count: None,
381 file_count: None,
382 languages: None,
383 supports_fuzzy: false,
384 supports_relations: false,
385 cross_language_relation_count: None,
386 symbol_counts_by_kind: None,
387 file_counts_by_language: None,
388 relation_counts_by_pair: None,
389 stale: None,
390 building: None,
391 build_age_seconds: None,
392 aggregate: None,
393 partial: None,
394 }
395 }
396
397 #[must_use]
437 pub fn aggregate(
438 member_path: std::path::PathBuf,
439 aggregate: crate::workspace::WorkspaceIndexStatus,
440 ) -> Self {
441 let exists =
442 aggregate.error_count == 0 && aggregate.missing_count == 0 && aggregate.ok_count > 0;
443 let building = if aggregate.building_count > 0 {
444 Some(true)
445 } else {
446 None
447 };
448 let partial = if aggregate.missing_count > 0 || aggregate.error_count > 0 {
449 Some(true)
450 } else {
451 None
452 };
453 let age_seconds = if aggregate.ok_count > 0 {
454 let now_secs = std::time::SystemTime::now()
455 .duration_since(std::time::UNIX_EPOCH)
456 .map_or(0, |d| d.as_secs());
457 let generated_secs = aggregate
458 .generated_at
459 .duration_since(std::time::UNIX_EPOCH)
460 .map_or(now_secs, |d| d.as_secs());
461 Some(now_secs.saturating_sub(generated_secs))
462 } else {
463 None
464 };
465 Self {
466 exists,
467 path: Some(member_path.display().to_string()),
468 created_at: None,
469 age_seconds,
470 symbol_count: None,
471 file_count: None,
472 languages: None,
473 supports_fuzzy: false,
474 supports_relations: false,
475 cross_language_relation_count: None,
476 symbol_counts_by_kind: None,
477 file_counts_by_language: None,
478 relation_counts_by_pair: None,
479 stale: None,
483 building,
484 build_age_seconds: None,
485 aggregate: Some(aggregate),
486 partial,
487 }
488 }
489
490 #[must_use]
492 pub fn from_index(path: String, created_at: String, age_seconds: u64) -> IndexStatusBuilder {
493 IndexStatusBuilder {
494 path,
495 created_at,
496 age_seconds,
497 symbol_count: 0,
498 file_count: None,
499 languages: Vec::new(),
500 has_relations: false,
501 has_trigram: false,
502 cross_language_relation_count: 0,
503 symbol_counts_by_kind: None,
504 file_counts_by_language: None,
505 relation_counts_by_pair: None,
506 building: None,
507 build_age_seconds: None,
508 }
509 }
510}
511
512pub struct IndexStatusBuilder {
514 path: String,
515 created_at: String,
516 age_seconds: u64,
517 symbol_count: usize,
518 file_count: Option<usize>,
519 languages: Vec<String>,
520 has_relations: bool,
521 has_trigram: bool,
522 cross_language_relation_count: usize,
523 symbol_counts_by_kind: Option<HashMap<String, usize>>,
524 file_counts_by_language: Option<HashMap<String, usize>>,
525 relation_counts_by_pair: Option<HashMap<String, usize>>,
526 building: Option<bool>,
527 build_age_seconds: Option<u64>,
528}
529
530impl IndexStatusBuilder {
531 #[must_use]
533 pub fn symbol_count(mut self, count: usize) -> Self {
534 self.symbol_count = count;
535 self
536 }
537
538 #[must_use]
540 pub fn file_count(mut self, count: usize) -> Self {
541 self.file_count = Some(count);
542 self
543 }
544
545 #[must_use]
547 pub fn file_count_opt(mut self, count: Option<usize>) -> Self {
548 self.file_count = count;
549 self
550 }
551
552 #[must_use]
554 pub fn languages(mut self, langs: Vec<String>) -> Self {
555 self.languages = langs;
556 self
557 }
558
559 #[must_use]
561 pub fn has_relations(mut self, value: bool) -> Self {
562 self.has_relations = value;
563 self
564 }
565
566 #[must_use]
568 pub fn has_trigram(mut self, value: bool) -> Self {
569 self.has_trigram = value;
570 self
571 }
572
573 #[must_use]
575 pub fn cross_language_relation_count(mut self, count: usize) -> Self {
576 self.cross_language_relation_count = count;
577 self
578 }
579
580 #[must_use]
582 pub fn symbol_counts_by_kind(mut self, counts: HashMap<String, usize>) -> Self {
583 self.symbol_counts_by_kind = Some(counts);
584 self
585 }
586
587 #[must_use]
589 pub fn file_counts_by_language(mut self, counts: HashMap<String, usize>) -> Self {
590 self.file_counts_by_language = Some(counts);
591 self
592 }
593
594 #[must_use]
596 pub fn relation_counts_by_pair(mut self, counts: HashMap<String, usize>) -> Self {
597 self.relation_counts_by_pair = Some(counts);
598 self
599 }
600
601 #[must_use]
603 pub fn building(mut self, value: bool) -> Self {
604 self.building = Some(value);
605 self
606 }
607
608 #[must_use]
610 pub fn build_age_seconds(mut self, value: u64) -> Self {
611 self.build_age_seconds = Some(value);
612 self
613 }
614
615 #[must_use]
617 pub fn build(self) -> IndexStatus {
618 let stale = self.age_seconds > 86400; IndexStatus {
620 exists: true,
621 path: Some(self.path),
622 created_at: Some(self.created_at),
623 age_seconds: Some(self.age_seconds),
624 symbol_count: Some(self.symbol_count),
625 file_count: self.file_count,
626 languages: Some(self.languages),
627 supports_fuzzy: self.has_trigram,
628 supports_relations: self.has_relations,
629 cross_language_relation_count: if self.cross_language_relation_count > 0 {
630 Some(self.cross_language_relation_count)
631 } else {
632 None
633 },
634 symbol_counts_by_kind: self.symbol_counts_by_kind,
635 file_counts_by_language: self.file_counts_by_language,
636 relation_counts_by_pair: self.relation_counts_by_pair,
637 stale: Some(stale),
638 building: self.building,
639 build_age_seconds: self.build_age_seconds,
640 aggregate: None,
641 partial: None,
642 }
643 }
644}
645
646#[cfg(test)]
647mod tests {
648 use super::*;
649 use serde_json;
650
651 #[test]
652 fn test_query_meta_serialization() {
653 let meta = QueryMeta::new(Some("handler".to_string()), Duration::from_millis(5));
654
655 let json = serde_json::to_string(&meta).unwrap();
656 assert!(json.contains("\"pattern\":\"handler\""));
657 assert!(json.contains("\"execution_time_ms\":5"));
658 }
659
660 #[test]
661 fn test_filters_empty() {
662 let filters = Filters::default();
663 assert!(filters.is_empty());
664
665 let json = serde_json::to_value(&filters).unwrap();
666 assert_eq!(json.as_object().unwrap().len(), 0);
667 }
668
669 #[test]
670 fn test_filters_with_values() {
671 let filters = Filters {
672 kind: Some("function".to_string()),
673 lang: Some("rust".to_string()),
674 ignore_case: true,
675 ..Default::default()
676 };
677
678 assert!(!filters.is_empty());
679 let json = serde_json::to_string(&filters).unwrap();
680 assert!(json.contains("\"kind\":\"function\""));
681 assert!(json.contains("\"lang\":\"rust\""));
682 assert!(json.contains("\"ignore_case\":true"));
683 }
684
685 #[test]
686 fn test_stats_truncation() {
687 let stats = Stats::new(100, 50);
688 assert_eq!(stats.total_matches, 100);
689 assert_eq!(stats.returned, 50);
690 assert!(stats.is_truncated);
691
692 let stats_not_truncated = Stats::new(50, 50);
693 assert!(!stats_not_truncated.is_truncated);
694 }
695
696 #[test]
697 fn test_json_response() {
698 let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(10));
699 let stats = Stats::new(5, 5);
700 let results = vec!["result1", "result2"];
701
702 let response = JsonResponse::new(meta, stats, results);
703
704 let json = serde_json::to_string(&response).unwrap();
705 assert!(json.contains("\"query\""));
706 assert!(json.contains("\"stats\""));
707 assert!(json.contains("\"results\""));
708 }
709
710 #[test]
711 fn test_stream_event_partial() {
712 let event = StreamEvent::PartialResult {
713 result: "test_symbol",
714 score: 0.95,
715 };
716
717 let json = serde_json::to_string(&event).unwrap();
718 assert!(json.contains("\"event\":\"partial_result\""));
719 assert!(json.contains("\"score\":0.95"));
720 }
721
722 #[test]
723 fn test_stream_event_final() {
724 let stats = Stats::new(42, 20);
725 let event: StreamEvent<()> = StreamEvent::FinalSummary { stats };
727
728 let json = serde_json::to_string(&event).unwrap();
729 assert!(json.contains("\"event\":\"final_summary\""));
730 assert!(json.contains("\"total_matches\":42"));
731 }
732
733 #[test]
734 fn test_query_meta_with_confidence() {
735 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
736
737 let confidence = ConfidenceMetadata {
738 level: ConfidenceLevel::AstOnly,
739 limitations: vec!["Missing rust-analyzer".to_string()],
740 unavailable_features: vec!["Type inference".to_string()],
741 };
742
743 let meta = QueryMeta::new(Some("test_pattern".to_string()), Duration::from_millis(10))
744 .with_confidence(confidence.clone());
745
746 assert!(meta.confidence.is_some());
747 let meta_confidence = meta.confidence.unwrap();
748 assert_eq!(meta_confidence.level, ConfidenceLevel::AstOnly);
749 assert_eq!(meta_confidence.limitations.len(), 1);
750 assert_eq!(meta_confidence.unavailable_features.len(), 1);
751 }
752
753 #[test]
754 fn test_query_meta_confidence_serialization() {
755 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
756
757 let confidence = ConfidenceMetadata {
758 level: ConfidenceLevel::Partial,
759 limitations: vec!["Limited accuracy".to_string()],
760 unavailable_features: vec![],
761 };
762
763 let meta = QueryMeta::new(Some("search_pattern".to_string()), Duration::from_millis(5))
764 .with_confidence(confidence);
765
766 let json = serde_json::to_string(&meta).unwrap();
767 assert!(json.contains("\"confidence\""));
768 assert!(json.contains("\"partial\""));
769 assert!(json.contains("\"limitations\""));
770 assert!(json.contains("\"Limited accuracy\""));
771 }
772
773 #[test]
774 fn test_query_meta_without_confidence() {
775 let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(5));
776
777 assert!(meta.confidence.is_none());
778
779 let json = serde_json::to_string(&meta).unwrap();
780 assert!(!json.contains("\"confidence\""));
782 }
783
784 #[test]
785 fn test_json_response_with_confidence() {
786 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
787
788 let confidence = ConfidenceMetadata {
789 level: ConfidenceLevel::Verified,
790 limitations: vec![],
791 unavailable_features: vec![],
792 };
793
794 let meta = QueryMeta::new(Some("pattern".to_string()), Duration::from_millis(8))
795 .with_confidence(confidence);
796 let stats = Stats::new(10, 10);
797 let results = vec!["result1", "result2"];
798
799 let response = JsonResponse::new(meta, stats, results);
800
801 let json = serde_json::to_string(&response).unwrap();
802 assert!(json.contains("\"confidence\""));
803 assert!(json.contains("\"verified\""));
804 }
805}