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
306impl IndexStatus {
307 #[must_use]
309 pub fn not_found() -> Self {
310 Self {
311 exists: false,
312 path: None,
313 created_at: None,
314 age_seconds: None,
315 symbol_count: None,
316 file_count: None,
317 languages: None,
318 supports_fuzzy: false,
319 supports_relations: false,
320 cross_language_relation_count: None,
321 symbol_counts_by_kind: None,
322 file_counts_by_language: None,
323 relation_counts_by_pair: None,
324 stale: None,
325 building: None,
326 build_age_seconds: None,
327 }
328 }
329
330 #[must_use]
332 pub fn from_index(path: String, created_at: String, age_seconds: u64) -> IndexStatusBuilder {
333 IndexStatusBuilder {
334 path,
335 created_at,
336 age_seconds,
337 symbol_count: 0,
338 file_count: None,
339 languages: Vec::new(),
340 has_relations: false,
341 has_trigram: false,
342 cross_language_relation_count: 0,
343 symbol_counts_by_kind: None,
344 file_counts_by_language: None,
345 relation_counts_by_pair: None,
346 building: None,
347 build_age_seconds: None,
348 }
349 }
350}
351
352pub struct IndexStatusBuilder {
354 path: String,
355 created_at: String,
356 age_seconds: u64,
357 symbol_count: usize,
358 file_count: Option<usize>,
359 languages: Vec<String>,
360 has_relations: bool,
361 has_trigram: bool,
362 cross_language_relation_count: usize,
363 symbol_counts_by_kind: Option<HashMap<String, usize>>,
364 file_counts_by_language: Option<HashMap<String, usize>>,
365 relation_counts_by_pair: Option<HashMap<String, usize>>,
366 building: Option<bool>,
367 build_age_seconds: Option<u64>,
368}
369
370impl IndexStatusBuilder {
371 #[must_use]
373 pub fn symbol_count(mut self, count: usize) -> Self {
374 self.symbol_count = count;
375 self
376 }
377
378 #[must_use]
380 pub fn file_count(mut self, count: usize) -> Self {
381 self.file_count = Some(count);
382 self
383 }
384
385 #[must_use]
387 pub fn file_count_opt(mut self, count: Option<usize>) -> Self {
388 self.file_count = count;
389 self
390 }
391
392 #[must_use]
394 pub fn languages(mut self, langs: Vec<String>) -> Self {
395 self.languages = langs;
396 self
397 }
398
399 #[must_use]
401 pub fn has_relations(mut self, value: bool) -> Self {
402 self.has_relations = value;
403 self
404 }
405
406 #[must_use]
408 pub fn has_trigram(mut self, value: bool) -> Self {
409 self.has_trigram = value;
410 self
411 }
412
413 #[must_use]
415 pub fn cross_language_relation_count(mut self, count: usize) -> Self {
416 self.cross_language_relation_count = count;
417 self
418 }
419
420 #[must_use]
422 pub fn symbol_counts_by_kind(mut self, counts: HashMap<String, usize>) -> Self {
423 self.symbol_counts_by_kind = Some(counts);
424 self
425 }
426
427 #[must_use]
429 pub fn file_counts_by_language(mut self, counts: HashMap<String, usize>) -> Self {
430 self.file_counts_by_language = Some(counts);
431 self
432 }
433
434 #[must_use]
436 pub fn relation_counts_by_pair(mut self, counts: HashMap<String, usize>) -> Self {
437 self.relation_counts_by_pair = Some(counts);
438 self
439 }
440
441 #[must_use]
443 pub fn building(mut self, value: bool) -> Self {
444 self.building = Some(value);
445 self
446 }
447
448 #[must_use]
450 pub fn build_age_seconds(mut self, value: u64) -> Self {
451 self.build_age_seconds = Some(value);
452 self
453 }
454
455 #[must_use]
457 pub fn build(self) -> IndexStatus {
458 let stale = self.age_seconds > 86400; IndexStatus {
460 exists: true,
461 path: Some(self.path),
462 created_at: Some(self.created_at),
463 age_seconds: Some(self.age_seconds),
464 symbol_count: Some(self.symbol_count),
465 file_count: self.file_count,
466 languages: Some(self.languages),
467 supports_fuzzy: self.has_trigram,
468 supports_relations: self.has_relations,
469 cross_language_relation_count: if self.cross_language_relation_count > 0 {
470 Some(self.cross_language_relation_count)
471 } else {
472 None
473 },
474 symbol_counts_by_kind: self.symbol_counts_by_kind,
475 file_counts_by_language: self.file_counts_by_language,
476 relation_counts_by_pair: self.relation_counts_by_pair,
477 stale: Some(stale),
478 building: self.building,
479 build_age_seconds: self.build_age_seconds,
480 }
481 }
482}
483
484#[cfg(test)]
485mod tests {
486 use super::*;
487 use serde_json;
488
489 #[test]
490 fn test_query_meta_serialization() {
491 let meta = QueryMeta::new(Some("handler".to_string()), Duration::from_millis(5));
492
493 let json = serde_json::to_string(&meta).unwrap();
494 assert!(json.contains("\"pattern\":\"handler\""));
495 assert!(json.contains("\"execution_time_ms\":5"));
496 }
497
498 #[test]
499 fn test_filters_empty() {
500 let filters = Filters::default();
501 assert!(filters.is_empty());
502
503 let json = serde_json::to_value(&filters).unwrap();
504 assert_eq!(json.as_object().unwrap().len(), 0);
505 }
506
507 #[test]
508 fn test_filters_with_values() {
509 let filters = Filters {
510 kind: Some("function".to_string()),
511 lang: Some("rust".to_string()),
512 ignore_case: true,
513 ..Default::default()
514 };
515
516 assert!(!filters.is_empty());
517 let json = serde_json::to_string(&filters).unwrap();
518 assert!(json.contains("\"kind\":\"function\""));
519 assert!(json.contains("\"lang\":\"rust\""));
520 assert!(json.contains("\"ignore_case\":true"));
521 }
522
523 #[test]
524 fn test_stats_truncation() {
525 let stats = Stats::new(100, 50);
526 assert_eq!(stats.total_matches, 100);
527 assert_eq!(stats.returned, 50);
528 assert!(stats.is_truncated);
529
530 let stats_not_truncated = Stats::new(50, 50);
531 assert!(!stats_not_truncated.is_truncated);
532 }
533
534 #[test]
535 fn test_json_response() {
536 let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(10));
537 let stats = Stats::new(5, 5);
538 let results = vec!["result1", "result2"];
539
540 let response = JsonResponse::new(meta, stats, results);
541
542 let json = serde_json::to_string(&response).unwrap();
543 assert!(json.contains("\"query\""));
544 assert!(json.contains("\"stats\""));
545 assert!(json.contains("\"results\""));
546 }
547
548 #[test]
549 fn test_stream_event_partial() {
550 let event = StreamEvent::PartialResult {
551 result: "test_symbol",
552 score: 0.95,
553 };
554
555 let json = serde_json::to_string(&event).unwrap();
556 assert!(json.contains("\"event\":\"partial_result\""));
557 assert!(json.contains("\"score\":0.95"));
558 }
559
560 #[test]
561 fn test_stream_event_final() {
562 let stats = Stats::new(42, 20);
563 let event: StreamEvent<()> = StreamEvent::FinalSummary { stats };
565
566 let json = serde_json::to_string(&event).unwrap();
567 assert!(json.contains("\"event\":\"final_summary\""));
568 assert!(json.contains("\"total_matches\":42"));
569 }
570
571 #[test]
572 fn test_query_meta_with_confidence() {
573 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
574
575 let confidence = ConfidenceMetadata {
576 level: ConfidenceLevel::AstOnly,
577 limitations: vec!["Missing rust-analyzer".to_string()],
578 unavailable_features: vec!["Type inference".to_string()],
579 };
580
581 let meta = QueryMeta::new(Some("test_pattern".to_string()), Duration::from_millis(10))
582 .with_confidence(confidence.clone());
583
584 assert!(meta.confidence.is_some());
585 let meta_confidence = meta.confidence.unwrap();
586 assert_eq!(meta_confidence.level, ConfidenceLevel::AstOnly);
587 assert_eq!(meta_confidence.limitations.len(), 1);
588 assert_eq!(meta_confidence.unavailable_features.len(), 1);
589 }
590
591 #[test]
592 fn test_query_meta_confidence_serialization() {
593 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
594
595 let confidence = ConfidenceMetadata {
596 level: ConfidenceLevel::Partial,
597 limitations: vec!["Limited accuracy".to_string()],
598 unavailable_features: vec![],
599 };
600
601 let meta = QueryMeta::new(Some("search_pattern".to_string()), Duration::from_millis(5))
602 .with_confidence(confidence);
603
604 let json = serde_json::to_string(&meta).unwrap();
605 assert!(json.contains("\"confidence\""));
606 assert!(json.contains("\"partial\""));
607 assert!(json.contains("\"limitations\""));
608 assert!(json.contains("\"Limited accuracy\""));
609 }
610
611 #[test]
612 fn test_query_meta_without_confidence() {
613 let meta = QueryMeta::new(Some("test".to_string()), Duration::from_millis(5));
614
615 assert!(meta.confidence.is_none());
616
617 let json = serde_json::to_string(&meta).unwrap();
618 assert!(!json.contains("\"confidence\""));
620 }
621
622 #[test]
623 fn test_json_response_with_confidence() {
624 use crate::confidence::{ConfidenceLevel, ConfidenceMetadata};
625
626 let confidence = ConfidenceMetadata {
627 level: ConfidenceLevel::Verified,
628 limitations: vec![],
629 unavailable_features: vec![],
630 };
631
632 let meta = QueryMeta::new(Some("pattern".to_string()), Duration::from_millis(8))
633 .with_confidence(confidence);
634 let stats = Stats::new(10, 10);
635 let results = vec!["result1", "result2"];
636
637 let response = JsonResponse::new(meta, stats, results);
638
639 let json = serde_json::to_string(&response).unwrap();
640 assert!(json.contains("\"confidence\""));
641 assert!(json.contains("\"verified\""));
642 }
643}