Skip to main content

protocol_droid/adapters/primary/transport/http/
frame_payload.rs

1//! What a stream payload has to be able to become on the wire.
2//!
3//! # Why this exists
4//!
5//! Because "the payload is a JSON value" stopped being true when `2024-11-05`
6//! arrived. On Streamable HTTP every frame is a `message` event and every
7//! payload is JSON, so `EventTail<Value>` and a `Borrow<Value>` bound said
8//! everything there was to say. The legacy transport sends one frame that is
9//! **not** JSON: the `endpoint` event, whose data is a bare URI.
10//!
11//! A URI is not JSON. Wrapping it as `Value::String` would put quotes in the
12//! data — `data: "/messages?sessionId=x"` — and a client doing what the
13//! specification says to do with the event, hand that to a URL parser and fail.
14//! The real servers write the bare URI, and so does this crate.
15//!
16//! # Why the trait is here and not in `domain/`
17//!
18//! Because it is an SSE question, not a domain one: it answers "how does this
19//! payload become a frame", and the answer is a field name, an id, and a
20//! serialisation. `EventName` and `SseEvent` are in this tree for the same
21//! reason. A domain that knew about `event:` would be a domain modelling a
22//! wire format, which is the thing the layering exists to prevent.
23//!
24//! # Why it is fallible
25//!
26//! Because encoding can fail and a stream cannot: a payload with no JSON form
27//! — a non-string map key — has no wire representation, and the honest answer
28//! is a refusal rather than a frame that decodes to something else. That
29//! refusal is the server's own bug, which is why the caller reports it as a
30//! `500` and not as the client's fault.
31
32use serde_json::Value;
33
34use crate::domain::EventId;
35
36use super::sse::{SseError, SseEvent};
37
38/// A payload an [`EventTail`](super::EventTail) can put on the wire.
39///
40/// The one method, because it is the only question the ring asks. The
41/// alternative shape — a payload that *is* an [`SseEvent`] — was rejected
42/// because it would push the id out to every call site, and the id is the
43/// ring's business, not the payload's.
44pub trait FramePayload: Send + 'static {
45    /// This payload as one frame, positioned at `id`.
46    ///
47    /// # Errors
48    ///
49    /// [`SseError::Unserialisable`] if the payload has no wire form. The
50    /// payload is this server's own data and the failure is this server's
51    /// fault, so a caller reports it as a `500`.
52    fn to_frame(&self, id: EventId) -> Result<SseEvent, SseError>;
53}
54
55/// A JSON payload becomes a `message` event — unnamed, because Streamable
56/// HTTP does not name its frames and a name here would be a `2026-07-28`
57/// client's problem to have an unexpected field.
58impl FramePayload for Value {
59    fn to_frame(&self, id: EventId) -> Result<SseEvent, SseError> {
60        SseEvent::json(id, self)
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use serde_json::Value;
67
68    use super::FramePayload;
69    use crate::domain::EventId;
70
71    type TestError = Box<dyn std::error::Error>;
72
73    #[test]
74    fn a_value_becomes_an_unnamed_frame_at_its_id() -> Result<(), TestError> {
75        let frame = Value::String("hi".to_owned()).to_frame(EventId::new(4))?;
76        if frame.name().is_some() {
77            return Err("a Value frame came out named".into());
78        }
79        if frame.data() != "\"hi\"" {
80            return Err(format!("encoded as {:?}", frame.data()).into());
81        }
82        Ok(())
83    }
84}