use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IndexRequest {
pub path: String,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub include_patterns: Vec<String>,
#[serde(default)]
pub exclude_patterns: Vec<String>,
#[serde(default = "default_max_file_size")]
pub max_file_size: usize,
}
fn default_max_file_size() -> usize {
1_048_576 }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "lowercase")]
pub enum IndexingMode {
Full,
Incremental,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IndexResponse {
pub mode: IndexingMode,
pub files_indexed: usize,
pub chunks_created: usize,
pub embeddings_generated: usize,
pub duration_ms: u64,
#[serde(default)]
pub errors: Vec<String>,
#[serde(default)]
pub files_updated: usize,
#[serde(default)]
pub files_removed: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct QueryRequest {
pub query: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub project: Option<String>,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default = "default_min_score")]
pub min_score: f32,
#[serde(default = "default_hybrid")]
pub hybrid: bool,
}
fn default_hybrid() -> bool {
true
}
fn default_limit() -> usize {
10
}
fn default_min_score() -> f32 {
0.7
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchResult {
pub file_path: String,
#[serde(default)]
pub root_path: Option<String>,
pub content: String,
pub score: f32,
pub vector_score: f32,
pub keyword_score: Option<f32>,
pub start_line: usize,
pub end_line: usize,
pub language: String,
pub project: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct QueryResponse {
pub results: Vec<SearchResult>,
pub duration_ms: u64,
#[serde(default)]
pub threshold_used: f32,
#[serde(default)]
pub threshold_lowered: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StatisticsRequest {}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct StatisticsResponse {
pub total_files: usize,
pub total_chunks: usize,
pub total_embeddings: usize,
pub database_size_bytes: u64,
pub language_breakdown: Vec<LanguageStats>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct LanguageStats {
pub language: String,
pub file_count: usize,
pub chunk_count: usize,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ClearRequest {}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct ClearResponse {
pub success: bool,
pub message: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IncrementalUpdateRequest {
pub path: String,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub include_patterns: Vec<String>,
#[serde(default)]
pub exclude_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct IncrementalUpdateResponse {
pub files_added: usize,
pub files_updated: usize,
pub files_removed: usize,
pub chunks_modified: usize,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct AdvancedSearchRequest {
pub query: String,
#[serde(default)]
pub path: Option<String>,
#[serde(default)]
pub project: Option<String>,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default = "default_min_score")]
pub min_score: f32,
#[serde(default)]
pub file_extensions: Vec<String>,
#[serde(default)]
pub languages: Vec<String>,
#[serde(default)]
pub path_patterns: Vec<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchGitHistoryRequest {
pub query: String,
#[serde(default = "default_git_path")]
pub path: String,
#[serde(default)]
pub project: Option<String>,
#[serde(default)]
pub branch: Option<String>,
#[serde(default = "default_max_commits")]
pub max_commits: usize,
#[serde(default = "default_limit")]
pub limit: usize,
#[serde(default = "default_min_score")]
pub min_score: f32,
#[serde(default)]
pub author: Option<String>,
#[serde(default)]
pub since: Option<String>,
#[serde(default)]
pub until: Option<String>,
#[serde(default)]
pub file_pattern: Option<String>,
}
fn default_git_path() -> String {
".".to_string()
}
fn default_max_commits() -> usize {
10
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GitSearchResult {
pub commit_hash: String,
pub commit_message: String,
pub author: String,
pub author_email: String,
pub commit_date: i64,
pub score: f32,
pub vector_score: f32,
pub keyword_score: Option<f32>,
pub files_changed: Vec<String>,
pub diff_snippet: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct SearchGitHistoryResponse {
pub results: Vec<GitSearchResult>,
pub commits_indexed: usize,
pub total_cached_commits: usize,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FindDefinitionRequest {
pub file_path: String,
pub line: usize,
pub column: usize,
#[serde(default)]
pub project: Option<String>,
}
impl FindDefinitionRequest {
pub fn validate(&self) -> Result<(), String> {
if self.file_path.is_empty() {
return Err("file_path cannot be empty".to_string());
}
if self.line == 0 {
return Err("line must be 1-based (cannot be 0)".to_string());
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FindDefinitionResponse {
pub definition: Option<crate::relations::DefinitionResult>,
pub precision: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FindReferencesRequest {
pub file_path: String,
pub line: usize,
pub column: usize,
#[serde(default = "default_references_limit")]
pub limit: usize,
#[serde(default)]
pub project: Option<String>,
#[serde(default = "default_include_definition")]
pub include_definition: bool,
}
fn default_references_limit() -> usize {
100
}
fn default_include_definition() -> bool {
true
}
impl FindReferencesRequest {
pub fn validate(&self) -> Result<(), String> {
if self.file_path.is_empty() {
return Err("file_path cannot be empty".to_string());
}
if self.line == 0 {
return Err("line must be 1-based (cannot be 0)".to_string());
}
const MAX_LIMIT: usize = 10000;
if self.limit > MAX_LIMIT {
return Err(format!("limit too large: {} (max: {})", self.limit, MAX_LIMIT));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct FindReferencesResponse {
pub symbol_name: Option<String>,
pub references: Vec<crate::relations::ReferenceResult>,
pub total_count: usize,
pub precision: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GetCallGraphRequest {
pub file_path: String,
pub line: usize,
pub column: usize,
#[serde(default = "default_call_graph_depth")]
pub depth: usize,
#[serde(default)]
pub project: Option<String>,
#[serde(default = "default_true")]
pub include_callers: bool,
#[serde(default = "default_true")]
pub include_callees: bool,
}
fn default_call_graph_depth() -> usize {
2
}
fn default_true() -> bool {
true
}
impl GetCallGraphRequest {
pub fn validate(&self) -> Result<(), String> {
if self.file_path.is_empty() {
return Err("file_path cannot be empty".to_string());
}
if self.line == 0 {
return Err("line must be 1-based (cannot be 0)".to_string());
}
const MAX_DEPTH: usize = 10;
if self.depth > MAX_DEPTH {
return Err(format!("depth too large: {} (max: {})", self.depth, MAX_DEPTH));
}
Ok(())
}
}
#[derive(Debug, Clone, Serialize, Deserialize, JsonSchema)]
pub struct GetCallGraphResponse {
pub root_symbol: Option<crate::relations::SymbolInfo>,
pub callers: Vec<crate::relations::CallGraphNode>,
pub callees: Vec<crate::relations::CallGraphNode>,
pub precision: String,
pub duration_ms: u64,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ChunkMetadata {
pub file_path: String,
#[serde(default)]
pub root_path: Option<String>,
pub project: Option<String>,
pub start_line: usize,
pub end_line: usize,
pub language: Option<String>,
pub extension: Option<String>,
pub file_hash: String,
pub indexed_at: i64,
}
impl IndexRequest {
pub fn validate(&self) -> Result<(), String> {
let path = std::path::Path::new(&self.path);
if !path.exists() {
return Err(format!("Path does not exist: {}", self.path));
}
if !path.is_dir() {
return Err(format!("Path is not a directory: {}", self.path));
}
let canonical = path
.canonicalize()
.map_err(|e| format!("Failed to canonicalize path: {}", e))?;
if !canonical.starts_with(
std::env::current_dir()
.unwrap_or_default()
.parent()
.unwrap_or(std::path::Path::new("/")),
) {
}
const MAX_FILE_SIZE_LIMIT: usize = 100_000_000; if self.max_file_size > MAX_FILE_SIZE_LIMIT {
return Err(format!(
"max_file_size too large: {} bytes (max: {} bytes)",
self.max_file_size, MAX_FILE_SIZE_LIMIT
));
}
if let Some(ref project) = self.project {
if project.is_empty() {
return Err("project name cannot be empty".to_string());
}
if project.len() > 256 {
return Err("project name too long (max 256 characters)".to_string());
}
}
Ok(())
}
}
impl QueryRequest {
pub fn validate(&self) -> Result<(), String> {
if self.query.trim().is_empty() {
return Err("query cannot be empty".to_string());
}
const MAX_QUERY_LENGTH: usize = 10_240; if self.query.len() > MAX_QUERY_LENGTH {
return Err(format!(
"query too long: {} bytes (max: {} bytes)",
self.query.len(),
MAX_QUERY_LENGTH
));
}
if !(0.0..=1.0).contains(&self.min_score) {
return Err(format!(
"min_score must be between 0.0 and 1.0, got: {}",
self.min_score
));
}
const MAX_LIMIT: usize = 1000;
if self.limit > MAX_LIMIT {
return Err(format!(
"limit too large: {} (max: {})",
self.limit, MAX_LIMIT
));
}
if let Some(ref project) = self.project {
if project.is_empty() {
return Err("project name cannot be empty".to_string());
}
if project.len() > 256 {
return Err("project name too long (max 256 characters)".to_string());
}
}
Ok(())
}
}
impl AdvancedSearchRequest {
pub fn validate(&self) -> Result<(), String> {
let query_req = QueryRequest {
query: self.query.clone(),
path: None,
project: self.project.clone(),
limit: self.limit,
min_score: self.min_score,
hybrid: true,
};
query_req.validate()?;
for ext in &self.file_extensions {
if ext.is_empty() {
return Err("file extension cannot be empty".to_string());
}
if ext.len() > 20 {
return Err(format!(
"file extension too long: {} (max 20 characters)",
ext
));
}
}
for lang in &self.languages {
if lang.is_empty() {
return Err("language name cannot be empty".to_string());
}
if lang.len() > 50 {
return Err(format!(
"language name too long: {} (max 50 characters)",
lang
));
}
}
Ok(())
}
}
impl SearchGitHistoryRequest {
pub fn validate(&self) -> Result<(), String> {
if self.query.trim().is_empty() {
return Err("query cannot be empty".to_string());
}
const MAX_QUERY_LENGTH: usize = 10_240; if self.query.len() > MAX_QUERY_LENGTH {
return Err(format!(
"query too long: {} bytes (max: {} bytes)",
self.query.len(),
MAX_QUERY_LENGTH
));
}
let path = std::path::Path::new(&self.path);
if !path.exists() {
return Err(format!("Path does not exist: {}", self.path));
}
if !(0.0..=1.0).contains(&self.min_score) {
return Err(format!(
"min_score must be between 0.0 and 1.0, got: {}",
self.min_score
));
}
const MAX_LIMIT: usize = 1000;
if self.limit > MAX_LIMIT {
return Err(format!(
"limit too large: {} (max: {})",
self.limit, MAX_LIMIT
));
}
const MAX_COMMITS_LIMIT: usize = 10000;
if self.max_commits > MAX_COMMITS_LIMIT {
return Err(format!(
"max_commits too large: {} (max: {})",
self.max_commits, MAX_COMMITS_LIMIT
));
}
if let Some(ref project) = self.project {
if project.is_empty() {
return Err("project name cannot be empty".to_string());
}
if project.len() > 256 {
return Err("project name too long (max 256 characters)".to_string());
}
}
Ok(())
}
}
#[cfg(test)]
mod tests;