Skip to main content

protocol_droid/application/server/
exchange.rs

1//! How a handler talks back to the peer.
2//!
3//! [`CallbackExchange`] is the port a transport implements to answer the
4//! server-originated requests a handler makes — sampling, elicitation, roots —
5//! and [`DynExchange`] is its boxed form for the callers that need dynamic
6//! dispatch. [`ExchangeError`] is what either returns when an exchange does
7//! not complete.
8//!
9//! It lives here rather than beside the adapters that implement it because
10//! these are port traits and a port belongs to its consumer. The consumer is
11//! [`PeerSession`](super::context::PeerSession), which hands one to every
12//! request; a transport's `impl` should be written against the shape that
13//! consumer asked for, and reading the consumer and the port in one file is
14//! what makes that check possible.
15
16use crate::domain::ir::JsonObject;
17
18/// Why a server-initiated request could not complete.
19///
20/// Distinct from [`GatedError`](super::error::GatedError): the gate already
21/// passed by the time an exchange runs, so this is the transport or the peer
22/// failing rather than the capability being absent. They are separate types
23/// because a handler that sees one should retry and a handler that sees the
24/// other should not.
25#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
26#[error("the exchange did not complete: {reason}")]
27pub struct ExchangeError {
28    /// What went wrong, in the adapter's words.
29    ///
30    /// A `String` because the transport names its own failures (a closed
31    /// stream, a cancelled request, a timeout) and the application layer has no
32    /// business enumerating them. The rule is that application code never names
33    /// a _third-party_ error type; a transport's own description is data the
34    /// adapter chose to hand over, not a type this signature commits to.
35    reason: String,
36}
37
38impl ExchangeError {
39    /// Builds an exchange failure from whatever the transport reported.
40    #[must_use]
41    pub fn new(reason: impl Into<String>) -> Self {
42        Self {
43            reason: reason.into(),
44        }
45    }
46}
47
48/// A round trip to the peer that a server-initiated request needs.
49///
50/// The port belongs to the consumer. It is declared here rather than in an
51/// adapter because the shape of what a server needs to ask a peer is a decision
52/// this module makes: a method name, a params object, and an answer back. A
53/// transport implements it; nothing in `adapters/` defines it.
54///
55/// Not object-safe by design — it is an RPITIT `async fn`, so a caller that
56/// knows its concrete exchange gets it inlined with no allocation. Callers that
57/// genuinely need a boxed one get [`DynExchange`].
58pub trait CallbackExchange {
59    /// Sends `method` to the peer and returns its answer.
60    ///
61    /// The implementation owns framing: it knows the negotiated revision, it
62    /// knows the method's wire name, and it parses the reply. Nothing above
63    /// this trait ever sees a byte.
64    ///
65    /// # Errors
66    ///
67    /// [`ExchangeError`] when the request could not be delivered or the peer
68    /// did not answer. Whether a non-answer is an error or a hang is the
69    /// adapter's decision, and a server that waits forever is a server whose
70    /// connection never closes.
71    fn exchange(
72        &self,
73        method: &str,
74        params: &JsonObject,
75    ) -> impl Future<Output = Result<JsonObject, ExchangeError>> + Send;
76}
77
78/// The boxed spelling of [`CallbackExchange`], for a caller holding a transport
79/// of unknown concrete type.
80///
81/// # Examples
82///
83/// ```
84/// use std::sync::Arc;
85///
86/// use protocol_droid::application::server::context::{DynExchange, ExchangeError};
87/// use protocol_droid::domain::ir::JsonObject;
88///
89/// let exchange: Arc<dyn DynExchange> = Arc::new(MyExchange);
90/// // ...handed to a PeerSession, which calls it through the trait object.
91/// # struct MyExchange;
92/// # impl DynExchange for MyExchange {
93/// #     fn exchange_boxed<'a>(&'a self, method: &'a str, params: &'a JsonObject)
94/// #         -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>>
95/// #     { Box::pin(async move { let _ = (method, params); Err(ExchangeError::new("unimplemented")) }) }
96/// # }
97/// ```
98pub trait DynExchange: Send + Sync {
99    /// The boxed form of [`CallbackExchange::exchange`].
100    ///
101    /// Written out rather than derived, for the same reason `DynFrameSink` is:
102    /// the box belongs to callers that need dynamic dispatch, not to the ones
103    /// that do not.
104    fn exchange_boxed<'a>(
105        &'a self,
106        method: &'a str,
107        params: &'a JsonObject,
108    ) -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>>;
109}
110
111impl<T: CallbackExchange + Send + Sync> DynExchange for T {
112    fn exchange_boxed<'a>(
113        &'a self,
114        method: &'a str,
115        params: &'a JsonObject,
116    ) -> std::pin::Pin<Box<dyn Future<Output = Result<JsonObject, ExchangeError>> + Send + 'a>>
117    {
118        Box::pin(self.exchange(method, params))
119    }
120}