pub mod daemon_active_artifacts;
pub mod daemon_cancel_rebuild;
pub mod daemon_load;
pub mod daemon_rebuild;
pub mod daemon_reset;
pub mod daemon_status;
pub mod daemon_stop;
pub mod daemon_unload;
pub mod daemon_workspace_status;
pub(crate) mod tool_dispatch;
use std::sync::Arc;
use serde_json::json;
use sqry_core::query::executor::QueryExecutor;
use thiserror::Error;
use tokio::task::JoinError;
use tokio_util::sync::CancellationToken;
use crate::config::DaemonConfig;
use crate::error::DaemonError;
use crate::rebuild::RebuildDispatcher;
use crate::workspace::{WorkspaceBuilder, WorkspaceManager};
use super::protocol::{JsonRpcId, JsonRpcPayload, JsonRpcRequest, JsonRpcResponse};
use super::shim_registry::ShimRegistry;
use sqry_daemon_protocol::LogicalWorkspaceWire;
#[derive(Clone)]
pub(crate) struct HandlerContext {
pub manager: Arc<WorkspaceManager>,
#[allow(dead_code)] pub dispatcher: Arc<RebuildDispatcher>,
pub workspace_builder: Arc<dyn WorkspaceBuilder>,
#[allow(dead_code)] pub tool_executor: Arc<QueryExecutor>,
pub shim_registry: Arc<ShimRegistry>,
pub shutdown: CancellationToken,
pub config: Arc<DaemonConfig>,
pub daemon_version: &'static str,
}
impl std::fmt::Debug for HandlerContext {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("HandlerContext")
.field("daemon_version", &self.daemon_version)
.finish_non_exhaustive()
}
}
#[derive(Debug, Clone, Default)]
pub(crate) struct ConnectionState {
pub connection_logical_workspace: Option<LogicalWorkspaceWire>,
}
impl ConnectionState {
#[allow(dead_code)] pub fn empty() -> Self {
Self::default()
}
pub fn from_hello(binding: Option<LogicalWorkspaceWire>) -> Self {
Self {
connection_logical_workspace: binding,
}
}
}
#[derive(Debug, Error)]
pub enum MethodError {
#[error("method not found: {0}")]
MethodNotFound(String),
#[error("invalid params: {0}")]
InvalidParams(#[source] serde_json::Error),
#[error("invalid params: {message}")]
InvalidParamsStructured {
message: String,
kind: String,
retryable: bool,
retry_after_ms: Option<u64>,
details: Option<serde_json::Value>,
},
#[error("invalid request: {0}")]
#[allow(dead_code)] InvalidRequest(String),
#[error(transparent)]
Daemon(#[from] DaemonError),
#[error("internal: {0}")]
Internal(#[from] anyhow::Error),
#[error("spawn_blocking join error: {0}")]
JoinError(#[from] JoinError),
}
impl MethodError {
#[must_use]
pub fn into_jsonrpc_response(self, id: Option<JsonRpcId>) -> JsonRpcResponse {
let (code, message, data) = match self {
Self::MethodNotFound(ref m) => (
-32601,
"Method not found".to_string(),
Some(json!({ "method": m })),
),
Self::InvalidParams(e) => (
-32602,
"Invalid params".to_string(),
Some(json!({ "reason": e.to_string() })),
),
Self::InvalidParamsStructured {
message,
kind,
retryable,
retry_after_ms,
details,
} => (
-32602,
message,
Some(json!({
"kind": kind,
"retryable": retryable,
"retry_after_ms": retry_after_ms,
"details": details,
})),
),
Self::InvalidRequest(ref reason) => (
-32600,
"Invalid Request".to_string(),
Some(json!({ "reason": reason })),
),
Self::Daemon(d) => match d.jsonrpc_code() {
Some(code) => (code, d.to_string(), d.error_data()),
None => (
-32603,
"Internal error".to_string(),
Some(json!({ "reason": d.to_string() })),
),
},
Self::Internal(e) => (
-32603,
"Internal error".to_string(),
Some(json!({ "reason": e.to_string() })),
),
Self::JoinError(e) => (
-32603,
"Internal error".to_string(),
Some(json!({ "reason": format!("blocking task join: {e}") })),
),
};
JsonRpcResponse::error(id, code, message, data)
}
}
#[must_use]
pub fn format_panic_payload(join_err: JoinError) -> String {
if join_err.is_panic() {
let any = join_err.into_panic();
if let Some(s) = any.downcast_ref::<&'static str>() {
return (*s).to_owned();
}
if let Some(s) = any.downcast_ref::<String>() {
return s.clone();
}
return "<non-string panic payload>".to_owned();
}
format!("task join failure: {join_err}")
}
pub(crate) async fn dispatch(
ctx: &HandlerContext,
conn: &ConnectionState,
req: JsonRpcRequest,
) -> Option<JsonRpcResponse> {
let id = req.id.clone()?;
let result = match req.method.as_str() {
"daemon/status" => daemon_status::handle(ctx, req.params).await,
"daemon/load" => daemon_load::handle(ctx, conn, req.params).await,
"daemon/unload" => daemon_unload::handle(ctx, req.params).await,
"daemon/stop" => daemon_stop::handle(ctx, req.params).await,
"daemon/rebuild" => daemon_rebuild::handle(ctx, req.params).await,
"daemon/cancel_rebuild" => daemon_cancel_rebuild::handle(ctx, req.params).await,
"daemon/workspaceStatus" => daemon_workspace_status::handle(ctx, req.params).await,
"daemon/active-artifacts" => daemon_active_artifacts::handle(ctx, req.params).await,
"daemon/reset" => daemon_reset::handle(ctx, req.params).await,
"semantic_search" | "relation_query" | "direct_callers" | "direct_callees"
| "find_unused" | "find_cycles" | "is_node_in_cycle" | "trace_path" | "subgraph"
| "export_graph" | "complexity_metrics" | "semantic_diff" | "dependency_impact"
| "show_dependencies" => {
tool_dispatch::dispatch_tool(ctx, req.method.as_str(), req.params).await
}
other => Err(MethodError::MethodNotFound(other.to_owned())),
};
Some(match result {
Ok(envelope_json) => JsonRpcResponse {
jsonrpc: super::protocol::JsonRpcVersion,
id: Some(id),
payload: JsonRpcPayload::Success {
result: envelope_json,
},
},
Err(e) => e.into_jsonrpc_response(Some(id)),
})
}
#[must_use]
pub(crate) fn internal_error_response(id: Option<JsonRpcId>, reason: &str) -> JsonRpcResponse {
JsonRpcResponse::error(
id,
-32603,
"Internal error",
Some(json!({ "reason": reason })),
)
}