hyperstack_server/websocket/
subscription.rs

1use serde::{Deserialize, Serialize};
2
3/// Client message types for subscription management
4#[derive(Debug, Clone, Serialize, Deserialize)]
5#[serde(tag = "type", rename_all = "lowercase")]
6pub enum ClientMessage {
7    /// Subscribe to a view
8    Subscribe(Subscription),
9    /// Unsubscribe from a view
10    Unsubscribe(Unsubscription),
11    /// Keep-alive ping (no response needed)
12    Ping,
13}
14
15/// Client subscription to a specific view
16#[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}
24
25/// Client unsubscription request
26#[derive(Debug, Clone, Serialize, Deserialize)]
27pub struct Unsubscription {
28    pub view: String,
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub key: Option<String>,
31}
32
33impl Unsubscription {
34    /// Generate the subscription key used for tracking
35    pub fn sub_key(&self) -> String {
36        match &self.key {
37            Some(k) => format!("{}:{}", self.view, k),
38            None => format!("{}:*", self.view),
39        }
40    }
41}
42
43impl Subscription {
44    pub fn matches_view(&self, view_id: &str) -> bool {
45        self.view == view_id
46    }
47
48    pub fn matches_key(&self, key: &str) -> bool {
49        self.key.as_ref().is_none_or(|k| k == key)
50    }
51
52    pub fn matches(&self, view_id: &str, key: &str) -> bool {
53        self.matches_view(view_id) && self.matches_key(key)
54    }
55
56    pub fn sub_key(&self) -> String {
57        match &self.key {
58            Some(k) => format!("{}:{}", self.view, k),
59            None => format!("{}:*", self.view),
60        }
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67    use serde_json::json;
68
69    #[test]
70    fn test_subscription_parse() {
71        let json = json!({
72            "view": "SettlementGame/list",
73            "key": "835"
74        });
75
76        let sub: Subscription = serde_json::from_value(json).unwrap();
77        assert_eq!(sub.view, "SettlementGame/list");
78        assert_eq!(sub.key, Some("835".to_string()));
79    }
80
81    #[test]
82    fn test_subscription_no_key() {
83        let json = json!({
84            "view": "SettlementGame/list"
85        });
86
87        let sub: Subscription = serde_json::from_value(json).unwrap();
88        assert_eq!(sub.view, "SettlementGame/list");
89        assert!(sub.key.is_none());
90    }
91
92    #[test]
93    fn test_subscription_matches() {
94        let sub = Subscription {
95            view: "SettlementGame/list".to_string(),
96            key: Some("835".to_string()),
97            partition: None,
98        };
99
100        assert!(sub.matches("SettlementGame/list", "835"));
101        assert!(!sub.matches("SettlementGame/list", "836"));
102        assert!(!sub.matches("SettlementGame/state", "835"));
103    }
104
105    #[test]
106    fn test_subscription_matches_all_keys() {
107        let sub = Subscription {
108            view: "SettlementGame/list".to_string(),
109            key: None,
110            partition: None,
111        };
112
113        assert!(sub.matches("SettlementGame/list", "835"));
114        assert!(sub.matches("SettlementGame/list", "836"));
115        assert!(!sub.matches("SettlementGame/state", "835"));
116    }
117
118    #[test]
119    fn test_client_message_subscribe_parse() {
120        let json = json!({
121            "type": "subscribe",
122            "view": "SettlementGame/list",
123            "key": "835"
124        });
125
126        let msg: ClientMessage = serde_json::from_value(json).unwrap();
127        match msg {
128            ClientMessage::Subscribe(sub) => {
129                assert_eq!(sub.view, "SettlementGame/list");
130                assert_eq!(sub.key, Some("835".to_string()));
131            }
132            _ => panic!("Expected Subscribe"),
133        }
134    }
135
136    #[test]
137    fn test_client_message_unsubscribe_parse() {
138        let json = json!({
139            "type": "unsubscribe",
140            "view": "SettlementGame/list",
141            "key": "835"
142        });
143
144        let msg: ClientMessage = serde_json::from_value(json).unwrap();
145        match msg {
146            ClientMessage::Unsubscribe(unsub) => {
147                assert_eq!(unsub.view, "SettlementGame/list");
148                assert_eq!(unsub.key, Some("835".to_string()));
149            }
150            _ => panic!("Expected Unsubscribe"),
151        }
152    }
153
154    #[test]
155    fn test_client_message_ping_parse() {
156        let json = json!({ "type": "ping" });
157
158        let msg: ClientMessage = serde_json::from_value(json).unwrap();
159        assert!(matches!(msg, ClientMessage::Ping));
160    }
161
162    #[test]
163    fn test_legacy_subscription_parse_as_subscribe() {
164        let json = json!({
165            "view": "SettlementGame/list",
166            "key": "835"
167        });
168
169        let sub: Subscription = serde_json::from_value(json).unwrap();
170        assert_eq!(sub.view, "SettlementGame/list");
171        assert_eq!(sub.key, Some("835".to_string()));
172    }
173
174    #[test]
175    fn test_sub_key_with_key() {
176        let sub = Subscription {
177            view: "SettlementGame/list".to_string(),
178            key: Some("835".to_string()),
179            partition: None,
180        };
181        assert_eq!(sub.sub_key(), "SettlementGame/list:835");
182    }
183
184    #[test]
185    fn test_sub_key_without_key() {
186        let sub = Subscription {
187            view: "SettlementGame/list".to_string(),
188            key: None,
189            partition: None,
190        };
191        assert_eq!(sub.sub_key(), "SettlementGame/list:*");
192    }
193
194    #[test]
195    fn test_unsubscription_sub_key() {
196        let unsub = Unsubscription {
197            view: "SettlementGame/list".to_string(),
198            key: Some("835".to_string()),
199        };
200        assert_eq!(unsub.sub_key(), "SettlementGame/list:835");
201
202        let unsub_all = Unsubscription {
203            view: "SettlementGame/list".to_string(),
204            key: None,
205        };
206        assert_eq!(unsub_all.sub_key(), "SettlementGame/list:*");
207    }
208}