1use serde::{Deserialize, Serialize};
4use serde_json::{json, Map, Value};
5use uuid::Uuid;
6
7use crate::VERSION;
8
9pub const PROTO_VERSION: &str = "1";
11
12pub const CLIENT_VERSION: &str = VERSION;
14
15pub const DEFAULT_CLIENT_CAPABILITIES: &[&str] = &["streaming", "batch", "heartbeat", "receipts"];
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(rename_all = "snake_case")]
21pub enum MessageType {
22 ConnectionInit,
24 ConnectionAck,
26 Request,
28 Response,
30 Notification,
32 Subscribe,
34 Next,
36 Error,
38 Complete,
40 Unsubscribe,
42 Ping,
44 Pong,
46 ReceiptResponse,
48 Disconnect,
50 Status,
52}
53
54impl MessageType {
55 pub fn as_str(self) -> &'static str {
57 match self {
58 Self::ConnectionInit => "connection_init",
59 Self::ConnectionAck => "connection_ack",
60 Self::Request => "request",
61 Self::Response => "response",
62 Self::Notification => "notification",
63 Self::Subscribe => "subscribe",
64 Self::Next => "next",
65 Self::Error => "error",
66 Self::Complete => "complete",
67 Self::Unsubscribe => "unsubscribe",
68 Self::Ping => "ping",
69 Self::Pong => "pong",
70 Self::ReceiptResponse => "receipt_response",
71 Self::Disconnect => "disconnect",
72 Self::Status => "status",
73 }
74 }
75}
76
77#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
79pub struct ErrorObject {
80 pub code: i64,
82 pub message: String,
84 #[serde(default, skip_serializing_if = "Option::is_none")]
86 pub data: Option<Value>,
87}
88
89#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
91pub struct Envelope {
92 pub proto: String,
94 #[serde(rename = "type")]
96 pub msg_type: String,
97 #[serde(default, skip_serializing_if = "Option::is_none")]
99 pub method: Option<String>,
100 #[serde(default, skip_serializing_if = "Option::is_none")]
102 pub params: Option<Map<String, Value>>,
103 #[serde(default, skip_serializing_if = "Option::is_none")]
105 pub id: Option<String>,
106 #[serde(default, skip_serializing_if = "Option::is_none")]
108 pub result: Option<Map<String, Value>>,
109 #[serde(default, skip_serializing_if = "Option::is_none")]
111 pub error: Option<ErrorObject>,
112 #[serde(default, skip_serializing_if = "Option::is_none")]
114 pub payload: Option<Value>,
115 #[serde(default, skip_serializing_if = "Option::is_none")]
117 pub receipt: Option<String>,
118 #[serde(flatten)]
120 pub extra: Map<String, Value>,
121}
122
123impl Envelope {
124 pub fn to_wire_json(&self) -> Result<String, serde_json::Error> {
126 serde_json::to_string(self)
127 }
128
129 pub fn to_value(&self) -> Value {
131 serde_json::to_value(self).unwrap_or(Value::Null)
132 }
133}
134
135pub fn new_request_id() -> String {
137 Uuid::new_v4().simple().to_string()
138}
139
140pub fn new_request(method: impl Into<String>, params: Map<String, Value>) -> Envelope {
142 Envelope {
143 proto: PROTO_VERSION.to_string(),
144 msg_type: "request".into(),
145 method: Some(method.into()),
146 params: if params.is_empty() {
147 None
148 } else {
149 Some(params)
150 },
151 id: Some(new_request_id()),
152 result: None,
153 error: None,
154 payload: None,
155 receipt: None,
156 extra: Map::new(),
157 }
158}
159
160pub fn new_notification(method: impl Into<String>, params: Map<String, Value>) -> Envelope {
162 Envelope {
163 proto: PROTO_VERSION.to_string(),
164 msg_type: "notification".into(),
165 method: Some(method.into()),
166 params: if params.is_empty() {
167 None
168 } else {
169 Some(params)
170 },
171 id: None,
172 result: None,
173 error: None,
174 payload: None,
175 receipt: None,
176 extra: Map::new(),
177 }
178}
179
180pub fn new_subscribe(method: impl Into<String>, params: Map<String, Value>) -> Envelope {
182 Envelope {
183 proto: PROTO_VERSION.to_string(),
184 msg_type: "subscribe".into(),
185 method: Some(method.into()),
186 params: if params.is_empty() {
187 None
188 } else {
189 Some(params)
190 },
191 id: Some(new_request_id()),
192 result: None,
193 error: None,
194 payload: None,
195 receipt: None,
196 extra: Map::new(),
197 }
198}
199
200pub fn new_unsubscribe(id: impl Into<String>) -> Envelope {
202 Envelope {
203 proto: PROTO_VERSION.to_string(),
204 msg_type: "unsubscribe".into(),
205 method: None,
206 params: None,
207 id: Some(id.into()),
208 result: None,
209 error: None,
210 payload: None,
211 receipt: None,
212 extra: Map::new(),
213 }
214}
215
216pub fn new_connection_init() -> Envelope {
218 let mut params = Map::new();
219 params.insert("client_version".into(), json!(CLIENT_VERSION));
220 params.insert("client_name".into(), json!("soothe-client-rust"));
221 params.insert("accept_proto".into(), json!([PROTO_VERSION]));
222 params.insert("capabilities".into(), json!(DEFAULT_CLIENT_CAPABILITIES));
223 Envelope {
224 proto: PROTO_VERSION.to_string(),
225 msg_type: "connection_init".into(),
226 method: None,
227 params: Some(params),
228 id: None,
229 result: None,
230 error: None,
231 payload: None,
232 receipt: None,
233 extra: Map::new(),
234 }
235}
236
237pub fn new_ping() -> Envelope {
239 Envelope {
240 proto: PROTO_VERSION.to_string(),
241 msg_type: "ping".into(),
242 method: None,
243 params: None,
244 id: None,
245 result: None,
246 error: None,
247 payload: None,
248 receipt: None,
249 extra: Map::new(),
250 }
251}
252
253pub fn new_pong() -> Envelope {
255 Envelope {
256 proto: PROTO_VERSION.to_string(),
257 msg_type: "pong".into(),
258 method: None,
259 params: None,
260 id: None,
261 result: None,
262 error: None,
263 payload: None,
264 receipt: None,
265 extra: Map::new(),
266 }
267}
268
269pub fn new_disconnect() -> Envelope {
271 new_notification("disconnect", Map::new())
272}
273
274pub fn decode_message(text: &str) -> Result<Value, serde_json::Error> {
276 let trimmed = text.trim();
278 if trimmed.contains('\n') {
279 for line in trimmed.lines() {
280 let line = line.trim();
281 if !line.is_empty() {
282 return serde_json::from_str(line);
283 }
284 }
285 }
286 serde_json::from_str(trimmed)
287}
288
289pub fn expand_wire_messages(msg: Value) -> Vec<Value> {
291 let Some(obj) = msg.as_object() else {
292 return vec![msg];
293 };
294 if obj.get("type").and_then(|v| v.as_str()) != Some("event_batch") {
295 return vec![msg];
296 }
297 match obj.get("events").and_then(|v| v.as_array()) {
298 Some(events) if !events.is_empty() => events.clone(),
299 _ => vec![msg],
300 }
301}
302
303pub fn unwrap_next(msg: &Value) -> Value {
308 let Some(obj) = msg.as_object() else {
309 return msg.clone();
310 };
311 if obj.get("type").and_then(|v| v.as_str()) != Some("next") {
312 return msg.clone();
313 }
314 let Some(payload) = obj.get("payload").and_then(|p| p.as_object()) else {
315 return msg.clone();
316 };
317 if let Some(data) = payload.get("data") {
318 if data.is_object() {
319 return data.clone();
320 }
321 }
322 msg.clone()
323}
324
325pub fn as_str(v: &Value) -> &str {
327 v.as_str().unwrap_or("")
328}
329
330pub fn params_map(pairs: &[(&str, Value)]) -> Map<String, Value> {
332 let mut m = Map::new();
333 for (k, v) in pairs {
334 m.insert((*k).to_string(), v.clone());
335 }
336 m
337}
338
339#[cfg(test)]
340mod tests {
341 use super::*;
342
343 #[test]
344 fn request_id_is_32_hex() {
345 let id = new_request_id();
346 assert_eq!(id.len(), 32);
347 assert!(id.chars().all(|c| c.is_ascii_hexdigit()));
348 }
349
350 #[test]
351 fn connection_init_shape() {
352 let env = new_connection_init();
353 assert_eq!(env.msg_type, "connection_init");
354 assert_eq!(env.proto, "1");
355 let params = env.params.unwrap();
356 assert_eq!(params["client_name"], json!("soothe-client-rust"));
357 }
358
359 #[test]
360 fn expand_event_batch() {
361 let batch = json!({
362 "type": "event_batch",
363 "events": [{"type":"event","mode":"messages"}, {"type":"status","state":"idle"}]
364 });
365 let expanded = expand_wire_messages(batch);
366 assert_eq!(expanded.len(), 2);
367 }
368
369 #[test]
370 fn unwrap_next_returns_payload_data() {
371 let frame = json!({
372 "type": "next",
373 "payload": {
374 "mode": "event",
375 "data": {"type": "event", "mode": "messages", "data": [{"content": "hi"}]}
376 }
377 });
378 let inner = unwrap_next(&frame);
379 assert_eq!(inner["mode"], json!("messages"));
380 assert_eq!(inner["type"], json!("event"));
381 }
382}