1use std::sync::Arc;
47
48use serde_json::{Map, Value};
49
50use super::cross_protocol::{
51 CrossProtocolCodec, CrossProtocolError, ProtocolSide, RequestCodecReport,
52};
53use super::forward::{BridgeError, BridgePeer, SendFuture};
54
55#[derive(Debug)]
63pub struct CrossBridgePeer<U: BridgePeer + ?Sized> {
64 downstream_side: ProtocolSide,
66 codec: Arc<dyn CrossProtocolCodec>,
70 inner: Arc<U>,
72}
73
74impl<U: BridgePeer + ?Sized> CrossBridgePeer<U> {
75 pub fn new(
86 downstream_side: ProtocolSide,
87 codec: Arc<dyn CrossProtocolCodec>,
88 inner: Arc<U>,
89 ) -> Result<Self, BridgeError> {
90 Ok(Self {
91 downstream_side,
92 codec,
93 inner,
94 })
95 }
96
97 #[must_use]
99 pub const fn downstream_side(&self) -> ProtocolSide {
100 self.downstream_side
101 }
102
103 #[must_use]
105 pub fn codec(&self) -> &Arc<dyn CrossProtocolCodec> {
106 &self.codec
107 }
108
109 #[must_use]
111 pub const fn inner(&self) -> &Arc<U> {
112 &self.inner
113 }
114
115 #[must_use]
124 pub fn bridges(&self, ir_method: &str) -> bool {
125 self.downstream_side.wire_method(ir_method).is_some()
126 }
127}
128
129impl<U: BridgePeer + ?Sized> BridgePeer for CrossBridgePeer<U> {
130 fn send(
131 &self,
132 envelope: Map<String, Value>,
133 ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
134 Box::pin(async move {
135 let ir_method = ir_method_of(&envelope);
136 let mut report = RequestCodecReport::empty();
137 let outbound = self
138 .codec
139 .encode_request(self.downstream_side, ir_method, &envelope, &mut report)
140 .map_err(|e| bridge_error_from(e, self.downstream_side))?;
141 let reply = self.inner.send(outbound).await?;
142 self.codec
143 .decode_reply(self.downstream_side, ir_method, &reply, &mut report)
144 .map_err(|e| bridge_error_from(e, self.downstream_side))
145 })
146 }
147
148 fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
149 Box::pin(async move {
150 let ir_method = ir_method_of(&envelope);
151 let mut report = RequestCodecReport::empty();
152 let outbound = self
153 .codec
154 .encode_request(self.downstream_side, ir_method, &envelope, &mut report)
155 .map_err(|e| bridge_error_from(e, self.downstream_side))?;
156 self.inner.notify(outbound).await
157 })
158 }
159
160 fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
161 self.inner.close()
162 }
163
164 fn listen(&self) -> SendFuture<'_, Result<(), BridgeError>> {
165 self.inner.listen()
166 }
167}
168
169fn ir_method_of(envelope: &Map<String, Value>) -> &str {
182 envelope.get("method").and_then(Value::as_str).unwrap_or("")
183}
184
185fn bridge_error_from(err: CrossProtocolError, downstream: ProtocolSide) -> BridgeError {
187 match err {
188 CrossProtocolError::MethodNotBridged(method, _) => {
189 BridgeError::MethodNotBridged { method, downstream }
190 }
191 CrossProtocolError::UnknownDownstream(side) => BridgeError::MethodNotBridged {
192 method: format!("<codec for {side:?}>"),
193 downstream,
194 },
195 }
196}
197
198#[cfg(test)]
199mod tests {
200 use std::sync::{Arc, Mutex, TryLockError};
201
202 use serde_json::{Map, Value, json};
203
204 use super::{
205 super::cross_protocol::{
206 CrossProtocolCodec, CrossProtocolError, ProtocolSide, RequestCodecReport,
207 RequestCodecStep,
208 },
209 super::forward::{BridgeError, BridgePeer, SendFuture},
210 CrossBridgePeer,
211 };
212
213 #[derive(Debug)]
219 struct RenameOnlyCodec {
220 downstream: ProtocolSide,
221 from: &'static str,
222 to: &'static str,
223 }
224
225 impl CrossProtocolCodec for RenameOnlyCodec {
226 fn downstream_protocol(&self) -> ProtocolSide {
227 self.downstream
228 }
229
230 fn encode_request(
231 &self,
232 _downstream_side: ProtocolSide,
233 ir_method: &str,
234 envelope: &Map<String, Value>,
235 report: &mut RequestCodecReport,
236 ) -> Result<Map<String, Value>, CrossProtocolError> {
237 if ir_method != self.from {
238 return Err(CrossProtocolError::MethodNotBridged(
239 ir_method.to_owned(),
240 self.downstream,
241 ));
242 }
243 report.record(RequestCodecStep::MethodRenamed {
244 from: self.from,
245 to: self.to,
246 });
247 let mut out = envelope.clone();
248 out.insert("method".to_owned(), Value::String(self.to.to_owned()));
249 Ok(out)
250 }
251
252 fn decode_reply(
253 &self,
254 _downstream_side: ProtocolSide,
255 _ir_method: &str,
256 reply: &Map<String, Value>,
257 _report: &mut RequestCodecReport,
258 ) -> Result<Map<String, Value>, CrossProtocolError> {
259 Ok(reply.clone())
260 }
261 }
262
263 #[derive(Debug, Default)]
265 struct RecorderPeer {
266 sent: Mutex<Vec<Map<String, Value>>>,
267 reply: Map<String, Value>,
268 }
269
270 impl RecorderPeer {
271 fn answering_with(reply: Map<String, Value>) -> Self {
272 Self {
273 sent: Mutex::new(Vec::new()),
274 reply,
275 }
276 }
277 }
278
279 impl RecorderPeer {
280 fn record(&self, envelope: Map<String, Value>) -> Result<(), BridgeError> {
286 self.sent
287 .lock()
288 .map_err(|_| BridgeError::Close("recorder mutex poisoned".to_owned()))
289 .map(|mut guard| guard.push(envelope))
290 }
291 }
292
293 impl BridgePeer for RecorderPeer {
294 fn send(
295 &self,
296 envelope: Map<String, Value>,
297 ) -> SendFuture<'_, Result<Map<String, Value>, BridgeError>> {
298 let reply = self.reply.clone();
299 let Ok(()) = self.record(envelope) else {
306 return Box::pin(async {
307 Err(BridgeError::Close("recorder mutex poisoned".to_owned()))
308 });
309 };
310 Box::pin(async move { Ok(reply) })
311 }
312
313 fn notify(&self, envelope: Map<String, Value>) -> SendFuture<'_, Result<(), BridgeError>> {
314 let Ok(()) = self.record(envelope) else {
315 return Box::pin(async {
316 Err(BridgeError::Close("recorder mutex poisoned".to_owned()))
317 });
318 };
319 Box::pin(async move { Ok(()) })
320 }
321
322 fn close(&self) -> SendFuture<'_, Result<(), BridgeError>> {
323 Box::pin(async { Ok(()) })
324 }
325 }
326
327 fn make_envelope(method: &str) -> Map<String, Value> {
328 let mut map = Map::new();
329 map.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
330 map.insert("id".to_owned(), Value::Number(1.into()));
331 map.insert("method".to_owned(), Value::String(method.to_owned()));
332 map.insert("params".to_owned(), Value::Object(Map::new()));
333 map
334 }
335
336 fn reply_with(message: &str) -> Map<String, Value> {
337 let mut map = Map::new();
338 map.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
339 map.insert("id".to_owned(), Value::Number(1.into()));
340 map.insert("result".to_owned(), Value::String(message.to_owned()));
341 map
342 }
343
344 fn codec_renaming_send_message_to_tools_call() -> Arc<dyn CrossProtocolCodec> {
345 Arc::new(RenameOnlyCodec {
346 downstream: ProtocolSide::A2a,
347 from: "SendMessage",
348 to: "tools/call",
349 })
350 }
351
352 type TestError = Failure;
353
354 #[derive(Debug)]
361 enum Failure {
362 RecorderPoisoned,
363 RecorderEmpty,
364 RecordedEnvelopeMissingMethod,
365 ReplyMissingResult,
366 BridgeError(String),
367 }
368
369 impl core::fmt::Display for Failure {
370 fn fmt(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
371 match self {
372 Self::RecorderPoisoned => formatter.write_str("the recorder mutex was poisoned"),
373 Self::RecorderEmpty => formatter.write_str("the recorder saw nothing"),
374 Self::RecordedEnvelopeMissingMethod => {
375 formatter.write_str("recorded envelope had no `method` member")
376 }
377 Self::ReplyMissingResult => formatter.write_str("reply had no `result` member"),
378 Self::BridgeError(message) => formatter.write_str(message),
379 }
380 }
381 }
382
383 impl std::error::Error for Failure {}
384
385 impl<T> From<TryLockError<T>> for Failure {
386 fn from(_: TryLockError<T>) -> Self {
387 Self::RecorderPoisoned
388 }
389 }
390
391 impl From<BridgeError> for Failure {
392 fn from(err: BridgeError) -> Self {
393 Self::BridgeError(format!("{err:?}"))
394 }
395 }
396
397 fn lock<T>(mutex: &Mutex<T>) -> Result<std::sync::MutexGuard<'_, T>, Failure> {
402 mutex.lock().map_err(|_| Failure::RecorderPoisoned)
403 }
404
405 #[tokio::test]
406 async fn a_send_envelope_is_translated_then_forwarded_then_reversed() -> Result<(), TestError> {
407 let peer = Arc::new(RecorderPeer::answering_with(reply_with("ok")));
408 let codec = codec_renaming_send_message_to_tools_call();
409 let wrapper = CrossBridgePeer::new(ProtocolSide::A2a, codec, peer.clone())?;
410
411 let sent = make_envelope("SendMessage");
412 let reply = wrapper.send(sent).await?;
413
414 let recorded = lock(&peer.sent)?;
416 let only = recorded.first().ok_or(Failure::RecorderEmpty)?;
417 let method = only
418 .get("method")
419 .and_then(Value::as_str)
420 .ok_or(Failure::RecordedEnvelopeMissingMethod)?;
421 assert_eq!(method, "tools/call");
422 drop(recorded);
423
424 let result = reply
427 .get("result")
428 .and_then(Value::as_str)
429 .ok_or(Failure::ReplyMissingResult)?;
430 assert_eq!(result, "ok");
431 Ok(())
432 }
433
434 #[tokio::test]
435 async fn a_method_the_codec_does_not_bridge_is_refused_with_method_not_bridged()
436 -> Result<(), TestError> {
437 let peer = Arc::new(RecorderPeer::default());
438 let codec = codec_renaming_send_message_to_tools_call();
439 let wrapper = CrossBridgePeer::new(ProtocolSide::A2a, codec, peer.clone())?;
440
441 let sent = make_envelope("GetTask");
442 let Err(err) = wrapper.send(sent).await else {
443 return Err(Failure::BridgeError(
444 "unbridged method must be refused".to_owned(),
445 ));
446 };
447
448 match err {
449 BridgeError::MethodNotBridged { method, downstream } => {
450 assert_eq!(method, "GetTask");
451 assert_eq!(downstream, ProtocolSide::A2a);
452 }
453 other => {
454 return Err(Failure::BridgeError(format!(
455 "expected MethodNotBridged, got {other:?}"
456 )));
457 }
458 }
459 let recorded = lock(&peer.sent)?;
461 let empty = recorded.is_empty();
462 drop(recorded);
463 assert!(empty, "recorder saw an envelope it should not have");
464 Ok(())
465 }
466
467 #[tokio::test]
468 async fn a_notify_envelope_is_translated_then_forwarded() -> Result<(), TestError> {
469 let peer = Arc::new(RecorderPeer::default());
470 let codec = codec_renaming_send_message_to_tools_call();
471 let wrapper = CrossBridgePeer::new(ProtocolSide::A2a, codec, peer.clone())?;
472
473 let mut envelope = Map::new();
474 envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
475 envelope.insert("method".to_owned(), Value::String("SendMessage".to_owned()));
476 envelope.insert("params".to_owned(), json!({}));
477 wrapper.notify(envelope).await?;
478
479 let recorded = lock(&peer.sent)?;
480 let only = recorded.first().ok_or(Failure::RecorderEmpty)?;
481 let method = only
482 .get("method")
483 .and_then(Value::as_str)
484 .ok_or(Failure::RecordedEnvelopeMissingMethod)?;
485 assert_eq!(method, "tools/call");
486 drop(recorded);
487 Ok(())
488 }
489
490 #[test]
491 fn a_symmetric_codec_can_be_wrapped_for_either_downstream() -> Result<(), TestError> {
492 let peer: Arc<RecorderPeer> = Arc::new(RecorderPeer::default());
497 let codec: Arc<dyn CrossProtocolCodec> = Arc::new(RenameOnlyCodec {
498 downstream: ProtocolSide::Mcp,
499 from: "tools/call",
500 to: "SendMessage",
501 });
502 let wrapper = match CrossBridgePeer::new(ProtocolSide::A2a, codec, peer) {
503 Ok(wrapper) => wrapper,
504 Err(err) => {
505 return Err(Failure::BridgeError(format!(
506 "construction should succeed: {err:?}"
507 )));
508 }
509 };
510 assert_eq!(wrapper.downstream_side(), ProtocolSide::A2a);
511 Ok(())
512 }
513
514 fn assert_send_sync<T: Send + Sync>(_: &T) {}
515
516 #[test]
517 fn the_wrapper_is_send_and_sync_via_arc() -> Result<(), TestError> {
518 let peer: Arc<RecorderPeer> = Arc::new(RecorderPeer::default());
519 let codec = codec_renaming_send_message_to_tools_call();
520 let wrapper = match CrossBridgePeer::new(ProtocolSide::A2a, codec, peer) {
521 Ok(wrapper) => wrapper,
522 Err(err) => {
523 return Err(Failure::BridgeError(format!("construction: {err:?}")));
524 }
525 };
526 assert_send_sync(&wrapper);
527 Ok(())
528 }
529
530 #[test]
531 fn bridges_reports_true_only_for_methods_in_the_codec_table() -> Result<(), TestError> {
532 let peer: Arc<RecorderPeer> = Arc::new(RecorderPeer::default());
533 let codec = codec_renaming_send_message_to_tools_call();
534 let wrapper = match CrossBridgePeer::new(ProtocolSide::A2a, codec, peer) {
535 Ok(wrapper) => wrapper,
536 Err(err) => {
537 return Err(Failure::BridgeError(format!("construction: {err:?}")));
538 }
539 };
540 assert!(wrapper.bridges("SendMessage"));
541 assert!(!wrapper.bridges("GetTask"));
542 assert!(!wrapper.bridges("tools/call"));
543 Ok(())
544 }
545
546 #[tokio::test]
551 async fn the_real_a2a_mcp_codec_round_trips_through_the_wrapper() -> Result<(), TestError> {
552 use crate::application::bridge::A2aMcpCodec;
553
554 let mcp_reply: Map<String, Value> = serde_json::from_str(
557 r#"{
558 "jsonrpc": "2.0",
559 "id": 1,
560 "result": {
561 "content": [{"type": "text", "text": "hello"}],
562 "isError": false
563 }
564 }"#,
565 )
566 .map_err(|err| Failure::BridgeError(format!("fixture parse: {err}")))?;
567 let peer = Arc::new(RecorderPeer::answering_with(mcp_reply));
568
569 let codec: Arc<dyn CrossProtocolCodec> = Arc::new(A2aMcpCodec::new());
570 let wrapper = CrossBridgePeer::new(ProtocolSide::A2a, codec, peer.clone())
571 .map_err(|err| Failure::BridgeError(format!("construction: {err:?}")))?;
572
573 let mut envelope = Map::new();
577 envelope.insert("jsonrpc".to_owned(), Value::String("2.0".to_owned()));
578 envelope.insert("id".to_owned(), Value::Number(1.into()));
579 envelope.insert("method".to_owned(), Value::String("SendMessage".to_owned()));
580 envelope.insert(
581 "params".to_owned(),
582 json!({
583 "message": {
584 "role": "user",
585 "parts": [
586 {"kind": "data", "data": {"message": "hi"}}
587 ]
588 },
589 "metadata": {"bridge_skill": "echo"}
590 }),
591 );
592
593 let reply = wrapper.send(envelope).await?;
594
595 let recorded = lock(&peer.sent)?;
597 let only = recorded.first().ok_or(Failure::RecorderEmpty)?;
598 let method = only
599 .get("method")
600 .and_then(Value::as_str)
601 .ok_or(Failure::RecordedEnvelopeMissingMethod)?;
602 assert_eq!(method, "tools/call");
603 let params = only
604 .get("params")
605 .and_then(Value::as_object)
606 .ok_or_else(|| Failure::BridgeError("params was not an object".to_owned()))?;
607 let name = params
608 .get("name")
609 .and_then(Value::as_str)
610 .ok_or_else(|| Failure::BridgeError("`name` was not a string".to_owned()))?;
611 assert_eq!(name, "echo");
612 drop(recorded);
613
614 let result = reply.get("result").ok_or(Failure::ReplyMissingResult)?;
617 let message = result
618 .as_object()
619 .and_then(|m| m.get("message"))
620 .and_then(Value::as_object)
621 .ok_or_else(|| {
622 Failure::BridgeError("reply.result.message was not an object".to_owned())
623 })?;
624 let role = message
625 .get("role")
626 .and_then(Value::as_str)
627 .ok_or_else(|| Failure::BridgeError("`role` was not a string".to_owned()))?;
628 assert_eq!(role, "assistant");
629 let parts = message
630 .get("parts")
631 .and_then(Value::as_array)
632 .ok_or_else(|| Failure::BridgeError("`parts` was not an array".to_owned()))?;
633 assert_eq!(parts.len(), 1, "expected one part in the reply");
634 Ok(())
635 }
636}