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}