use axum::Json;
use axum::extract::{Path, Query, State};
use axum::http::StatusCode;
use axum::response::{IntoResponse, Response};
use std::sync::Arc;
use crate::handlers::models::*;
use crate::models::AppState;
#[utoipa::path(
get,
path = "/api/repos/{id}/search",
tag = "Search",
params(
("id" = String, Path, description = "Repository ID"),
SearchParams,
),
responses(
(status = 200, description = "Search results", body = serde_json::Value),
(status = 400, description = "Missing or invalid query parameter", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
description = "Semantic + structural search. Find code by meaning, class name, method signature, or docstrings.",
)]
#[tracing::instrument(
name = "search",
skip_all,
fields(
repo_id = %id,
query_len = tracing::field::Empty,
max_results = tracing::field::Empty,
)
)]
pub async fn search_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Query(params): Query<SearchParams>,
) -> Response {
let query = match ¶ms.q {
Some(q) if !q.trim().is_empty() => q.as_str(),
_ => return error_response(StatusCode::BAD_REQUEST, "Missing required parameter 'q'"),
};
let max_results = params.max_results.unwrap_or(5);
let span = tracing::Span::current();
span.record("query_len", query.len());
span.record("max_results", max_results);
let embedder = match &state.embedder {
Some(e) => e,
None => {
return error_response(
StatusCode::INTERNAL_SERVER_ERROR,
"Embedding model not initialized",
);
}
};
match knot::cli_tools::run_search_hybrid_context(
query,
max_results,
Some(&id),
&state.vector_db,
&state.graph_db,
embedder,
)
.await
{
Ok(value) => (StatusCode::OK, Json(value)).into_response(),
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Search failed: {e}"),
),
}
}
#[utoipa::path(
get,
path = "/api/repos/{id}/callers",
tag = "Search",
params(
("id" = String, Path, description = "Repository ID"),
CallersParams,
),
responses(
(status = 200, description = "Caller analysis results", body = serde_json::Value),
(status = 400, description = "Missing or invalid query parameter", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
description = "Find all callers referencing a specific entity. Returns reverse dependency graph.",
)]
#[tracing::instrument(
name = "callers",
skip_all,
fields(repo_id = %id, entity = tracing::field::Empty)
)]
pub async fn callers_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Query(params): Query<CallersParams>,
) -> Response {
let entity_name = match ¶ms.entity {
Some(e) if !e.trim().is_empty() => e.as_str(),
_ => {
return error_response(
StatusCode::BAD_REQUEST,
"Missing required parameter 'entity'",
);
}
};
tracing::Span::current().record("entity", entity_name);
match knot::cli_tools::run_find_callers(entity_name, Some(&id), &state.graph_db).await {
Ok(value) => (StatusCode::OK, Json(value)).into_response(),
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Find callers failed: {e}"),
),
}
}
#[utoipa::path(
get,
path = "/api/repos/{id}/explore",
tag = "Search",
params(
("id" = String, Path, description = "Repository ID"),
ExploreParams,
),
responses(
(status = 200, description = "File exploration results", body = serde_json::Value),
(status = 400, description = "Missing or invalid query parameter", body = ErrorResponse),
(status = 404, description = "Repository not found", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
description = "Explore a file's architecture. Returns all classes, methods, and properties with signatures.",
)]
#[tracing::instrument(
name = "explore",
skip_all,
fields(repo_id = %id, path = tracing::field::Empty)
)]
pub async fn explore_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Query(params): Query<ExploreParams>,
) -> Response {
let relative = match ¶ms.path {
Some(p) if !p.trim().is_empty() => p.as_str(),
_ => return error_response(StatusCode::BAD_REQUEST, "Missing required parameter 'path'"),
};
tracing::Span::current().record("path", relative);
let relative_path = {
let mut registry = state.registry.lock().unwrap();
if registry.get(&id).is_none() {
return error_response(
StatusCode::NOT_FOUND,
format!("Repository '{}' not found", id),
);
}
relative.trim_start_matches('/').to_string()
};
match knot::cli_tools::run_explore_file(&relative_path, Some(&id), &state.graph_db).await {
Ok((_display_path, entities_json)) => (StatusCode::OK, Json(entities_json)).into_response(),
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Explore file failed: {e}"),
),
}
}
#[utoipa::path(
get,
path = "/api/repos/{id}/deps",
tag = "Search",
params(
("id" = String, Path, description = "Repository ID"),
DepsParams,
),
responses(
(status = 200, description = "Dependency lookup results", body = serde_json::Value),
(status = 400, description = "Missing or invalid query parameter", body = ErrorResponse),
(status = 500, description = "Internal server error", body = ErrorResponse),
),
description = "Cross-repository dependency lookup. Shows which repos depend on this one or vice versa.",
)]
#[tracing::instrument(
name = "deps",
skip_all,
fields(repo_id = %id, max_depth = tracing::field::Empty, reverse = tracing::field::Empty)
)]
pub async fn deps_handler(
State(state): State<Arc<AppState>>,
Path(id): Path<String>,
Query(params): Query<DepsParams>,
) -> Response {
let max_depth = params.max_depth.unwrap_or(3);
let reverse = params.reverse.unwrap_or(false);
let span = tracing::Span::current();
span.record("max_depth", max_depth);
span.record("reverse", reverse);
match knot::cli_tools::run_deps(&id, max_depth, reverse, &state.graph_db).await {
Ok(value) => (StatusCode::OK, Json(value)).into_response(),
Err(e) => error_response(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Deps lookup failed: {e}"),
),
}
}