Skip to main content

protocol_droid/application/jsonrpc/
frame_source.rs

1//! The inbound edge: a stream of whole frames from a peer.
2//!
3//! # Why this trait exists
4//!
5//! [`FrameSink`](crate::application::session::frame_sink::FrameSink) already covers the outbound half, and it lives in
6//! [`crate::application::session`] because the session engine is what consumes
7//! it. The inbound half has a different consumer — the loop that reads a frame
8//! and routes it — and by the project's rule that a port trait belongs to the
9//! consumer, that puts this one here rather than beside its sibling. Two
10//! transports facing opposite directions are not one capability, and a shared
11//! `ports` module would have implied they were.
12//!
13//! It is deliberately not a `Transport` trait. A trait that both read and wrote
14//! would force every implementor to answer for both, which is exactly the
15//! "three adapters wearing one name" objection that
16//! [`FrameSink`](crate::application::session::frame_sink::FrameSink) is documented against.
17//! The stdio server writes to a child it spawned and reads from the same pipe;
18//! the bridge reads from an HTTP body and writes to stdio; a replay source only
19//! reads at all.
20//!
21//! # What it hides
22//!
23//! Chunking. An implementor is given no way to express "I have half a message",
24//! and cannot: whatever a read returns goes into a
25//! [`FrameDecoder`](crate::domain::FrameDecoder), and a frame comes out when it
26//! is complete. A caller of this trait never sees a partial line, never has to
27//! know that a read boundary can fall inside a UTF-8 character, and never has to
28//! know what the ceiling is — although the implementor does, and is the one that
29//! must set it.
30//!
31//! # The distinction this trait has to be careful about
32//!
33//! `Ok(None)` means **the peer closed**, and nothing else. It does not mean "no
34//! frame is ready". The decoder underneath answers `None` for exactly that
35//! second thing, so the two meanings are opposites and an implementor that
36//! forwards the decoder's answer straight up has inverted them. An implementor
37//! awaits more bytes instead; this method resolves only on a complete frame, an
38//! end of stream, or a failure, and a caller can therefore treat `None` as "the
39//! session is over" without also having to ask whether anything was pending.
40//!
41//! # Two traits, because one of them cannot be both
42//!
43//! [`FrameSource`] is a native `async fn`, which is a return-position impl trait
44//! and therefore **not** object-safe. A dispatch loop that is generic over its
45//! source takes `impl FrameSource` and the call monomorphises to nothing. The
46//! CLI bridge, which holds a source of an unknown concrete type behind one
47//! handle, needs `Arc<dyn DynFrameSource>` and cannot have it from this trait.
48//!
49//! [`DynFrameSource`] is the object-safe spelling, written out by hand. A
50//! blanket impl with `async-trait` would put a boxed future on the generic path
51//! too, and one allocation per inbound frame is what the generic form exists to
52//! avoid. The box is confined to the callers that asked for it.
53//!
54//! # Examples
55//!
56//! ```
57//! use std::sync::Arc;
58//!
59//! use protocol_droid::application::jsonrpc::{DynFrameSource, FrameSource, SourceError};
60//! use protocol_droid::domain::{FrameDecoder, MaxFrameSize};
61//!
62//! /// A source backed by a fixed buffer: everything a read would have produced.
63//! struct Replay {
64//!     decoder: std::sync::Mutex<FrameDecoder>,
65//! }
66//!
67//! impl Replay {
68//!     fn new(wire: &[u8]) -> Self {
69//!         let mut decoder = FrameDecoder::new(MaxFrameSize::DEFAULT);
70//!         // A test's fixture is trusted, so a refusal here is the fixture's
71//!         // problem and is ignored rather than unwrapped. A real implementor
72//!         // would have nowhere to put the error either — it has no frame to
73//!         // report it against — and would surface it on the first `next_frame`.
74//!         let _ = decoder.push(wire);
75//!         Self {
76//!             decoder: std::sync::Mutex::new(decoder),
77//!         }
78//!     }
79//! }
80//!
81//! impl FrameSource for Replay {
82//!     async fn next_frame(&self) -> Result<Option<String>, SourceError> {
83//!         let mut decoder = self.decoder.lock().map_err(|_| SourceError::Transport)?;
84//!         decoder
85//!             .next_frame()
86//!             .map(|frame| frame.map(str::to_owned))
87//!             .map_err(SourceError::from)
88//!     }
89//! }
90//!
91//! let runtime = tokio::runtime::Builder::new_current_thread()
92//!     .build()
93//!     .map_err(|error| error.to_string())?;
94//!
95//! // The generic form: the dispatch loop is written once and monomorphises.
96//! let generic = Replay::new(b"{\"jsonrpc\":\"2.0\"}\n");
97//! assert_eq!(
98//!     runtime.block_on(FrameSource::next_frame(&generic)),
99//!     Ok(Some(String::from("{\"jsonrpc\":\"2.0\"}")))
100//! );
101//!
102//! // The boxed form: the same implementation, behind a trait object.
103//! let boxed: Arc<dyn DynFrameSource> = Arc::new(Replay::new(b"{\"id\":1}\n"));
104//! assert_eq!(
105//!     runtime.block_on(boxed.next_frame()),
106//!     Ok(Some(String::from("{\"id\":1}")))
107//! );
108//! # Ok::<(), String>(())
109//! ```
110
111use core::future::Future;
112use core::pin::Pin;
113
114use thiserror::Error;
115
116use crate::domain::FramingError;
117
118/// An inbound frame stream from a peer.
119///
120/// A stream of whole frames, protocol-neutral: nothing here names a method, a
121/// revision, or a message kind, because routing a frame to a method is the next
122/// layer's job and the next layer is the one that knows the protocol.
123///
124/// Implemented by a transport. Consumed by the loop that routes. One method, for
125/// the same reason [`FrameSink`](crate::application::session::frame_sink::FrameSink) has
126/// one.
127///
128/// # Examples
129///
130/// ```
131/// use protocol_droid::application::jsonrpc::{FrameSource, SourceError};
132///
133/// /// A source with nothing behind it, which reports a closed stream at once.
134/// struct Closed;
135///
136/// impl FrameSource for Closed {
137///     async fn next_frame(&self) -> Result<Option<String>, SourceError> {
138///         Ok(None)
139///     }
140/// }
141/// ```
142pub trait FrameSource {
143    /// Waits for the peer's next frame.
144    ///
145    /// # Errors
146    ///
147    /// [`SourceError`] when the frame could not be read, or when the bytes that
148    /// arrived were not a frame. An implementor that has already failed may
149    /// return the error for every later call; a caller does not assume a failure
150    /// is permanent.
151    ///
152    /// No `+ '_` on the return: edition 2024 already captures every in-scope
153    /// lifetime in an RPIT, so spelling one out is not merely redundant — it is
154    /// a bound the implementor would fail to satisfy.
155    fn next_frame(&self) -> impl Future<Output = Result<Option<String>, SourceError>> + Send;
156}
157
158/// A [`FrameSource`] that can sit behind a trait object.
159///
160/// The hand-written spelling of the same method, and the only reason
161/// [`FrameSource`] does not use `async-trait`: the box belongs to the callers
162/// that need dynamic dispatch, not to the ones that do not.
163pub trait DynFrameSource: Send + Sync {
164    /// Waits for the peer's next frame.
165    ///
166    /// # Errors
167    ///
168    /// As [`FrameSource::next_frame`].
169    fn next_frame<'a>(
170        &'a self,
171    ) -> Pin<Box<dyn Future<Output = Result<Option<String>, SourceError>> + Send + 'a>>;
172}
173
174impl<T> DynFrameSource for T
175where
176    T: FrameSource + Send + Sync,
177{
178    fn next_frame<'a>(
179        &'a self,
180    ) -> Pin<Box<dyn Future<Output = Result<Option<String>, SourceError>> + Send + 'a>> {
181        Box::pin(FrameSource::next_frame(self))
182    }
183}
184
185/// Why the next inbound frame did not arrive.
186///
187/// Domain types only. An adapter maps its own `io::Error` into
188/// [`Transport`](Self::Transport) at the boundary, so nothing in `application/`
189/// names a type from a runtime or from the operating system.
190#[derive(Debug, Clone, PartialEq, Eq, Error)]
191pub enum SourceError {
192    /// The transport could not read.
193    ///
194    /// Carries nothing, and that is the point. Every adapter has its own failures
195    /// — a broken pipe, a reset, a child that exited, an HTTP body that ended
196    /// early — and rendering any of them into a `String` an implementor controls
197    /// would put a peer's bytes on the application layer's error path for
198    /// nothing. A caller that needs to know *which* failure maps its own.
199    #[error("the transport could not read the next frame")]
200    Transport,
201
202    /// The bytes on the wire were not a frame.
203    ///
204    /// A peer that will not terminate its line, or that sent something that is
205    /// not text, is a fact about the peer rather than about the pipe, and it is
206    /// worth distinguishing from a transport that simply failed.
207    #[error("the peer's bytes are not a valid frame: {0}")]
208    Framing(#[from] FramingError),
209}
210
211#[cfg(test)]
212mod tests {
213    use core::future::Future;
214    use core::task::{Context, Poll, Waker};
215    use std::sync::Arc;
216
217    use assert_matches::assert_matches;
218
219    use super::{DynFrameSource, FrameSource, SourceError};
220    use crate::domain::{FrameDecoder, FramingError, MaxFrameSize};
221
222    /// A source over a fixed buffer, with the framing done by the real decoder so
223    /// the port is tested against the code a transport would use.
224    struct Replay {
225        decoder: std::sync::Mutex<FrameDecoder>,
226    }
227
228    impl Replay {
229        fn new(wire: &[u8]) -> Self {
230            let mut decoder =
231                FrameDecoder::new(MaxFrameSize::new(1024).unwrap_or(MaxFrameSize::DEFAULT));
232            let _ = decoder.push(wire);
233            Self {
234                decoder: std::sync::Mutex::new(decoder),
235            }
236        }
237    }
238
239    impl FrameSource for Replay {
240        #[expect(
241            clippy::unused_async_trait_impl,
242            reason = "FrameSource is a native async fn trait, so the signature is the trait's and not the fixture's to change; locking a std mutex and polling the decoder are both synchronous, which is the whole point of the fixture"
243        )]
244        async fn next_frame(&self) -> Result<Option<String>, SourceError> {
245            let mut decoder = self.decoder.lock().map_err(|_| SourceError::Transport)?;
246            decoder
247                .next_frame()
248                .map(|frame| frame.map(str::to_owned))
249                .map_err(SourceError::from)
250        }
251    }
252
253    /// A source whose transport has already failed.
254    ///
255    /// [`Replay`] can only ever reach [`SourceError::Framing`], because the real
256    /// decoder turns a failed read into a typed framing answer. The unit
257    /// variant needs a source of its own, and this is the one an adapter looks
258    /// like after its pipe breaks.
259    struct Broken;
260
261    impl FrameSource for Broken {
262        #[expect(
263            clippy::unused_async_trait_impl,
264            reason = "FrameSource is a native async fn trait, so the signature is the trait's and not the fixture's to change; there is nothing to await in reporting a failure that has already happened"
265        )]
266        async fn next_frame(&self) -> Result<Option<String>, SourceError> {
267            Err(SourceError::Transport)
268        }
269    }
270
271    /// Drives a future to its first poll. `None` means it suspended, which these
272    /// sources never do — a future that did would need a real executor, and
273    /// reporting `None` is more honest than pretending it completed.
274    fn drive<F: Future>(future: F) -> Option<F::Output> {
275        let mut context = Context::from_waker(Waker::noop());
276        let mut future = Box::pin(future);
277        match future.as_mut().poll(&mut context) {
278            Poll::Ready(value) => Some(value),
279            Poll::Pending => None,
280        }
281    }
282
283    #[test]
284    fn a_frame_arrives_through_the_generic_spelling() {
285        let source = Replay::new(b"{\"jsonrpc\":\"2.0\"}\n");
286        assert_eq!(
287            drive(FrameSource::next_frame(&source)),
288            Some(Ok(Some(String::from("{\"jsonrpc\":\"2.0\"}"))))
289        );
290    }
291
292    #[test]
293    fn the_boxed_spelling_forwards_to_the_same_implementation() {
294        let source = Replay::new(b"{\"jsonrpc\":\"2.0\"}\n");
295        let boxed: Arc<dyn DynFrameSource> = Arc::new(source);
296        assert_eq!(
297            drive(boxed.next_frame()),
298            Some(Ok(Some(String::from("{\"jsonrpc\":\"2.0\"}"))))
299        );
300    }
301
302    /// The whole reason `Ok(None)` is documented as "the peer closed" and not
303    /// "nothing is ready": the decoder underneath says `None` for exactly the
304    /// second thing, and the port must not pass that answer up.
305    #[test]
306    fn a_partially_read_frame_is_not_reported_as_a_closed_stream() {
307        let source = Replay::new(b"{\"jsonrpc\":\"2.0\"}");
308        assert_eq!(drive(FrameSource::next_frame(&source)), Some(Ok(None)));
309        // The fixture holds no terminator at all, so a faithful implementor
310        // would await more bytes. What must not happen is it answering "the peer
311        // closed" as though a partial read were a completed session, and this
312        // test is the reminder that the two answers are opposites.
313    }
314
315    #[test]
316    fn a_framing_refusal_keeps_its_own_type_through_the_port() {
317        let source = Replay::new(b"{\"a\":\"\xff\"}\n");
318        assert_matches!(
319            drive(FrameSource::next_frame(&source)),
320            Some(Err(SourceError::Framing(FramingError::NotUtf8 { .. })))
321        );
322    }
323
324    #[test]
325    fn a_transport_failure_is_a_domain_error_and_names_no_third_party() {
326        // The point of the unit variant: an adapter's `io::Error` is mapped here
327        // and the application layer never sees a type it does not own.
328        assert_eq!(
329            SourceError::Transport.to_string(),
330            "the transport could not read the next frame"
331        );
332    }
333
334    /// [`SourceError::Transport`] is the one answer that no test above produces
335    /// on its way through the port: `Replay` can only ever reach
336    /// [`SourceError::Framing`], because the real decoder turns every byte-level
337    /// refusal into a typed framing error. The unit variant is the adapter's to
338    /// build — a broken pipe has no bytes to frame — so it needs a source of its
339    /// own here.
340    #[test]
341    fn a_failed_read_stays_failed_and_never_degrades_into_a_closed_stream() {
342        let source = Broken;
343
344        // Asked twice, because the second call is the assertion that carries
345        // weight. A source that answered `Ok(None)` after a failure would be
346        // reporting a clean close, and `Ok(None)` means exactly one thing: the
347        // peer went away. A caller cannot tell a broken transport from a polite
348        // goodbye on the wire, so a failure has to keep being reported rather
349        // than settle into the other answer.
350        for attempt in 0..2 {
351            assert_eq!(
352                drive(FrameSource::next_frame(&source)),
353                Some(Err(SourceError::Transport)),
354                "attempt {attempt}"
355            );
356        }
357    }
358}