use serde_json::{Map, Value};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum ProtocolSide {
A2a,
Mcp,
}
impl ProtocolSide {
#[must_use]
pub fn wire_method(self, ir_method: &str) -> Option<&'static str> {
match (self, ir_method) {
(Self::A2a, "SendMessage") => Some("tools/call"),
(Self::Mcp, "tools/call") => Some("SendMessage"),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct RequestCodecReport {
pub steps: Vec<RequestCodecStep>,
}
impl RequestCodecReport {
#[must_use]
pub fn empty() -> Self {
Self::default()
}
#[must_use]
pub const fn is_empty(&self) -> bool {
self.steps.is_empty()
}
#[must_use]
pub const fn len(&self) -> usize {
self.steps.len()
}
pub fn record(&mut self, step: RequestCodecStep) {
self.steps.push(step);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RequestCodecStep {
MethodRenamed {
from: &'static str,
to: &'static str,
},
ParamsTranslated {
from: &'static str,
to: &'static str,
},
ReplyTranslated {
from: &'static str,
to: &'static str,
},
}
impl core::fmt::Display for RequestCodecStep {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
Self::MethodRenamed { from, to } => {
write!(formatter, "method {from} → {to}")
}
Self::ParamsTranslated { from, to } => {
write!(formatter, "params {from} → {to}")
}
Self::ReplyTranslated { from, to } => {
write!(formatter, "reply {from} → {to}")
}
}
}
}
impl core::fmt::Display for RequestCodecReport {
fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
for (i, step) in self.steps.iter().enumerate() {
if i > 0 {
formatter.write_str("; ")?;
}
core::fmt::Display::fmt(step, formatter)?;
}
Ok(())
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum CrossProtocolError {
#[error("downstream leg speaks {0:?}, which is not bridged by this codec")]
UnknownDownstream(ProtocolSide),
#[error("method {0:?} is not bridged between {1:?} and the upstream")]
MethodNotBridged(String, ProtocolSide),
}
pub trait CrossProtocolCodec: Send + Sync + core::fmt::Debug {
fn downstream_protocol(&self) -> ProtocolSide;
fn encode_request(
&self,
downstream_side: ProtocolSide,
ir_method: &str,
raw_envelope: &Map<String, Value>,
report: &mut RequestCodecReport,
) -> Result<Map<String, Value>, CrossProtocolError>;
fn decode_reply(
&self,
downstream_side: ProtocolSide,
ir_method: &str,
reply: &Map<String, Value>,
report: &mut RequestCodecReport,
) -> Result<Map<String, Value>, CrossProtocolError>;
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_a2a_downstream_calling_send_message_lets_the_codec_route_to_tools_call() {
assert_eq!(
ProtocolSide::A2a.wire_method("SendMessage"),
Some("tools/call")
);
}
#[test]
fn an_mcp_downstream_calling_tools_call_lets_the_codec_route_to_send_message() {
assert_eq!(
ProtocolSide::Mcp.wire_method("tools/call"),
Some("SendMessage")
);
}
#[test]
fn an_unknown_method_returns_none_on_both_sides() {
assert_eq!(ProtocolSide::Mcp.wire_method("GetTask"), None);
assert_eq!(ProtocolSide::A2a.wire_method("resources/list"), None);
}
#[test]
fn report_starts_empty_and_records_steps() {
let mut report = RequestCodecReport::empty();
assert!(report.is_empty());
report.record(RequestCodecStep::MethodRenamed {
from: "SendMessage",
to: "tools/call",
});
assert_eq!(report.len(), 1);
}
#[test]
fn report_display_lists_steps_in_order() {
let mut report = RequestCodecReport::empty();
report.record(RequestCodecStep::MethodRenamed {
from: "SendMessage",
to: "tools/call",
});
report.record(RequestCodecStep::ParamsTranslated {
from: "A2A message+metadata",
to: "MCP arguments",
});
let s = report.to_string();
assert!(s.contains("SendMessage → tools/call"));
assert!(s.contains("A2A message+metadata → MCP arguments"));
}
}