1use schemars::JsonSchema;
2use serde::{Deserialize, Serialize};
3
4#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
6pub struct IndexRequest {
7 pub path: String,
9 #[serde(default)]
11 pub project: Option<String>,
12 #[serde(default)]
14 pub include_patterns: Vec<String>,
15 #[serde(default)]
17 pub exclude_patterns: Vec<String>,
18 #[serde(default = "default_max_file_size")]
20 pub max_file_size: usize,
21}
22
23fn default_max_file_size() -> usize {
24 1_048_576 }
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
29#[serde(rename_all = "lowercase")]
30pub enum IndexingMode {
31 Full,
33 Incremental,
35}
36
37#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
39pub struct IndexResponse {
40 pub mode: IndexingMode,
42 pub files_indexed: usize,
44 pub chunks_created: usize,
46 pub embeddings_generated: usize,
48 pub duration_ms: u64,
50 #[serde(default)]
52 pub errors: Vec<String>,
53 #[serde(default)]
55 pub files_updated: usize,
56 #[serde(default)]
58 pub files_removed: usize,
59}
60
61#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
63pub struct QueryRequest {
64 pub query: String,
66 #[serde(default)]
68 pub path: Option<String>,
69 #[serde(default)]
71 pub project: Option<String>,
72 #[serde(default = "default_limit")]
74 pub limit: usize,
75 #[serde(default = "default_min_score")]
77 pub min_score: f32,
78 #[serde(default = "default_hybrid")]
80 pub hybrid: bool,
81}
82
83fn default_hybrid() -> bool {
84 true
85}
86
87fn default_limit() -> usize {
88 10
89}
90
91fn default_min_score() -> f32 {
92 0.7
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
97pub struct SearchResult {
98 pub file_path: String,
100 #[serde(default)]
102 pub root_path: Option<String>,
103 pub content: String,
105 pub score: f32,
107 pub vector_score: f32,
109 pub keyword_score: Option<f32>,
111 pub start_line: usize,
113 pub end_line: usize,
115 pub language: String,
117 pub project: Option<String>,
119}
120
121#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
123pub struct QueryResponse {
124 pub results: Vec<SearchResult>,
126 pub duration_ms: u64,
128 #[serde(default)]
130 pub threshold_used: f32,
131 #[serde(default)]
133 pub threshold_lowered: bool,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
138pub struct StatisticsRequest {}
139
140#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
142pub struct StatisticsResponse {
143 pub total_files: usize,
145 pub total_chunks: usize,
147 pub total_embeddings: usize,
149 pub database_size_bytes: u64,
151 pub language_breakdown: Vec<LanguageStats>,
153}
154
155#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
156pub struct LanguageStats {
157 pub language: String,
158 pub file_count: usize,
159 pub chunk_count: usize,
160}
161
162#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
164pub struct ClearRequest {}
165
166#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
168pub struct ClearResponse {
169 pub success: bool,
171 pub message: String,
173}
174
175#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
177pub struct IncrementalUpdateRequest {
178 pub path: String,
180 #[serde(default)]
182 pub project: Option<String>,
183 #[serde(default)]
185 pub include_patterns: Vec<String>,
186 #[serde(default)]
188 pub exclude_patterns: Vec<String>,
189}
190
191#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
193pub struct IncrementalUpdateResponse {
194 pub files_added: usize,
196 pub files_updated: usize,
198 pub files_removed: usize,
200 pub chunks_modified: usize,
202 pub duration_ms: u64,
204}
205
206#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
208pub struct AdvancedSearchRequest {
209 pub query: String,
211 #[serde(default)]
213 pub path: Option<String>,
214 #[serde(default)]
216 pub project: Option<String>,
217 #[serde(default = "default_limit")]
219 pub limit: usize,
220 #[serde(default = "default_min_score")]
222 pub min_score: f32,
223 #[serde(default)]
225 pub file_extensions: Vec<String>,
226 #[serde(default)]
228 pub languages: Vec<String>,
229 #[serde(default)]
231 pub path_patterns: Vec<String>,
232}
233
234#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
236pub struct SearchGitHistoryRequest {
237 pub query: String,
239 #[serde(default = "default_git_path")]
241 pub path: String,
242 #[serde(default)]
244 pub project: Option<String>,
245 #[serde(default)]
247 pub branch: Option<String>,
248 #[serde(default = "default_max_commits")]
250 pub max_commits: usize,
251 #[serde(default = "default_limit")]
253 pub limit: usize,
254 #[serde(default = "default_min_score")]
256 pub min_score: f32,
257 #[serde(default)]
259 pub author: Option<String>,
260 #[serde(default)]
262 pub since: Option<String>,
263 #[serde(default)]
265 pub until: Option<String>,
266 #[serde(default)]
268 pub file_pattern: Option<String>,
269}
270
271fn default_git_path() -> String {
272 ".".to_string()
273}
274
275fn default_max_commits() -> usize {
276 10
277}
278
279#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
281pub struct GitSearchResult {
282 pub commit_hash: String,
284 pub commit_message: String,
286 pub author: String,
288 pub author_email: String,
290 pub commit_date: i64,
292 pub score: f32,
294 pub vector_score: f32,
296 pub keyword_score: Option<f32>,
298 pub files_changed: Vec<String>,
300 pub diff_snippet: String,
302}
303
304#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
306pub struct SearchGitHistoryResponse {
307 pub results: Vec<GitSearchResult>,
309 pub commits_indexed: usize,
311 pub total_cached_commits: usize,
313 pub duration_ms: u64,
315}
316
317#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
323pub struct FindDefinitionRequest {
324 pub file_path: String,
326 pub line: usize,
328 pub column: usize,
330 #[serde(default)]
332 pub project: Option<String>,
333}
334
335impl FindDefinitionRequest {
336 pub fn validate(&self) -> Result<(), String> {
338 if self.file_path.is_empty() {
339 return Err("file_path cannot be empty".to_string());
340 }
341 if self.line == 0 {
342 return Err("line must be 1-based (cannot be 0)".to_string());
343 }
344 Ok(())
345 }
346}
347
348#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
350pub struct FindDefinitionResponse {
351 pub definition: Option<crate::relations::DefinitionResult>,
353 pub precision: String,
355 pub duration_ms: u64,
357}
358
359#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
361pub struct FindReferencesRequest {
362 pub file_path: String,
364 pub line: usize,
366 pub column: usize,
368 #[serde(default = "default_references_limit")]
370 pub limit: usize,
371 #[serde(default)]
373 pub project: Option<String>,
374 #[serde(default = "default_include_definition")]
376 pub include_definition: bool,
377}
378
379fn default_references_limit() -> usize {
380 100
381}
382
383fn default_include_definition() -> bool {
384 true
385}
386
387impl FindReferencesRequest {
388 pub fn validate(&self) -> Result<(), String> {
390 if self.file_path.is_empty() {
391 return Err("file_path cannot be empty".to_string());
392 }
393 if self.line == 0 {
394 return Err("line must be 1-based (cannot be 0)".to_string());
395 }
396 const MAX_LIMIT: usize = 10000;
397 if self.limit > MAX_LIMIT {
398 return Err(format!("limit too large: {} (max: {})", self.limit, MAX_LIMIT));
399 }
400 Ok(())
401 }
402}
403
404#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
406pub struct FindReferencesResponse {
407 pub symbol_name: Option<String>,
409 pub references: Vec<crate::relations::ReferenceResult>,
411 pub total_count: usize,
413 pub precision: String,
415 pub duration_ms: u64,
417}
418
419#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
421pub struct GetCallGraphRequest {
422 pub file_path: String,
424 pub line: usize,
426 pub column: usize,
428 #[serde(default = "default_call_graph_depth")]
430 pub depth: usize,
431 #[serde(default)]
433 pub project: Option<String>,
434 #[serde(default = "default_true")]
436 pub include_callers: bool,
437 #[serde(default = "default_true")]
439 pub include_callees: bool,
440}
441
442fn default_call_graph_depth() -> usize {
443 2
444}
445
446fn default_true() -> bool {
447 true
448}
449
450impl GetCallGraphRequest {
451 pub fn validate(&self) -> Result<(), String> {
453 if self.file_path.is_empty() {
454 return Err("file_path cannot be empty".to_string());
455 }
456 if self.line == 0 {
457 return Err("line must be 1-based (cannot be 0)".to_string());
458 }
459 const MAX_DEPTH: usize = 10;
460 if self.depth > MAX_DEPTH {
461 return Err(format!("depth too large: {} (max: {})", self.depth, MAX_DEPTH));
462 }
463 Ok(())
464 }
465}
466
467#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
469pub struct GetCallGraphResponse {
470 pub root_symbol: Option<crate::relations::SymbolInfo>,
472 pub callers: Vec<crate::relations::CallGraphNode>,
474 pub callees: Vec<crate::relations::CallGraphNode>,
476 pub precision: String,
478 pub duration_ms: u64,
480}
481
482#[derive(Debug, Clone, Serialize, Deserialize)]
484pub struct ChunkMetadata {
485 pub file_path: String,
487 #[serde(default)]
489 pub root_path: Option<String>,
490 pub project: Option<String>,
492 pub start_line: usize,
494 pub end_line: usize,
496 pub language: Option<String>,
498 pub extension: Option<String>,
500 pub file_hash: String,
502 pub indexed_at: i64,
504}
505
506impl IndexRequest {
511 pub fn validate(&self) -> Result<(), String> {
513 let path = std::path::Path::new(&self.path);
515 if !path.exists() {
516 return Err(format!("Path does not exist: {}", self.path));
517 }
518 if !path.is_dir() {
519 return Err(format!("Path is not a directory: {}", self.path));
520 }
521
522 let canonical = path
524 .canonicalize()
525 .map_err(|e| format!("Failed to canonicalize path: {}", e))?;
526
527 if !canonical.starts_with(
529 std::env::current_dir()
530 .unwrap_or_default()
531 .parent()
532 .unwrap_or(std::path::Path::new("/")),
533 ) {
534 }
536
537 const MAX_FILE_SIZE_LIMIT: usize = 100_000_000; if self.max_file_size > MAX_FILE_SIZE_LIMIT {
540 return Err(format!(
541 "max_file_size too large: {} bytes (max: {} bytes)",
542 self.max_file_size, MAX_FILE_SIZE_LIMIT
543 ));
544 }
545
546 if let Some(ref project) = self.project {
548 if project.is_empty() {
549 return Err("project name cannot be empty".to_string());
550 }
551 if project.len() > 256 {
552 return Err("project name too long (max 256 characters)".to_string());
553 }
554 }
555
556 Ok(())
557 }
558}
559
560impl QueryRequest {
561 pub fn validate(&self) -> Result<(), String> {
563 if self.query.trim().is_empty() {
565 return Err("query cannot be empty".to_string());
566 }
567
568 const MAX_QUERY_LENGTH: usize = 10_240; if self.query.len() > MAX_QUERY_LENGTH {
571 return Err(format!(
572 "query too long: {} bytes (max: {} bytes)",
573 self.query.len(),
574 MAX_QUERY_LENGTH
575 ));
576 }
577
578 if !(0.0..=1.0).contains(&self.min_score) {
580 return Err(format!(
581 "min_score must be between 0.0 and 1.0, got: {}",
582 self.min_score
583 ));
584 }
585
586 const MAX_LIMIT: usize = 1000;
588 if self.limit > MAX_LIMIT {
589 return Err(format!(
590 "limit too large: {} (max: {})",
591 self.limit, MAX_LIMIT
592 ));
593 }
594
595 if let Some(ref project) = self.project {
597 if project.is_empty() {
598 return Err("project name cannot be empty".to_string());
599 }
600 if project.len() > 256 {
601 return Err("project name too long (max 256 characters)".to_string());
602 }
603 }
604
605 Ok(())
606 }
607}
608
609impl AdvancedSearchRequest {
610 pub fn validate(&self) -> Result<(), String> {
612 let query_req = QueryRequest {
614 query: self.query.clone(),
615 path: None,
616 project: self.project.clone(),
617 limit: self.limit,
618 min_score: self.min_score,
619 hybrid: true,
620 };
621 query_req.validate()?;
622
623 for ext in &self.file_extensions {
625 if ext.is_empty() {
626 return Err("file extension cannot be empty".to_string());
627 }
628 if ext.len() > 20 {
629 return Err(format!(
630 "file extension too long: {} (max 20 characters)",
631 ext
632 ));
633 }
634 }
635
636 for lang in &self.languages {
638 if lang.is_empty() {
639 return Err("language name cannot be empty".to_string());
640 }
641 if lang.len() > 50 {
642 return Err(format!(
643 "language name too long: {} (max 50 characters)",
644 lang
645 ));
646 }
647 }
648
649 Ok(())
650 }
651}
652
653impl SearchGitHistoryRequest {
654 pub fn validate(&self) -> Result<(), String> {
656 if self.query.trim().is_empty() {
658 return Err("query cannot be empty".to_string());
659 }
660
661 const MAX_QUERY_LENGTH: usize = 10_240; if self.query.len() > MAX_QUERY_LENGTH {
663 return Err(format!(
664 "query too long: {} bytes (max: {} bytes)",
665 self.query.len(),
666 MAX_QUERY_LENGTH
667 ));
668 }
669
670 let path = std::path::Path::new(&self.path);
672 if !path.exists() {
673 return Err(format!("Path does not exist: {}", self.path));
674 }
675
676 if !(0.0..=1.0).contains(&self.min_score) {
678 return Err(format!(
679 "min_score must be between 0.0 and 1.0, got: {}",
680 self.min_score
681 ));
682 }
683
684 const MAX_LIMIT: usize = 1000;
686 if self.limit > MAX_LIMIT {
687 return Err(format!(
688 "limit too large: {} (max: {})",
689 self.limit, MAX_LIMIT
690 ));
691 }
692
693 const MAX_COMMITS_LIMIT: usize = 10000;
695 if self.max_commits > MAX_COMMITS_LIMIT {
696 return Err(format!(
697 "max_commits too large: {} (max: {})",
698 self.max_commits, MAX_COMMITS_LIMIT
699 ));
700 }
701
702 if let Some(ref project) = self.project {
704 if project.is_empty() {
705 return Err("project name cannot be empty".to_string());
706 }
707 if project.len() > 256 {
708 return Err("project name too long (max 256 characters)".to_string());
709 }
710 }
711
712 Ok(())
713 }
714}
715
716#[cfg(test)]
717mod tests;