Skip to main content

dig_rpc/
handler.rs

1//! The [`RpcHandler`] trait — the seam a DIG node implements.
2//!
3//! `dig-rpc` owns the transport, the JSON-RPC envelope, the method-known /
4//! tier / allowlist boundary, and rate limiting. It does NOT know how to serve
5//! content, resolve a chain-anchored root, or read a store — the consuming node
6//! (the digstore `dig-node` crate, the standalone binary) supplies that via
7//! this one small async trait. This is why `dig-rpc` depends only on
8//! [`dig_rpc_protocol`], never on a node/service crate.
9//!
10//! A handler receives a **resolved** [`Method`] plus its raw params `Value` and
11//! returns either a result `Value` or a canonical [`RpcError`]. By the time the
12//! handler is called, the dispatcher has already:
13//!
14//! 1. rejected unknown methods with `-32601`;
15//! 2. enforced the [`Tier`](dig_rpc_protocol::Tier) boundary (a non-allowlisted method on the peer
16//!    surface is `-32601`; a control method off the loopback surface is
17//!    `-32030`);
18//! 3. applied rate limiting.
19//!
20//! So a handler implements only the *method semantics*, not the boundary.
21
22use async_trait::async_trait;
23use dig_rpc_protocol::{ErrorCode, Method, RpcError};
24use serde_json::Value;
25
26/// The node behind the RPC server.
27///
28/// Implemented by the DIG node; consumed by [`RpcServer`](crate::RpcServer).
29#[async_trait]
30pub trait RpcHandler: Send + Sync + 'static {
31    /// Handle a resolved method call, returning a result value or a canonical
32    /// error. `params` is the raw JSON-RPC `params` (`Null` when absent) — the
33    /// handler deserializes it into the method's params type from
34    /// [`dig_rpc_protocol::types`].
35    ///
36    /// The default implementation rejects every method with `-32601`, so a
37    /// handler need only override the methods it actually serves (its profile).
38    async fn handle(&self, method: Method, params: Value) -> Result<Value, RpcError> {
39        let _ = params;
40        Err(RpcError::of(
41            ErrorCode::MethodNotFound,
42            format!("method {} not implemented by this node", method.name()),
43        ))
44    }
45
46    /// Liveness probe backing the HTTP `GET /healthz` route. `Ok(())` ⇒ the
47    /// node can serve. Default: always healthy.
48    async fn healthz(&self) -> Result<(), RpcError> {
49        Ok(())
50    }
51
52    /// The node's software/API version, embedded in the generated OpenRPC
53    /// document served by `rpc.discover`. Default: this crate's version.
54    fn version(&self) -> String {
55        env!("CARGO_PKG_VERSION").to_string()
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62
63    struct Blank;
64    impl RpcHandler for Blank {}
65
66    /// **Proves:** the default handler rejects any method with `-32601` and a
67    /// message naming the method — so an unimplemented profile method is a
68    /// clean method-not-found, not a panic.
69    #[tokio::test]
70    async fn default_handler_rejects_with_method_not_found() {
71        let h = Blank;
72        let err = h.handle(Method::GetContent, Value::Null).await.unwrap_err();
73        assert_eq!(err.code, ErrorCode::MethodNotFound);
74        assert!(err.message.contains("dig.getContent"));
75    }
76
77    /// **Proves:** the default `healthz` reports healthy and `version` returns
78    /// a non-empty string.
79    #[tokio::test]
80    async fn defaults() {
81        let h = Blank;
82        assert!(h.healthz().await.is_ok());
83        assert!(!h.version().is_empty());
84    }
85}