phoenix_channel_runtime/
frame.rs1use serde_json::Value;
2use thiserror::Error;
3
4use crate::{EventRoute, Payload, PayloadError};
5
6#[derive(Clone, Debug, PartialEq)]
11pub struct Frame {
12 pub join_ref: Option<String>,
14 pub reference: Option<String>,
16 pub topic: String,
18 pub event: String,
20 pub payload: Payload,
22}
23
24impl Frame {
25 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 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 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 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#[derive(Debug, Error)]
108pub enum FrameCodecError {
109 #[error("failed to decode a Phoenix frame: {0}")]
111 Decode(#[source] serde_json::Error),
112 #[error("failed to encode a Phoenix frame: {0}")]
114 Encode(#[source] serde_json::Error),
115 #[error("Phoenix v2 frame must contain five values, received {0}")]
117 InvalidLength(usize),
118 #[error("Phoenix frame contains an invalid {0} field")]
120 InvalidField(&'static str),
121 #[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}