hyperstack_server/websocket/
subscription.rs1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(tag = "type", rename_all = "lowercase")]
6pub enum ClientMessage {
7 Subscribe(Subscription),
9 Unsubscribe(Unsubscription),
11 Ping,
13}
14
15#[derive(Debug, Clone, Serialize, Deserialize)]
17pub struct Subscription {
18 pub view: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
20 pub key: Option<String>,
21 #[serde(skip_serializing_if = "Option::is_none")]
22 pub partition: Option<String>,
23 #[serde(skip_serializing_if = "Option::is_none")]
25 pub take: Option<usize>,
26 #[serde(skip_serializing_if = "Option::is_none")]
28 pub skip: Option<usize>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
33pub struct Unsubscription {
34 pub view: String,
35 #[serde(skip_serializing_if = "Option::is_none")]
36 pub key: Option<String>,
37}
38
39impl Unsubscription {
40 pub fn sub_key(&self) -> String {
42 match &self.key {
43 Some(k) => format!("{}:{}", self.view, k),
44 None => format!("{}:*", self.view),
45 }
46 }
47}
48
49impl Subscription {
50 pub fn matches_view(&self, view_id: &str) -> bool {
51 self.view == view_id
52 }
53
54 pub fn matches_key(&self, key: &str) -> bool {
55 self.key.as_ref().is_none_or(|k| k == key)
56 }
57
58 pub fn matches(&self, view_id: &str, key: &str) -> bool {
59 self.matches_view(view_id) && self.matches_key(key)
60 }
61
62 pub fn sub_key(&self) -> String {
63 match &self.key {
64 Some(k) => format!("{}:{}", self.view, k),
65 None => format!("{}:*", self.view),
66 }
67 }
68}
69
70#[cfg(test)]
71mod tests {
72 use super::*;
73 use serde_json::json;
74
75 #[test]
76 fn test_subscription_parse() {
77 let json = json!({
78 "view": "SettlementGame/list",
79 "key": "835"
80 });
81
82 let sub: Subscription = serde_json::from_value(json).unwrap();
83 assert_eq!(sub.view, "SettlementGame/list");
84 assert_eq!(sub.key, Some("835".to_string()));
85 }
86
87 #[test]
88 fn test_subscription_no_key() {
89 let json = json!({
90 "view": "SettlementGame/list"
91 });
92
93 let sub: Subscription = serde_json::from_value(json).unwrap();
94 assert_eq!(sub.view, "SettlementGame/list");
95 assert!(sub.key.is_none());
96 }
97
98 #[test]
99 fn test_subscription_matches() {
100 let sub = Subscription {
101 view: "SettlementGame/list".to_string(),
102 key: Some("835".to_string()),
103 partition: None,
104 take: None,
105 skip: None,
106 };
107
108 assert!(sub.matches("SettlementGame/list", "835"));
109 assert!(!sub.matches("SettlementGame/list", "836"));
110 assert!(!sub.matches("SettlementGame/state", "835"));
111 }
112
113 #[test]
114 fn test_subscription_matches_all_keys() {
115 let sub = Subscription {
116 view: "SettlementGame/list".to_string(),
117 key: None,
118 partition: None,
119 take: None,
120 skip: None,
121 };
122
123 assert!(sub.matches("SettlementGame/list", "835"));
124 assert!(sub.matches("SettlementGame/list", "836"));
125 assert!(!sub.matches("SettlementGame/state", "835"));
126 }
127
128 #[test]
129 fn test_client_message_subscribe_parse() {
130 let json = json!({
131 "type": "subscribe",
132 "view": "SettlementGame/list",
133 "key": "835"
134 });
135
136 let msg: ClientMessage = serde_json::from_value(json).unwrap();
137 match msg {
138 ClientMessage::Subscribe(sub) => {
139 assert_eq!(sub.view, "SettlementGame/list");
140 assert_eq!(sub.key, Some("835".to_string()));
141 }
142 _ => panic!("Expected Subscribe"),
143 }
144 }
145
146 #[test]
147 fn test_client_message_unsubscribe_parse() {
148 let json = json!({
149 "type": "unsubscribe",
150 "view": "SettlementGame/list",
151 "key": "835"
152 });
153
154 let msg: ClientMessage = serde_json::from_value(json).unwrap();
155 match msg {
156 ClientMessage::Unsubscribe(unsub) => {
157 assert_eq!(unsub.view, "SettlementGame/list");
158 assert_eq!(unsub.key, Some("835".to_string()));
159 }
160 _ => panic!("Expected Unsubscribe"),
161 }
162 }
163
164 #[test]
165 fn test_client_message_ping_parse() {
166 let json = json!({ "type": "ping" });
167
168 let msg: ClientMessage = serde_json::from_value(json).unwrap();
169 assert!(matches!(msg, ClientMessage::Ping));
170 }
171
172 #[test]
173 fn test_legacy_subscription_parse_as_subscribe() {
174 let json = json!({
175 "view": "SettlementGame/list",
176 "key": "835"
177 });
178
179 let sub: Subscription = serde_json::from_value(json).unwrap();
180 assert_eq!(sub.view, "SettlementGame/list");
181 assert_eq!(sub.key, Some("835".to_string()));
182 }
183
184 #[test]
185 fn test_sub_key_with_key() {
186 let sub = Subscription {
187 view: "SettlementGame/list".to_string(),
188 key: Some("835".to_string()),
189 partition: None,
190 take: None,
191 skip: None,
192 };
193 assert_eq!(sub.sub_key(), "SettlementGame/list:835");
194 }
195
196 #[test]
197 fn test_sub_key_without_key() {
198 let sub = Subscription {
199 view: "SettlementGame/list".to_string(),
200 key: None,
201 partition: None,
202 take: None,
203 skip: None,
204 };
205 assert_eq!(sub.sub_key(), "SettlementGame/list:*");
206 }
207
208 #[test]
209 fn test_unsubscription_sub_key() {
210 let unsub = Unsubscription {
211 view: "SettlementGame/list".to_string(),
212 key: Some("835".to_string()),
213 };
214 assert_eq!(unsub.sub_key(), "SettlementGame/list:835");
215
216 let unsub_all = Unsubscription {
217 view: "SettlementGame/list".to_string(),
218 key: None,
219 };
220 assert_eq!(unsub_all.sub_key(), "SettlementGame/list:*");
221 }
222
223 #[test]
224 fn test_subscription_with_take_skip() {
225 let json = json!({
226 "type": "subscribe",
227 "view": "OreRound/latest",
228 "take": 10,
229 "skip": 0
230 });
231
232 let msg: ClientMessage = serde_json::from_value(json).unwrap();
233 match msg {
234 ClientMessage::Subscribe(sub) => {
235 assert_eq!(sub.view, "OreRound/latest");
236 assert_eq!(sub.take, Some(10));
237 assert_eq!(sub.skip, Some(0));
238 }
239 _ => panic!("Expected Subscribe"),
240 }
241 }
242}