use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::response::{IntoResponse, Response}; use axum::routing::{get, post}; use axum::{Json, Router}; use export::rest::{AppResponseData, NoData}; use serde::Deserialize; use jwt::{Session, SessionUser}; use utoipa::{IntoParams, OpenApi, ToSchema}; use uuid::Uuid; use ::search::{SearchError, SearchPage, SearchQuery, SearchService}; #[derive(Debug, Clone, Deserialize, IntoParams)] pub struct SymbolQuery { pub q: String, pub limit: Option, pub offset: Option, } #[derive(Debug, Clone, Deserialize, ToSchema)] pub struct ReindexCommand { pub repo_id: Option, #[serde(default)] pub force_full: bool, } #[derive(Debug)] pub struct SearchApiError(SearchError); impl From for SearchApiError { fn from(value: SearchError) -> Self { Self(value) } } impl IntoResponse for SearchApiError { fn into_response(self) -> Response { let (status, message, code) = match &self.0 { SearchError::Disabled => ( StatusCode::SERVICE_UNAVAILABLE, "search is unavailable", "search_disabled", ), SearchError::Invalid(_) => ( StatusCode::BAD_REQUEST, "invalid search request", "invalid_search_query", ), SearchError::Task { .. } | SearchError::Remote { .. } | SearchError::TimedOut => ( StatusCode::SERVICE_UNAVAILABLE, "search is temporarily unavailable", "search_unavailable", ), _ => ( StatusCode::INTERNAL_SERVER_ERROR, "search request failed", "search_failed", ), }; ( status, AppResponseData::::err(status.as_u16() as i32, message) .with_error(self.0.to_string()) .with_error_code(code), ) .into_response() } } #[utoipa::path( get, path = "/search", tag = "search", description = "全局检索当前调用者可见的代码、符号、议题、合并请求与聊天消息。", params(SearchQuery), responses((status = 200, description = "search results", body = AppResponseData)) )] pub async fn global_search( State(service): State, session: Session, Query(query): Query, ) -> Result, SearchApiError> { Ok(AppResponseData::ok( service.search(session.uid(), query).await?, )) } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/symbols", tag = "search", description = "检索仓库默认分支中已索引的符号。", params(git::handler::RepoPath, SymbolQuery), responses((status = 200, description = "symbol results", body = AppResponseData)) )] pub async fn repo_symbols( State(service): State, session: Session, Path(path): Path, Query(query): Query, ) -> Result, SearchApiError> { Ok(AppResponseData::ok( service .symbols( session.uid(), path.space_name, path.repo_name, query.q, query.limit, query.offset, ) .await?, )) } #[utoipa::path( get, path = "/repos/{space_name}/{repo_name}/symbols/{symbol_name}/references", tag = "search", description = "返回仓库内同名符号的词法引用与定义候选。", params(git::handler::RepoPath, SymbolQuery), responses((status = 200, description = "symbol references", body = AppResponseData)) )] pub async fn symbol_references( State(service): State, session: Session, Path((space_name, repo_name, symbol_name)): Path<(String, String, String)>, Query(query): Query, ) -> Result, SearchApiError> { let name = if query.q.trim().is_empty() { symbol_name } else { query.q }; Ok(AppResponseData::ok( service .references( session.uid(), space_name, repo_name, name, query.limit, query.offset, ) .await?, )) } pub async fn reindex( State(service): State, Json(command): Json, ) -> Result, SearchApiError> { if command.force_full { ::search::outbox::clear_repo_state(service.database().write(), command.repo_id) .await .map_err(SearchError::from)?; } if let Some(repo_id) = command.repo_id { ::search::outbox::enqueue_repo(service.database().write(), repo_id) .await .map_err(SearchError::from)?; } else { service.queue_all().await?; } Ok(AppResponseData::empty()) } pub fn router() -> Router { Router::new() .route("/search", get(global_search)) .route("/repos/{space_name}/{repo_name}/symbols", get(repo_symbols)) .route( "/repos/{space_name}/{repo_name}/symbols/{symbol_name}/references", get(symbol_references), ) } pub fn internal_router() -> Router { Router::new().route("/search/reindex", post(reindex)) } #[derive(OpenApi)] #[openapi( paths(global_search, repo_symbols, symbol_references), components(schemas(SearchPage, ::search::SearchHit, ::search::SearchKind)) )] pub struct SearchApiDoc;