Skip to main content

phoenix_channel_runtime/
frame.rs

1use serde_json::Value;
2use thiserror::Error;
3
4use crate::{EventRoute, Payload, PayloadError};
5
6/// A Phoenix Channels v2 JSON frame.
7///
8/// The serialized representation is
9/// `[join_ref, ref, topic, event, payload]`.
10#[derive(Clone, Debug, PartialEq)]
11pub struct Frame {
12    /// Reference of the channel join generation, when present.
13    pub join_ref: Option<String>,
14    /// Reference used to correlate a push and reply, when present.
15    pub reference: Option<String>,
16    /// Channel topic or the special `phoenix` heartbeat topic.
17    pub topic: String,
18    /// Phoenix or application-defined event name.
19    pub event: String,
20    /// JSON, binary, or reply payload carried by the frame.
21    pub payload: Payload,
22}
23
24impl Frame {
25    /// Creates a protocol frame from its v2 serializer fields.
26    pub fn new(
27        join_ref: Option<String>,
28        reference: Option<String>,
29        topic: impl Into<String>,
30        event: impl Into<String>,
31        payload: impl Into<Payload>,
32    ) -> Self {
33        Self {
34            join_ref,
35            reference,
36            topic: topic.into(),
37            event: event.into(),
38            payload: payload.into(),
39        }
40    }
41
42    /// Encodes this frame using Phoenix's JSON v2 representation.
43    ///
44    /// Binary payloads must be encoded with [`crate::Codec`] instead.
45    pub fn encode_text(&self) -> Result<String, FrameCodecError> {
46        serde_json::to_string(&(
47            &self.join_ref,
48            &self.reference,
49            &self.topic,
50            &self.event,
51            self.payload
52                .as_json()
53                .ok_or(FrameCodecError::BinaryPayloadRequiresBinaryFrame)?,
54        ))
55        .map_err(FrameCodecError::Encode)
56    }
57
58    /// Decodes a Phoenix JSON v2 frame.
59    pub fn decode_text(input: &str) -> Result<Self, FrameCodecError> {
60        let values: Vec<Value> = serde_json::from_str(input).map_err(FrameCodecError::Decode)?;
61        if values.len() != 5 {
62            return Err(FrameCodecError::InvalidLength(values.len()));
63        }
64
65        let join_ref = decode_reference(&values[0], "join_ref")?;
66        let reference = decode_reference(&values[1], "ref")?;
67        let topic = values[2]
68            .as_str()
69            .ok_or(FrameCodecError::InvalidField("topic"))?
70            .to_owned();
71        let event = values[3]
72            .as_str()
73            .ok_or(FrameCodecError::InvalidField("event"))?
74            .to_owned();
75
76        Ok(Self {
77            join_ref,
78            reference,
79            topic,
80            event,
81            payload: Payload::Json(values[4].clone()),
82        })
83    }
84
85    /// Decodes this frame's JSON payload when its event matches `R::EVENT`.
86    ///
87    /// Returns `Ok(None)` for a different event.
88    pub fn route<R: EventRoute>(&self) -> Result<Option<R::Output>, PayloadError> {
89        if self.event == R::EVENT {
90            self.payload.deserialize().map(Some)
91        } else {
92            Ok(None)
93        }
94    }
95}
96
97fn decode_reference(value: &Value, field: &'static str) -> Result<Option<String>, FrameCodecError> {
98    match value {
99        Value::Null => Ok(None),
100        Value::String(value) => Ok(Some(value.clone())),
101        Value::Number(value) => Ok(Some(value.to_string())),
102        _ => Err(FrameCodecError::InvalidField(field)),
103    }
104}
105
106/// Failure while encoding or decoding a Phoenix JSON v2 frame.
107#[derive(Debug, Error)]
108pub enum FrameCodecError {
109    /// The JSON text could not be decoded.
110    #[error("failed to decode a Phoenix frame: {0}")]
111    Decode(#[source] serde_json::Error),
112    /// The frame could not be serialized as JSON.
113    #[error("failed to encode a Phoenix frame: {0}")]
114    Encode(#[source] serde_json::Error),
115    /// The JSON array did not contain exactly five fields.
116    #[error("Phoenix v2 frame must contain five values, received {0}")]
117    InvalidLength(usize),
118    /// A serializer field had the wrong JSON type.
119    #[error("Phoenix frame contains an invalid {0} field")]
120    InvalidField(&'static str),
121    /// A binary payload was passed to the JSON text encoder.
122    #[error("binary payloads require a binary Phoenix frame")]
123    BinaryPayloadRequiresBinaryFrame,
124}
125
126#[cfg(test)]
127mod tests {
128    use serde::Deserialize;
129    use serde_json::json;
130
131    use super::*;
132
133    #[test]
134    fn round_trips_v2_text_frames() {
135        let frame = Frame::new(
136            Some("7".into()),
137            Some("8".into()),
138            "document:123",
139            "update",
140            json!({"body": "hello"}),
141        );
142
143        let encoded = frame.encode_text().unwrap();
144        assert_eq!(Frame::decode_text(&encoded).unwrap(), frame);
145    }
146
147    #[test]
148    fn accepts_numeric_references_from_non_javascript_clients() {
149        let frame = Frame::decode_text(r#"[1,2,"room:lobby","phx_reply",{}]"#).unwrap();
150
151        assert_eq!(frame.join_ref.as_deref(), Some("1"));
152        assert_eq!(frame.reference.as_deref(), Some("2"));
153    }
154
155    #[test]
156    fn rejects_frames_with_the_wrong_shape() {
157        let error = Frame::decode_text(r#"[null,"1","topic","event"]"#).unwrap_err();
158
159        assert!(matches!(error, FrameCodecError::InvalidLength(4)));
160    }
161
162    #[test]
163    fn routes_typed_event_payloads() {
164        #[derive(Deserialize, PartialEq, Debug)]
165        struct Updated {
166            version: u64,
167        }
168
169        struct UpdatedRoute;
170
171        impl EventRoute for UpdatedRoute {
172            const EVENT: &'static str = "updated";
173            type Output = Updated;
174        }
175
176        let frame = Frame::new(None, None, "room", "updated", json!({"version": 4}));
177        assert_eq!(
178            frame.route::<UpdatedRoute>().unwrap(),
179            Some(Updated { version: 4 })
180        );
181    }
182}