Skip to main content

aimdb_core/session/
mod.rs

1//! Connector-session substrate — the shared machinery for transport-based
2//! remote access.
3//!
4//! Three layers: transport ([`Connection`]/[`Listener`]/[`Dialer`]), codec
5//! ([`EnvelopeCodec`]), and dispatch ([`Dispatch`]/[`Session`]), over a
6//! role-neutral [`Inbound`]/[`Outbound`] message set shared by the reactive
7//! server engine (`serve`/`run_session`) and the proactive client engine
8//! (`run_client`/`pump_client`). Data-plane connectors use `pump_sink`/
9//! `pump_source` over the [`Source`] / [`Connector`](crate::transport::Connector)
10//! capabilities.
11//!
12//! All contracts are `dyn`-safe and compile on `std` and `no_std + alloc`.
13
14use alloc::{boxed::Box, string::String, sync::Arc, vec::Vec};
15use core::future::Future;
16use core::pin::Pin;
17
18use futures_core::Stream;
19
20// The engines are runtime-neutral (`futures` channels + the adapter's `RuntimeOps`
21// clock, no `tokio`/`embassy-*`), so they cross-compile to `no_std + alloc`.
22#[cfg(feature = "connector-session")]
23mod client;
24#[cfg(feature = "connector-session")]
25mod connector;
26#[cfg(feature = "connector-session")]
27mod endpoint;
28#[cfg(feature = "connector-session")]
29mod io;
30#[cfg(feature = "connector-session")]
31mod pump;
32#[cfg(feature = "connector-session")]
33mod server;
34
35// Concrete AimX protocol substrate. The transport lives in a separate connector
36// crate (`aimdb-uds-connector`) — core keeps the protocol plus the generic
37// [`SessionClientConnector`] / [`SessionServerConnector`] spine.
38#[cfg(all(feature = "connector-session", feature = "remote"))]
39pub mod aimx;
40
41mod topic_match;
42pub use topic_match::{is_wildcard, pattern_contains, topic_matches};
43
44#[cfg(feature = "connector-session")]
45pub use client::{pump_client, run_client, ClientConfig, ClientHandle};
46#[cfg(feature = "connector-session")]
47pub use connector::{SessionClientConnector, SessionServerConnector};
48#[cfg(feature = "connector-session")]
49pub use endpoint::{split_host_port, split_host_port_opt, EndpointError};
50#[cfg(feature = "connector-session")]
51pub use io::{
52    ByteRead, ByteStream, ByteWrite, Datagram, DatagramBinder, Delay, FrameFault, FramedConnection,
53    Framer, FramerFactory, FramingDialer, FramingListener, IoError, OneShot, OneShotDialer,
54    OneShotListener, StreamDialer, StreamListener,
55};
56#[cfg(feature = "connector-session")]
57pub use pump::{pump_sink, pump_source};
58#[cfg(feature = "connector-session")]
59pub use server::{run_session, serve, SessionConfig};
60
61// ===========================================================================
62// Shared aliases
63// ===========================================================================
64
65/// Boxed, `Send` future — the object-safe async return shape used throughout.
66pub type BoxFut<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
67
68/// Boxed, `Send` stream — the reply shape of a subscription ([`Session::subscribe`]).
69pub type BoxStream<'a, T> = Pin<Box<dyn Stream<Item = T> + Send + 'a>>;
70
71/// A serialized record value, carried opaquely through the codec.
72///
73/// `Arc<[u8]>` so fan-out is a refcount bump. Bytes stay opaque until a handler
74/// inspects them.
75pub type Payload = Arc<[u8]>;
76
77/// One update delivered on a subscription stream (server [`Session::subscribe`]
78/// and client [`ClientHandle::subscribe`] alike).
79///
80/// `topic` names the concrete record that fired: `Some` on wildcard
81/// subscriptions, which fan in many records under one subscription id, and on
82/// any transport that tags every event; `None` on an exact-topic subscription.
83#[derive(Clone, Debug)]
84pub struct SubUpdate {
85    /// Concrete record topic that fired, when the producer side tags it.
86    pub topic: Option<Arc<str>>,
87    /// The serialized record value.
88    pub data: Payload,
89    /// Count of skipped records
90    pub skipped: u64,
91    /// Set on the last update of the late-join snapshot burst, carrying its total `skipped`.
92    ///
93    /// Only set when the burst produced a final update to carry it — an empty
94    /// burst, an exact-topic subscribe, or a tail that failed to encode end
95    /// without one — so its absence says nothing.
96    pub snapshot_end: bool,
97}
98
99impl SubUpdate {
100    /// An untagged update (exact-topic subscription).
101    pub fn new(data: Payload) -> Self {
102        Self {
103            topic: None,
104            data,
105            skipped: 0,
106            snapshot_end: false,
107        }
108    }
109
110    /// A topic-tagged update (wildcard / bus subscription).
111    pub fn tagged(topic: Arc<str>, data: Payload) -> Self {
112        Self {
113            topic: Some(topic),
114            data,
115            skipped: 0,
116            snapshot_end: false,
117        }
118    }
119
120    /// Mark that `n` updates were lost on this subscription immediately before
121    /// this one (`0` leaves the update lossless).
122    pub fn with_skipped(mut self, n: u64) -> Self {
123        self.skipped = n;
124        self
125    }
126
127    /// Mark this update as the last of the late-join snapshot burst (see
128    /// [`snapshot_end`](Self::snapshot_end)).
129    pub fn with_snapshot_end(mut self) -> Self {
130        self.snapshot_end = true;
131        self
132    }
133}
134
135/// Result of a transport-layer operation.
136pub type TransportResult<T> = Result<T, TransportError>;
137
138// ===========================================================================
139// Supporting types (stubs — sufficient for the signatures to compile)
140// ===========================================================================
141
142/// Remote-peer metadata carried by a [`Connection`].
143///
144/// A neutral [`peer_addr`](Self::peer_addr) plus a type-erased
145/// [`ext`](Self::ext) slot a connector fills with its own resolved identity
146/// (e.g. WS attaches `ClientInfo` at the HTTP upgrade), keeping core
147/// connector-agnostic. Downcast `ext` with [`ext_as`](Self::ext_as).
148#[derive(Clone, Default)]
149#[non_exhaustive]
150pub struct PeerInfo {
151    /// Remote address, if the transport exposes one.
152    pub peer_addr: Option<String>,
153    /// Connector-resolved identity, type-erased so core need not know the
154    /// connector's auth types. Downcast with [`ext_as`](Self::ext_as).
155    pub ext: Option<Arc<dyn core::any::Any + Send + Sync>>,
156}
157
158impl PeerInfo {
159    /// Attach a connector-resolved identity (consumed by [`Dispatch::authenticate`]).
160    pub fn with_ext(mut self, ext: Arc<dyn core::any::Any + Send + Sync>) -> Self {
161        self.ext = Some(ext);
162        self
163    }
164
165    /// Downcast the [`ext`](Self::ext) identity to a concrete connector type.
166    pub fn ext_as<T: core::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
167        self.ext.clone()?.downcast::<T>().ok()
168    }
169}
170
171impl core::fmt::Debug for PeerInfo {
172    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
173        f.debug_struct("PeerInfo")
174            .field("peer_addr", &self.peer_addr)
175            .field("ext", &self.ext.as_ref().map(|_| "<opaque>"))
176            .finish()
177    }
178}
179
180/// The authenticated session context produced by [`Dispatch::authenticate`] and
181/// threaded into [`Dispatch::open`].
182///
183/// Carries the resolved principal as a type-erased [`ext`](Self::ext) for
184/// per-operation authorization in the [`Session`]; downcast with
185/// [`ext_as`](Self::ext_as). Connectors that don't authenticate leave it `None`.
186#[derive(Clone, Default)]
187#[non_exhaustive]
188pub struct SessionCtx {
189    /// The resolved principal, type-erased. Downcast with [`ext_as`](Self::ext_as).
190    pub ext: Option<Arc<dyn core::any::Any + Send + Sync>>,
191}
192
193impl SessionCtx {
194    /// Build a context carrying a connector-resolved principal.
195    pub fn with_ext(ext: Arc<dyn core::any::Any + Send + Sync>) -> Self {
196        Self { ext: Some(ext) }
197    }
198
199    /// Downcast the [`ext`](Self::ext) principal to a concrete connector type.
200    pub fn ext_as<T: core::any::Any + Send + Sync>(&self) -> Option<Arc<T>> {
201        self.ext.clone()?.downcast::<T>().ok()
202    }
203}
204
205impl core::fmt::Debug for SessionCtx {
206    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
207        f.debug_struct("SessionCtx")
208            .field("ext", &self.ext.as_ref().map(|_| "<opaque>"))
209            .finish()
210    }
211}
212
213/// Per-session resource bounds consumed by the engines.
214#[derive(Debug, Clone)]
215pub struct SessionLimits {
216    /// Maximum concurrently served connections.
217    pub max_connections: usize,
218    /// Maximum live subscriptions per connection.
219    pub max_subs_per_connection: usize,
220}
221
222impl Default for SessionLimits {
223    fn default() -> Self {
224        Self {
225            max_connections: 16,
226            max_subs_per_connection: 32,
227        }
228    }
229}
230
231/// Transport-layer failure (Layer 1).
232#[derive(Debug, Clone, PartialEq, Eq)]
233#[non_exhaustive]
234pub enum TransportError {
235    /// The connection was closed or reset by the peer.
236    Closed,
237    /// An underlying I/O operation failed.
238    Io,
239    /// The byte stream could not be framed, and the framer cannot resynchronize.
240    Framing,
241    /// The transport's one endpoint resource is already in use — a second dial
242    /// on a single-socket transport while the first connection is live.
243    Busy,
244}
245
246/// Envelope-codec failure — a frame could not be decoded/encoded.
247#[derive(Debug, Clone, PartialEq, Eq)]
248#[non_exhaustive]
249pub enum CodecError {
250    /// The frame was not valid for this envelope format.
251    Malformed,
252}
253
254/// Dispatch-layer (application) failure for `call` / `subscribe` / `write`.
255#[derive(Debug, Clone, PartialEq, Eq)]
256#[non_exhaustive]
257pub enum RpcError {
258    /// No such method or topic.
259    NotFound,
260    /// The caller lacks permission for this operation.
261    Denied,
262    /// The handler failed.
263    Internal,
264    /// The peer's declared protocol version is incompatible with this server's
265    /// `PROTOCOL_VERSION`. Raised at the handshake, before `hello` completes.
266    VersionMismatch,
267}
268
269/// Authentication failure raised by [`Dispatch::authenticate`].
270#[derive(Debug, Clone, PartialEq, Eq)]
271#[non_exhaustive]
272pub enum AuthError {
273    /// Credentials were missing or rejected.
274    Unauthorized,
275}
276
277// ===========================================================================
278// Logical message set — role-neutral: the server's `Inbound` is the client's
279// outbound and vice versa.
280// ===========================================================================
281
282/// A logical request arriving over a [`Connection`] (what the server receives).
283pub enum Inbound {
284    /// An RPC call expecting a single [`Outbound::Reply`].
285    Request {
286        /// Correlation id, echoed in the reply.
287        id: u64,
288        /// Method name (e.g. `"record.set"`, `"query"`).
289        method: String,
290        /// Unparsed method parameters.
291        params: Payload,
292    },
293    /// Open a subscription producing many [`Outbound::Event`]s.
294    Subscribe {
295        /// Correlation id for the subscription handshake.
296        id: u64,
297        /// Topic to subscribe to.
298        topic: String,
299    },
300    /// Close a previously opened subscription.
301    Unsubscribe {
302        /// Subscription id to cancel.
303        sub: String,
304    },
305    /// A fire-and-forget write (no reply).
306    Write {
307        /// Destination topic.
308        topic: String,
309        /// Unparsed record value.
310        payload: Payload,
311    },
312    /// Keepalive.
313    Ping,
314}
315
316/// A logical message sent back over a [`Connection`] (what the server emits).
317pub enum Outbound<'a> {
318    /// Reply to an [`Inbound::Request`].
319    Reply {
320        /// Correlation id of the originating request.
321        id: u64,
322        /// The result, or an [`RpcError`].
323        result: Result<Payload, RpcError>,
324    },
325    /// A subscription update.
326    Event {
327        /// Subscription id this event belongs to.
328        sub: &'a str,
329        /// Monotonic sequence number.
330        seq: u64,
331        /// Concrete record topic that fired, when the subscription tags it
332        /// (wildcard subscriptions fan in many records — see [`SubUpdate`]).
333        topic: Option<&'a str>,
334        /// Unparsed record value.
335        data: Payload,
336    },
337    /// An initial snapshot emitted when a subscription opens (late-join).
338    Snapshot {
339        /// Subscription id the snapshot belongs to, so the client demux routes
340        /// it like an [`Event`](Outbound::Event) (a wildcard subscription's
341        /// snapshots carry topics that don't string-match the pattern key).
342        sub: &'a str,
343        /// Monotonic sequence number, in the **same space** as this
344        /// subscription's [`Event`](Outbound::Event)s: the burst is numbered
345        /// `1..=N` and the first event continues at `N + 1`, so a lost snapshot
346        /// surfaces in the next update's [`SubUpdate::skipped`].
347        seq: u64,
348        /// Set on the last snapshot of the burst, so the loss total lands on an
349        /// update the subscriber is guaranteed to see. The client engine
350        /// reserves a sink slot for this frame.
351        last: bool,
352        /// Topic the snapshot is for.
353        topic: &'a str,
354        /// Unparsed record value.
355        data: Payload,
356    },
357    /// An explicit acknowledgement that a subscription opened. Emitted by
358    /// [`run_session`] only when [`SessionConfig::acks_subscribe`] is set.
359    /// The `sub` is the subscription's routing id — the same value that tags its
360    /// [`Event`](Outbound::Event)s.
361    Subscribed {
362        /// Subscription id that was opened.
363        sub: &'a str,
364    },
365    /// Keepalive response.
366    Pong,
367}
368
369// ===========================================================================
370// Layer 1 — transport. Framing lives in the transport: `recv` returns one
371// logical frame. `Dialer` is the client-side dual of `Listener`.
372// ===========================================================================
373
374/// A framed, bidirectional pipe — role-neutral (yielded by either
375/// [`Listener::accept`] or [`Dialer::connect`]).
376pub trait Connection: Send {
377    /// Receive one logical frame. `Ok(None)` signals the peer closed.
378    fn recv(&mut self) -> BoxFut<'_, TransportResult<Option<Vec<u8>>>>;
379
380    /// Send one logical frame.
381    fn send<'a>(&'a mut self, frame: &'a [u8]) -> BoxFut<'a, TransportResult<()>>;
382
383    /// Peer metadata (remote addr, headers, pre-resolved auth).
384    fn peer(&self) -> &PeerInfo;
385}
386
387/// The accepting (server) side — produces [`Connection`]s we did not initiate.
388pub trait Listener: Send {
389    /// Accept the next inbound connection.
390    fn accept(&mut self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>>;
391}
392
393/// The initiating (client) side — the dual of [`Listener`]; dials out and
394/// produces the same [`Connection`].
395pub trait Dialer: Send {
396    /// Open a connection to the configured remote.
397    fn connect(&self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>>;
398}
399
400/// A boxed dialer is itself a [`Dialer`], so a runtime-selected
401/// `Box<dyn Dialer>` — from a `scheme://` URL resolver, say — can be passed
402/// where [`run_client`]`<D: Dialer>` is expected. The `Send` supertrait makes
403/// the box `Send + 'static`.
404impl Dialer for Box<dyn Dialer> {
405    fn connect(&self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
406        (**self).connect()
407    }
408}
409
410// ===========================================================================
411// Layer 3 — dispatch. RPC and streaming unify in one per-connection role with
412// three reply cardinalities: `call` (one) / `subscribe` (many) / `write` (none).
413// Split across two traits: the shared, immutable [`Dispatch`] (one
414// `Arc<dyn Dispatch>` per server) and the per-connection, `&mut`-threaded
415// [`Session`] that can hold mutable state without a lock.
416// ===========================================================================
417
418/// The shared application dispatch: authenticate a connection, then open a
419/// per-connection [`Session`]. One `Arc<dyn Dispatch>` is shared across every
420/// accepted connection, so it stays `Send + Sync` and behind `&self`.
421pub trait Dispatch: Send + Sync {
422    /// Resolve a [`SessionCtx`] from peer metadata and/or the first frame
423    /// (a pre-resolved identity in [`PeerInfo`], or an in-band Hello in `first`).
424    fn authenticate<'a>(
425        &'a self,
426        peer: &'a PeerInfo,
427        first: Option<&'a [u8]>,
428    ) -> BoxFut<'a, Result<SessionCtx, AuthError>>;
429
430    /// Open the per-connection [`Session`] once, after [`authenticate`](Self::authenticate). It owns
431    /// the connection's mutable dispatch state that the shared `Arc<Self>` cannot
432    /// hold behind `&self`; the engine threads `&mut` into it.
433    fn open(&self, ctx: &SessionCtx) -> Box<dyn Session>;
434}
435
436/// The per-connection session: serves calls, subscriptions, and writes for one
437/// accepted [`Connection`]. The engine owns the `Box<dyn Session>` and threads
438/// `&mut self` into each method, so it can hold per-connection state without a
439/// lock; the shared, immutable role stays on [`Dispatch`].
440pub trait Session: Send {
441    /// One-shot RPC: one request → one reply.
442    fn call<'a>(
443        &'a mut self,
444        method: &'a str,
445        params: Payload,
446    ) -> BoxFut<'a, Result<Payload, RpcError>>;
447
448    /// Open a subscription yielding many [`SubUpdate`]s. The stream is `'static`
449    /// (it captures cloned handles) so it outlives the `&mut` borrow and lives in
450    /// the engine. Async so a connector can await per-operation authorization.
451    /// Defaulted to [`RpcError::NotFound`] for dispatches with no streaming.
452    fn subscribe<'a>(
453        &'a mut self,
454        topic: &'a str,
455    ) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
456        let _ = topic;
457        Box::pin(async { Err(RpcError::NotFound) })
458    }
459
460    /// Late-join snapshots: one `(topic, value)` per record covered by `topic`
461    /// (several for a wildcard subscription), each emitted as an
462    /// [`Outbound::Snapshot`] right after a successful
463    /// [`subscribe`](Session::subscribe) and before the first event. Defaulted
464    /// to empty (no snapshots).
465    ///
466    /// The engine numbers the burst `1..=N` and starts the event stream at
467    /// `N + 1`, so a snapshot dropped in transit shows up as
468    /// [`SubUpdate::skipped`] on the next delivered update.
469    ///
470    /// The burst's last snapshot is flagged and reaches the subscriber as the
471    /// one update with [`SubUpdate::snapshot_end`] set, carrying the burst's
472    /// whole loss; the client engine reserves a sink slot so it survives a slow
473    /// consumer.
474    ///
475    /// **Do not block on that flag.** It is absent when `topic` matches no
476    /// records, and when the final snapshot fails to encode or its frame is
477    /// rejected — the flag rides that frame. Loss accounting survives all of
478    /// these; only the end-of-burst signal is lost.
479    fn snapshots(&mut self, topic: &str) -> Vec<(String, Payload)> {
480        let _ = topic;
481        Vec::new()
482    }
483
484    /// Fire-and-forget write: no reply. Routes through the producer/arbiter path,
485    /// so single-writer-per-key stays intact.
486    fn write<'a>(
487        &'a mut self,
488        topic: &'a str,
489        payload: Payload,
490    ) -> BoxFut<'a, Result<(), RpcError>>;
491}
492
493/// The protocol-envelope codec: frame bytes ↔ the logical message set. Layered
494/// above (and nesting) the record-value `JsonCodec`, so the wire format stays
495/// pluggable; `decode`/`encode` keep the record value as an opaque [`Payload`]
496/// sliced from / spliced into the frame.
497///
498/// Symmetric, so one codec object serves both engines: `decode`/`encode` are the
499/// server direction (read requests / write replies) and
500/// [`encode_inbound`](EnvelopeCodec::encode_inbound) /
501/// [`decode_outbound`](EnvelopeCodec::decode_outbound) the client direction.
502pub trait EnvelopeCodec: Send + Sync {
503    /// Decode one frame into a logical [`Inbound`] message (server reads a request).
504    fn decode(&self, frame: &[u8]) -> Result<Inbound, CodecError>;
505
506    /// Encode a logical [`Outbound`] message, appending its bytes to `out`
507    /// (server writes a reply/event).
508    fn encode(&self, msg: Outbound<'_>, out: &mut Vec<u8>) -> Result<(), CodecError>;
509
510    /// Encode a logical [`Inbound`] message, appending its bytes to `out`
511    /// (client writes a request). The dual of [`decode`](EnvelopeCodec::decode).
512    fn encode_inbound(&self, msg: Inbound, out: &mut Vec<u8>) -> Result<(), CodecError>;
513
514    /// Decode one frame into a logical [`Outbound`] message (client reads a
515    /// reply/event). The dual of [`encode`](EnvelopeCodec::encode); the result
516    /// borrows the frame (`Outbound`'s `sub`/`topic` are `&str` slices into it).
517    fn decode_outbound<'a>(&self, frame: &'a [u8]) -> Result<Outbound<'a>, CodecError>;
518}
519
520// ===========================================================================
521// Data-plane capabilities — connectionless (an external library owns any
522// session). The outbound `Sink` is the canonical
523// [`Connector`](crate::transport::Connector); the inbound `Source` is below.
524// ===========================================================================
525
526/// External → AimDB data-plane: a stream of inbound frames, drained by
527/// `pump_source`.
528pub trait Source: Send {
529    /// Yield the next `(topic, payload)`, or `None` when the source is done.
530    fn next(&mut self) -> BoxFut<'_, Option<(String, Payload)>>;
531}
532
533// ===========================================================================
534// Taking each trait as `&dyn Trait` forces the dyn-compatibility check on all
535// targets, not just under `cargo test`.
536// ===========================================================================
537
538#[allow(dead_code, clippy::too_many_arguments)]
539fn _assert_object_safe(
540    _connection: &dyn Connection,
541    _listener: &dyn Listener,
542    _dialer: &dyn Dialer,
543    _dispatch: &dyn Dispatch,
544    _session: &dyn Session,
545    _codec: &dyn EnvelopeCodec,
546    _source: &dyn Source,
547) {
548}
549
550#[cfg(test)]
551mod tests {
552    use super::*;
553
554    struct MockConnection;
555    impl Connection for MockConnection {
556        fn recv(&mut self) -> BoxFut<'_, TransportResult<Option<Vec<u8>>>> {
557            unimplemented!()
558        }
559        fn send<'a>(&'a mut self, _frame: &'a [u8]) -> BoxFut<'a, TransportResult<()>> {
560            unimplemented!()
561        }
562        fn peer(&self) -> &PeerInfo {
563            unimplemented!()
564        }
565    }
566
567    struct MockListener;
568    impl Listener for MockListener {
569        fn accept(&mut self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
570            unimplemented!()
571        }
572    }
573
574    struct MockDialer;
575    impl Dialer for MockDialer {
576        fn connect(&self) -> BoxFut<'_, TransportResult<Box<dyn Connection>>> {
577            unimplemented!()
578        }
579    }
580
581    struct MockDispatch;
582    impl Dispatch for MockDispatch {
583        fn authenticate<'a>(
584            &'a self,
585            _peer: &'a PeerInfo,
586            _first: Option<&'a [u8]>,
587        ) -> BoxFut<'a, Result<SessionCtx, AuthError>> {
588            unimplemented!()
589        }
590        fn open(&self, _ctx: &SessionCtx) -> Box<dyn Session> {
591            unimplemented!()
592        }
593    }
594
595    struct MockSession;
596    impl Session for MockSession {
597        fn call<'a>(
598            &'a mut self,
599            _method: &'a str,
600            _params: Payload,
601        ) -> BoxFut<'a, Result<Payload, RpcError>> {
602            unimplemented!()
603        }
604        fn subscribe<'a>(
605            &'a mut self,
606            _topic: &'a str,
607        ) -> BoxFut<'a, Result<BoxStream<'static, SubUpdate>, RpcError>> {
608            unimplemented!()
609        }
610        fn write<'a>(
611            &'a mut self,
612            _topic: &'a str,
613            _payload: Payload,
614        ) -> BoxFut<'a, Result<(), RpcError>> {
615            unimplemented!()
616        }
617    }
618
619    struct MockCodec;
620    impl EnvelopeCodec for MockCodec {
621        fn decode(&self, _frame: &[u8]) -> Result<Inbound, CodecError> {
622            unimplemented!()
623        }
624        fn encode(&self, _msg: Outbound<'_>, _out: &mut Vec<u8>) -> Result<(), CodecError> {
625            unimplemented!()
626        }
627        fn encode_inbound(&self, _msg: Inbound, _out: &mut Vec<u8>) -> Result<(), CodecError> {
628            unimplemented!()
629        }
630        fn decode_outbound<'a>(&self, _frame: &'a [u8]) -> Result<Outbound<'a>, CodecError> {
631            unimplemented!()
632        }
633    }
634
635    struct MockSource;
636    impl Source for MockSource {
637        fn next(&mut self) -> BoxFut<'_, Option<(String, Payload)>> {
638            unimplemented!()
639        }
640    }
641
642    /// Every trait is `dyn`-usable.
643    #[test]
644    fn traits_are_object_safe() {
645        let _connection: Box<dyn Connection> = Box::new(MockConnection);
646        let _listener: Box<dyn Listener> = Box::new(MockListener);
647        let _dialer: Box<dyn Dialer> = Box::new(MockDialer);
648        let _dispatch: Box<dyn Dispatch> = Box::new(MockDispatch);
649        let _session: Box<dyn Session> = Box::new(MockSession);
650        let _codec: Box<dyn EnvelopeCodec> = Box::new(MockCodec);
651        let _source: Box<dyn Source> = Box::new(MockSource);
652    }
653
654    /// `Box<dyn Dialer>` satisfies the `Dialer` bound, so a runtime-selected
655    /// dialer can be passed where `D: Dialer` is expected.
656    #[test]
657    fn boxed_dialer_is_a_dialer() {
658        fn takes_dialer<D: Dialer + 'static>(_d: D) {}
659        let boxed: Box<dyn Dialer> = Box::new(MockDialer);
660        takes_dialer(boxed);
661    }
662}