1use alloc::string::String;
10use alloc::vec::Vec;
11use serde::{Deserialize, Serialize};
12use serde_json::Value as JsonValue;
13
14pub const PROTOCOL_VERSION: &str = "3.0";
16
17pub const VERSION_PARAM: &str = "v";
25
26pub fn ws_url_with_version(url: &str) -> String {
33 let sep = if url.contains('?') { '&' } else { '?' };
34 alloc::format!("{url}{sep}{VERSION_PARAM}={PROTOCOL_VERSION}")
35}
36
37fn protocol_major(version: &str) -> Option<&str> {
40 match version.split('.').next() {
41 Some(major) if !major.is_empty() => Some(major),
42 _ => None,
43 }
44}
45
46pub fn version_compatible(their_version: &str) -> bool {
55 match (
56 protocol_major(their_version),
57 protocol_major(PROTOCOL_VERSION),
58 ) {
59 (Some(theirs), Some(ours)) => theirs == ours,
60 _ => false,
61 }
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
66pub struct HelloMessage {
67 pub version: String,
69
70 pub client: String,
72
73 #[serde(skip_serializing_if = "Option::is_none")]
75 pub capabilities: Option<Vec<String>>,
76
77 #[serde(skip_serializing_if = "Option::is_none")]
79 pub auth_token: Option<String>,
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct WelcomeMessage {
85 pub version: String,
87
88 pub server: String,
90
91 pub permissions: Vec<String>,
93
94 pub writable_records: Vec<String>,
96
97 #[serde(skip_serializing_if = "Option::is_none")]
99 pub max_subscriptions: Option<usize>,
100
101 #[serde(skip_serializing_if = "Option::is_none")]
103 pub authenticated: Option<bool>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct Request {
109 pub id: u64,
111
112 pub method: String,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub params: Option<JsonValue>,
118}
119
120#[derive(Debug, Clone, Serialize, Deserialize)]
122#[serde(untagged)]
123pub enum Response {
124 Success {
126 id: u64,
128 result: JsonValue,
130 },
131 Error {
133 id: u64,
135 error: ErrorObject,
137 },
138}
139
140impl Response {
141 pub fn success(id: u64, result: JsonValue) -> Self {
143 Self::Success { id, result }
144 }
145
146 pub fn error(id: u64, code: impl Into<String>, message: impl Into<String>) -> Self {
148 Self::Error {
149 id,
150 error: ErrorObject {
151 code: code.into(),
152 message: message.into(),
153 details: None,
154 },
155 }
156 }
157
158 pub fn error_with_details(
160 id: u64,
161 code: impl Into<String>,
162 message: impl Into<String>,
163 details: JsonValue,
164 ) -> Self {
165 Self::Error {
166 id,
167 error: ErrorObject {
168 code: code.into(),
169 message: message.into(),
170 details: Some(details),
171 },
172 }
173 }
174}
175
176#[derive(Debug, Clone, Serialize, Deserialize)]
178pub struct ErrorObject {
179 pub code: String,
181
182 pub message: String,
184
185 #[serde(skip_serializing_if = "Option::is_none")]
187 pub details: Option<JsonValue>,
188}
189
190#[derive(Debug, Clone, Serialize, Deserialize)]
192pub struct Event {
193 pub subscription_id: String,
195
196 pub sequence: u64,
198
199 pub data: JsonValue,
201
202 pub timestamp: String,
204
205 #[serde(skip_serializing_if = "Option::is_none")]
207 pub dropped: Option<u64>,
208}
209
210#[cfg(test)]
211mod tests {
212 use super::*;
213 use alloc::string::ToString;
214 use alloc::vec;
215
216 #[test]
217 fn test_hello_serialization() {
218 let hello = HelloMessage {
219 version: PROTOCOL_VERSION.to_string(),
220 client: "test-client".to_string(),
221 capabilities: Some(vec!["read".to_string()]),
222 auth_token: None,
223 };
224
225 let json = serde_json::to_string(&hello).unwrap();
226 assert!(json.contains("\"version\":\"3.0\""));
227 assert!(json.contains("\"client\":\"test-client\""));
228 }
229
230 #[test]
231 fn version_compatible_matches_on_major() {
232 assert!(version_compatible(PROTOCOL_VERSION));
234 assert!(version_compatible("3.0"));
235 assert!(version_compatible("3.7"));
236 assert!(version_compatible("3")); assert!(!version_compatible("2.0"));
240 assert!(!version_compatible("1.0"));
241
242 assert!(!version_compatible(""));
244 assert!(!version_compatible(".0"));
245 assert!(!version_compatible("garbage"));
246 }
247
248 #[test]
249 fn test_request_serialization() {
250 let request = Request {
251 id: 1,
252 method: "record.list".to_string(),
253 params: Some(serde_json::json!({})),
254 };
255
256 let json = serde_json::to_string(&request).unwrap();
257 assert!(json.contains("\"id\":1"));
258 assert!(json.contains("\"method\":\"record.list\""));
259 }
260
261 #[test]
262 fn test_response_success() {
263 let response = Response::success(1, serde_json::json!({"status": "ok"}));
264
265 let json = serde_json::to_string(&response).unwrap();
266 assert!(json.contains("\"id\":1"));
267 assert!(json.contains("\"result\""));
268 assert!(json.contains("\"status\":\"ok\""));
269 }
270
271 #[test]
272 fn test_response_error() {
273 let response = Response::error(2, "NOT_FOUND", "Record not found");
274
275 let json = serde_json::to_string(&response).unwrap();
276 assert!(json.contains("\"id\":2"));
277 assert!(json.contains("\"error\""));
278 assert!(json.contains("\"code\":\"NOT_FOUND\""));
279 assert!(json.contains("\"message\":\"Record not found\""));
280 }
281
282 #[test]
283 fn test_event_serialization() {
284 let event = Event {
285 subscription_id: "sub-123".to_string(),
286 sequence: 42,
287 data: serde_json::json!({"temp": 23.5}),
288 timestamp: "1730379296.123456789".to_string(),
289 dropped: None,
290 };
291
292 let json = serde_json::to_string(&event).unwrap();
293 assert!(json.contains("\"subscription_id\":\"sub-123\""));
294 assert!(json.contains("\"sequence\":42"));
295 assert!(json.contains("\"temp\":23.5"));
296 }
297
298 #[test]
299 fn test_event_with_dropped() {
300 let event = Event {
301 subscription_id: "sub-456".to_string(),
302 sequence: 100,
303 data: serde_json::json!({"value": 1}),
304 timestamp: "1730379300.987654321".to_string(),
305 dropped: Some(5),
306 };
307
308 let json = serde_json::to_string(&event).unwrap();
309 assert!(json.contains("\"dropped\":5"));
310 }
311}