1use serde::{Deserialize, Serialize};
2
3use crate::websocket::subscription::{SubscriptionQuery, PROTOCOL_VERSION};
4
5#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
6#[serde(rename_all = "lowercase")]
7pub enum Mode {
8 State,
9 Append,
10 List,
11}
12
13#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
14#[serde(rename_all = "lowercase")]
15pub enum SortOrder {
16 Asc,
17 Desc,
18}
19
20#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
21pub struct SortConfig {
22 pub field: Vec<String>,
23 pub order: SortOrder,
24}
25
26#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize)]
27pub struct WireFormat {
28 pub wide_int_paths: Vec<Vec<String>>,
29}
30
31#[derive(Debug, Clone, Serialize, Deserialize)]
32#[serde(rename_all = "camelCase")]
33pub struct SubscribedFrame {
34 pub protocol_version: u8,
35 pub subscription_id: String,
36 pub op: &'static str,
37 pub query: SubscriptionQuery,
38 pub mode: Mode,
39 #[serde(skip_serializing_if = "Option::is_none")]
40 pub sort: Option<SortConfig>,
41 #[serde(skip_serializing_if = "Option::is_none")]
44 pub replay_window: Option<crate::journal::ReplayWindow>,
45 #[serde(default)]
50 pub whole_entities: bool,
51}
52
53impl SubscribedFrame {
54 pub fn new(
55 subscription_id: String,
56 query: SubscriptionQuery,
57 mode: Mode,
58 sort: Option<SortConfig>,
59 ) -> Self {
60 Self {
61 protocol_version: PROTOCOL_VERSION,
62 subscription_id,
63 op: "subscribed",
64 query,
65 mode,
66 sort,
67 replay_window: None,
68 whole_entities: true,
69 }
70 }
71
72 pub fn with_replay_window(mut self, window: crate::journal::ReplayWindow) -> Self {
74 self.replay_window = Some(window);
75 self
76 }
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
80#[serde(rename_all = "camelCase")]
81pub struct UnsubscribedFrame {
82 pub protocol_version: u8,
83 pub subscription_id: String,
84 pub op: &'static str,
85}
86
87impl UnsubscribedFrame {
88 pub fn new(subscription_id: String) -> Self {
89 Self {
90 protocol_version: PROTOCOL_VERSION,
91 subscription_id,
92 op: "unsubscribed",
93 }
94 }
95}
96
97#[derive(Debug, Clone, Serialize, Deserialize)]
98#[serde(rename_all = "camelCase")]
99pub struct Frame {
100 pub protocol_version: u8,
101 pub subscription_id: String,
102 pub mode: Mode,
103 #[serde(rename = "entity")]
104 pub export: String,
105 pub op: String,
106 pub key: String,
107 pub data: serde_json::Value,
108 #[serde(skip_serializing_if = "Vec::is_empty", default)]
109 pub append: Vec<String>,
110 #[serde(skip_serializing_if = "Option::is_none")]
111 pub seq: Option<String>,
112}
113
114impl Frame {
115 pub fn scoped(
116 subscription_id: impl Into<String>,
117 mode: Mode,
118 export: impl Into<String>,
119 op: impl Into<String>,
120 key: impl Into<String>,
121 data: serde_json::Value,
122 seq: Option<String>,
123 ) -> Self {
124 Self {
125 protocol_version: PROTOCOL_VERSION,
126 subscription_id: subscription_id.into(),
127 mode,
128 export: export.into(),
129 op: op.into(),
130 key: key.into(),
131 data,
132 append: vec![],
133 seq,
134 }
135 }
136
137 pub fn entity(&self) -> &str {
138 &self.export
139 }
140
141 pub fn key(&self) -> &str {
142 &self.key
143 }
144}
145
146#[derive(Debug, Clone, Serialize)]
148pub(crate) struct SourceFrame {
149 pub mode: Mode,
150 #[serde(rename = "entity")]
151 pub export: String,
152 pub op: &'static str,
153 pub key: String,
154 pub data: serde_json::Value,
155 #[serde(skip_serializing_if = "Vec::is_empty", default)]
156 pub append: Vec<String>,
157 #[serde(skip_serializing_if = "Option::is_none")]
158 pub seq: Option<String>,
159 #[serde(skip_serializing_if = "Option::is_none")]
162 pub offset: Option<u64>,
163}
164
165#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
166pub struct SnapshotEntity {
167 pub key: String,
168 pub data: serde_json::Value,
169}
170
171#[derive(Debug, Clone, Serialize, Deserialize)]
172#[serde(rename_all = "camelCase")]
173pub struct SnapshotFrame {
174 pub protocol_version: u8,
175 pub subscription_id: String,
176 pub snapshot_id: String,
177 pub authoritative: bool,
178 pub mode: Mode,
179 #[serde(rename = "entity")]
180 pub export: String,
181 pub op: &'static str,
182 #[serde(skip_serializing_if = "Option::is_none")]
183 pub key: Option<String>,
184 pub data: Vec<SnapshotEntity>,
185 pub complete: bool,
186}
187
188pub fn apply_wire_format(value: &mut serde_json::Value, wire_format: &WireFormat) {
189 for path in &wire_format.wide_int_paths {
190 stringify_value_at_path(value, path);
191 }
192}
193
194fn stringify_value_at_path(value: &mut serde_json::Value, path: &[String]) {
195 if path.is_empty() {
196 stringify_wide_int_value(value);
197 return;
198 }
199
200 match value {
201 serde_json::Value::Object(map) => {
202 if let Some(child) = map.get_mut(&path[0]) {
203 stringify_value_at_path(child, &path[1..]);
204 }
205 }
206 serde_json::Value::Array(values) => {
207 for child in values {
208 stringify_value_at_path(child, path);
209 }
210 }
211 _ => {}
212 }
213}
214
215fn stringify_wide_int_value(value: &mut serde_json::Value) {
216 match value {
217 serde_json::Value::Number(number) => {
218 if let Some(unsigned) = number.as_u64() {
219 *value = serde_json::Value::String(unsigned.to_string());
220 } else if let Some(signed) = number.as_i64() {
221 *value = serde_json::Value::String(signed.to_string());
222 }
223 }
224 serde_json::Value::Array(values) => {
225 for child in values {
226 stringify_wide_int_value(child);
227 }
228 }
229 _ => {}
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use serde_json::json;
237
238 #[test]
239 fn scoped_live_frames_carry_v2_identity() {
240 let frame = Frame::scoped(
241 "rounds:1",
242 Mode::List,
243 "OreRound/latest",
244 "upsert",
245 "123",
246 json!({"id": 123}),
247 Some("10:000000000001".to_string()),
248 );
249 let value = serde_json::to_value(frame).unwrap();
250 assert_eq!(value["protocolVersion"], 2);
251 assert_eq!(value["subscriptionId"], "rounds:1");
252 assert_eq!(value["seq"], "10:000000000001");
253 }
254
255 #[test]
256 fn snapshot_serializes_conformance_metadata() {
257 let frame = SnapshotFrame {
258 protocol_version: PROTOCOL_VERSION,
259 subscription_id: "rounds:1".to_string(),
260 snapshot_id: "snapshot-1".to_string(),
261 authoritative: true,
262 mode: Mode::List,
263 export: "OreRound/latest".to_string(),
264 op: "snapshot",
265 key: None,
266 data: vec![],
267 complete: true,
268 };
269 let value = serde_json::to_value(frame).unwrap();
270 assert_eq!(value["snapshotId"], "snapshot-1");
271 assert_eq!(value["authoritative"], true);
272 assert_eq!(value["complete"], true);
273 }
274
275 #[test]
276 fn wire_format_stringifies_marked_wide_int_paths() {
277 let wire_format = WireFormat {
278 wide_int_paths: vec![
279 vec!["amount".to_string()],
280 vec!["positions".to_string(), "liquidity".to_string()],
281 ],
282 };
283 let mut value = json!({
284 "amount": 42,
285 "positions": [{"liquidity": 9}, {"liquidity": 11}],
286 "small": 5,
287 });
288 apply_wire_format(&mut value, &wire_format);
289 assert_eq!(value["amount"], "42");
290 assert_eq!(value["positions"][1]["liquidity"], "11");
291 assert_eq!(value["small"], 5);
292 }
293}