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}