use std::sync::Arc;
use dig_rpc_protocol::{
envelope::{JsonRpcRequest, JsonRpcResponse, RequestId},
openrpc, ErrorCode, ErrorOrigin, Method, RpcError, Tier,
};
use serde_json::Value;
use crate::handler::RpcHandler;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Surface {
Loopback,
PublicRead,
Peer,
}
impl Surface {
pub const fn discriminant(self) -> u8 {
match self {
Surface::Loopback => 0,
Surface::PublicRead => 1,
Surface::Peer => 2,
}
}
fn allows(self, method: Method) -> bool {
match self {
Surface::Loopback => true,
Surface::PublicRead => method.tier() == Tier::PublicRead,
Surface::Peer => method.is_peer_reachable(),
}
}
fn rejection(self, method: Method) -> RpcError {
if method.tier() == Tier::Control && self != Surface::Loopback {
RpcError::new(
ErrorCode::Unauthorized,
format!(
"{} is a control method; reachable only on the loopback surface",
method.name()
),
ErrorOrigin::Control,
)
} else {
RpcError::of(
ErrorCode::MethodNotFound,
format!("method {} not available on this surface", method.name()),
)
}
}
}
pub async fn dispatch<H: RpcHandler + ?Sized>(
handler: &H,
surface: Surface,
req: JsonRpcRequest<Value>,
) -> JsonRpcResponse<Value> {
let id = req.id.clone();
let Some(method) = Method::from_name(&req.method) else {
return JsonRpcResponse::error(
id,
RpcError::of(
ErrorCode::MethodNotFound,
format!("method {:?} not found", req.method),
),
);
};
if !surface.allows(method) {
return JsonRpcResponse::error(id, surface.rejection(method));
}
if method == Method::RpcDiscover {
return JsonRpcResponse::success(id, openrpc::openrpc_document(&handler.version()));
}
let params = req.params.unwrap_or(Value::Null);
match handler.handle(method, params).await {
Ok(result) => JsonRpcResponse::success(id, result),
Err(err) => JsonRpcResponse::error(id, err),
}
}
pub fn parse_error_response(id: RequestId, message: impl Into<String>) -> JsonRpcResponse<Value> {
JsonRpcResponse::error(id, RpcError::of(ErrorCode::ParseError, message))
}
pub type SharedHandler = Arc<dyn RpcHandler>;
#[cfg(test)]
mod tests {
use super::*;
use async_trait::async_trait;
use dig_rpc_protocol::envelope::JsonRpcResponseBody;
use serde_json::json;
struct Echo;
#[async_trait]
impl RpcHandler for Echo {
async fn handle(&self, method: Method, _params: Value) -> Result<Value, RpcError> {
Ok(json!({ "method": method.name() }))
}
}
fn req(method: &str) -> JsonRpcRequest<Value> {
JsonRpcRequest::new(1, method, json!({}))
}
fn err_of(resp: &JsonRpcResponse<Value>) -> &RpcError {
match &resp.body {
JsonRpcResponseBody::Error { error } => error,
_ => panic!("expected error, got {resp:?}"),
}
}
#[tokio::test]
async fn unknown_method_not_found() {
let resp = dispatch(&Echo, Surface::Loopback, req("dig.nope")).await;
assert_eq!(err_of(&resp).code, ErrorCode::MethodNotFound);
assert_eq!(resp.id, RequestId::Num(1));
}
#[tokio::test]
async fn control_method_gated_to_loopback() {
let ok = dispatch(&Echo, Surface::Loopback, req("cache.clear")).await;
assert!(matches!(ok.body, JsonRpcResponseBody::Success { .. }));
for surface in [Surface::Peer, Surface::PublicRead] {
let resp = dispatch(&Echo, surface, req("cache.clear")).await;
assert_eq!(err_of(&resp).code, ErrorCode::Unauthorized, "{surface:?}");
assert_eq!(err_of(&resp).data.origin, ErrorOrigin::Control);
}
}
#[tokio::test]
async fn public_read_not_on_peer_unless_allowlisted() {
let resp = dispatch(&Echo, Surface::Peer, req("dig.getManifest")).await;
assert_eq!(err_of(&resp).code, ErrorCode::MethodNotFound);
let ok = dispatch(&Echo, Surface::Peer, req("dig.getContent")).await;
assert!(matches!(ok.body, JsonRpcResponseBody::Success { .. }));
}
#[tokio::test]
async fn anchored_reads_served_on_peer() {
for m in [
"dig.getAnchoredRoot",
"dig.getCollection",
"dig.listCollectionItems",
] {
let resp = dispatch(&Echo, Surface::Peer, req(m)).await;
assert!(
matches!(resp.body, JsonRpcResponseBody::Success { .. }),
"{m}"
);
}
}
#[tokio::test]
async fn module_pull_methods_served_on_peer() {
for m in ["dig.getModuleInfo", "dig.fetchModuleRange"] {
let resp = dispatch(&Echo, Surface::Peer, req(m)).await;
assert!(
matches!(resp.body, JsonRpcResponseBody::Success { .. }),
"{m}"
);
}
}
#[tokio::test]
async fn rpc_discover_served_from_generator() {
let resp = dispatch(&Echo, Surface::Loopback, req("rpc.discover")).await;
match resp.body {
JsonRpcResponseBody::Success { result } => {
assert_eq!(result["openrpc"], "1.2.6");
assert!(result.get("methods").is_some());
}
_ => panic!("expected discovery document"),
}
let peer = dispatch(&Echo, Surface::Peer, req("rpc.discover")).await;
assert_eq!(err_of(&peer).code, ErrorCode::Unauthorized);
}
#[tokio::test]
async fn handler_error_propagates() {
struct Failing;
#[async_trait]
impl RpcHandler for Failing {
async fn handle(&self, _m: Method, _p: Value) -> Result<Value, RpcError> {
Err(RpcError::of(ErrorCode::RootNotAnchored, "stale root"))
}
}
let resp = dispatch(&Failing, Surface::Loopback, req("dig.getContent")).await;
assert_eq!(err_of(&resp).code, ErrorCode::RootNotAnchored);
assert_eq!(err_of(&resp).data.code, "ROOT_NOT_ANCHORED");
}
#[test]
fn parse_error_shape() {
let resp = parse_error_response(RequestId::Null, "bad json");
assert_eq!(err_of(&resp).code, ErrorCode::ParseError);
}
}