use serde_json::{Map, Number, Value};
use std::fmt;
const KIND_VALIDATION: &str = "validation_error";
const KIND_DEADLINE_EXCEEDED: &str = "deadline_exceeded";
pub const CODE_ONNX_RUNTIME_MISSING: &str = "ONNX_RUNTIME_MISSING";
#[allow(dead_code)]
pub const KIND_QUERY_TOO_BROAD: &str = "query_too_broad";
#[allow(dead_code)]
pub const QUERY_TOO_BROAD_DOC_URL: &str = "https://docs.verivus.dev/sqry/query-cost-gate";
#[derive(Debug, Clone)]
pub struct RpcError {
pub code: i32,
pub message: String,
pub kind: String,
pub retryable: bool,
pub retry_after_ms: Option<u64>,
pub details: Option<Value>,
}
impl RpcError {
pub fn validation(message: impl Into<String>) -> Self {
Self {
code: -32602,
message: message.into(),
kind: KIND_VALIDATION.to_string(),
retryable: false,
retry_after_ms: None,
details: None,
}
}
pub fn validation_with_data(message: impl Into<String>, data: Value) -> Self {
Self {
code: -32602,
message: message.into(),
kind: KIND_VALIDATION.to_string(),
retryable: false,
retry_after_ms: None,
details: Some(data),
}
}
#[must_use]
pub fn onnx_runtime_missing(hint: impl Into<String>) -> Self {
let hint_str = hint.into();
let mut detail_map = Map::new();
detail_map.insert(
"code".to_string(),
Value::String(CODE_ONNX_RUNTIME_MISSING.to_string()),
);
detail_map.insert("message".to_string(), Value::String(hint_str.clone()));
detail_map.insert("retriable".to_string(), Value::Bool(false));
Self {
code: -32603,
message: format!("ONNX Runtime not found: {hint_str}"),
kind: CODE_ONNX_RUNTIME_MISSING.to_string(),
retryable: false,
retry_after_ms: None,
details: Some(Value::Object(detail_map)),
}
}
#[must_use]
#[allow(dead_code)]
pub fn query_too_broad(message: impl Into<String>, details: Value) -> Self {
Self {
code: -32602,
message: message.into(),
kind: KIND_QUERY_TOO_BROAD.to_string(),
retryable: false,
retry_after_ms: None,
details: Some(details),
}
}
pub fn deadline_exceeded(tool: &str, deadline_ms: u64, retry_delay_ms: u64) -> Self {
let mut detail_map = Map::new();
detail_map.insert("tool".to_string(), Value::String(tool.to_string()));
detail_map.insert(
"deadline_ms".to_string(),
Value::Number(Number::from(deadline_ms)),
);
Self {
code: -32000,
message: format!("Tool '{tool}' exceeded deadline of {deadline_ms}ms"),
kind: KIND_DEADLINE_EXCEEDED.to_string(),
retryable: true,
retry_after_ms: Some(retry_delay_ms),
details: Some(Value::Object(detail_map)),
}
}
}
impl fmt::Display for RpcError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{} ({})", self.message, self.kind)
}
}
impl std::error::Error for RpcError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn query_too_broad_envelope_has_canonical_kind_and_code() {
let details = serde_json::json!({
"source": "static_estimate",
"kind": "query_too_broad",
"limit": 50_000,
"doc_url": QUERY_TOO_BROAD_DOC_URL,
});
let err = RpcError::query_too_broad("rejected: scope filter required", details);
assert_eq!(err.code, -32602);
assert_eq!(err.kind, KIND_QUERY_TOO_BROAD);
assert_eq!(err.kind, "query_too_broad");
assert!(!err.retryable);
assert!(err.retry_after_ms.is_none());
let payload = err.details.expect("query_too_broad must carry details");
assert_eq!(payload["source"], "static_estimate");
assert_eq!(payload["kind"], "query_too_broad");
assert_eq!(payload["limit"], 50_000);
assert_eq!(payload["doc_url"], QUERY_TOO_BROAD_DOC_URL);
}
}