use std::sync::Arc;
use futures::future::BoxFuture;
use serde_json::Value;
use super::host::BridgeError;
#[derive(Debug, Clone, PartialEq)]
pub struct EdgeFrame {
pub method: String,
pub params: Option<Value>,
pub relay: Option<super::hops::RelayContext>,
}
pub trait EdgeFarEnd: Send + Sync {
fn call(&self, frame: EdgeFrame) -> BoxFuture<'_, Result<Value, BridgeError>>;
}
pub type EdgeListener = Arc<dyn Fn(&str, Option<&Value>) + Send + Sync>;
pub trait EdgeTransport: Send + Sync {
fn call(&self, frame: EdgeFrame) -> BoxFuture<'_, Result<Value, BridgeError>>;
fn listen(&self, handler: EdgeListener) -> BoxFuture<'_, Box<dyn FnOnce() + Send>>;
}
#[derive(Clone)]
pub struct MemoryTransport {
far: Arc<dyn EdgeFarEnd>,
}
impl MemoryTransport {
pub fn new(far: Arc<dyn EdgeFarEnd>) -> Self {
Self { far }
}
}
impl EdgeTransport for MemoryTransport {
fn call(&self, frame: EdgeFrame) -> BoxFuture<'_, Result<Value, BridgeError>> {
self.far.call(frame)
}
fn listen(
&self,
_handler: Arc<dyn Fn(&str, Option<&Value>) + Send + Sync>,
) -> BoxFuture<'_, Box<dyn FnOnce() + Send>> {
Box::pin(async { Box::new(|| {}) as Box<dyn FnOnce() + Send> })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::relay::hops::{HopGuard, RelayContext};
use crate::relay::host::{BridgeError, RelayHost, RpcSink};
use futures::FutureExt;
use serde_json::json;
use std::sync::{Arc, Mutex};
struct SharedSink(Arc<dyn RpcSink>);
impl RpcSink for SharedSink {
fn call(
&self,
endpoint: &str,
method: &str,
params: Option<Value>,
) -> futures::future::BoxFuture<'_, Result<Value, BridgeError>> {
self.0.call(endpoint, method, params)
}
}
struct RelayFarEnd {
host: RelayHost,
sink: Arc<dyn RpcSink>,
guard: HopGuard,
forwarded: Mutex<u8>,
}
impl EdgeFarEnd for RelayFarEnd {
fn call(&self, frame: EdgeFrame) -> BoxFuture<'_, Result<Value, BridgeError>> {
let routed = self.host.forwarding_table().route(&frame.method);
if let crate::relay::Route::Forward(_) = routed {
if let Err(message) = self
.guard
.admit(frame.relay.as_ref().unwrap_or(&RelayContext::default()))
{
return Box::pin(async move {
Err(BridgeError::Forward {
endpoint: String::new(),
detail: message,
})
});
}
*self.forwarded.lock().unwrap() += 1;
let stepped = HopGuard::step(
frame.relay.as_ref().unwrap_or(&RelayContext::default()),
"next",
);
let sink = Arc::clone(&self.sink);
return Box::pin(async move {
sink.call("upstream", &frame.method, frame.params)
.await
.map(|mut v| {
if let Value::Object(map) = &mut v {
map.insert("hops".into(), json!(stepped.hops));
}
v
})
});
}
Box::pin(async move {
self.host
.dispatch(
&SharedSink(Arc::clone(&self.sink)),
&frame.method,
frame.params,
)
.await
.map(|outcome| match outcome {
crate::relay::host::BridgeOutcome::Local(v)
| crate::relay::host::BridgeOutcome::Forwarded { result: v, .. } => v,
})
})
}
}
struct ServiceC;
impl RpcSink for ServiceC {
fn call(
&self,
_endpoint: &str,
method: &str,
params: Option<Value>,
) -> futures::future::BoxFuture<'_, Result<Value, BridgeError>> {
let method = method.to_string();
Box::pin(async move {
match method.as_str() {
"enrollment.mint" => Ok(json!({ "minted": true, "echo": params })),
_ => Err(BridgeError::MethodNotFound(method)),
}
})
}
}
#[tokio::test]
async fn chain_smoke_edge_through_relay_to_service() {
let mut host_c = RelayHost::new();
host_c.mount(
"enrollment.mint",
Arc::new(|params| async move { Ok(json!({ "minted": true, "echo": params })) }.boxed()),
);
let sink_c: Arc<dyn RpcSink> = Arc::new(ServiceC);
let mut host_b = RelayHost::new();
host_b
.forwarding_table_mut()
.allow("enrollment", "C")
.unwrap();
let far_b = Arc::new(RelayFarEnd {
host: host_b,
sink: Arc::clone(&sink_c),
guard: HopGuard::default(),
forwarded: Mutex::new(0),
});
let transport = MemoryTransport::new(Arc::clone(&far_b) as Arc<dyn EdgeFarEnd>);
let result = transport
.call(EdgeFrame {
method: "enrollment.mint".into(),
params: Some(json!({ "ttl": 30 })),
relay: Some(RelayContext {
hops: 0,
via: vec![],
}),
})
.await
.expect("chain call succeeds");
assert_eq!(result["minted"], json!(true));
assert_eq!(result["hops"], json!(1), "exactly one hop stepped");
assert_eq!(*far_b.forwarded.lock().unwrap(), 1);
let refused = transport
.call(EdgeFrame {
method: "enrollment.mint".into(),
params: None,
relay: Some(RelayContext {
hops: 4,
via: vec![],
}),
})
.await;
assert!(refused.is_err(), "hop limit must refuse the forward");
assert_eq!(*far_b.forwarded.lock().unwrap(), 1, "no extra forward");
}
#[tokio::test]
async fn unrouted_methods_deny() {
let mut host_b = RelayHost::new();
host_b
.forwarding_table_mut()
.allow("enrollment", "C")
.unwrap();
let sink_c: Arc<dyn RpcSink> = Arc::new(ServiceC);
let far_b = Arc::new(RelayFarEnd {
host: host_b,
sink: sink_c,
guard: HopGuard::default(),
forwarded: Mutex::new(0),
});
let transport = MemoryTransport::new(far_b as Arc<dyn EdgeFarEnd>);
let denied = transport
.call(EdgeFrame {
method: "rescue.open_session".into(),
params: None,
relay: None,
})
.await;
match denied {
Err(BridgeError::MethodNotFound(method)) => {
assert_eq!(method, "rescue.open_session");
}
other => panic!("expected MethodNotFound denial, got {other:?}"),
}
}
}