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