use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "lowercase")]
pub enum Mode {
State,
Append,
List,
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum SortOrder {
Asc,
Desc,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct SortConfig {
pub field: Vec<String>,
pub order: SortOrder,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct WireFormat {
pub wide_int_paths: Vec<Vec<String>>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubscribedFrame {
pub op: &'static str,
pub view: String,
pub mode: Mode,
#[serde(skip_serializing_if = "Option::is_none")]
pub sort: Option<SortConfig>,
}
impl SubscribedFrame {
pub fn new(view: String, mode: Mode, sort: Option<SortConfig>) -> Self {
Self {
op: "subscribed",
view,
mode,
sort,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Frame {
pub mode: Mode,
#[serde(rename = "entity")]
pub export: String,
pub op: &'static str,
pub key: String,
pub data: serde_json::Value,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub append: Vec<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub seq: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotEntity {
pub key: String,
pub data: serde_json::Value,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SnapshotFrame {
pub mode: Mode,
#[serde(rename = "entity")]
pub export: String,
pub op: &'static str,
pub data: Vec<SnapshotEntity>,
#[serde(default = "default_complete")]
pub complete: bool,
}
fn default_complete() -> bool {
true
}
pub fn apply_wire_format(value: &mut serde_json::Value, wire_format: &WireFormat) {
for path in &wire_format.wide_int_paths {
stringify_value_at_path(value, path);
}
}
fn stringify_value_at_path(value: &mut serde_json::Value, path: &[String]) {
if path.is_empty() {
stringify_wide_int_value(value);
return;
}
match value {
serde_json::Value::Object(map) => {
if let Some(child) = map.get_mut(&path[0]) {
stringify_value_at_path(child, &path[1..]);
}
}
serde_json::Value::Array(values) => {
for child in values {
stringify_value_at_path(child, path);
}
}
_ => {}
}
}
fn stringify_wide_int_value(value: &mut serde_json::Value) {
match value {
serde_json::Value::Number(number) => {
if let Some(unsigned) = number.as_u64() {
*value = serde_json::Value::String(unsigned.to_string());
} else if let Some(signed) = number.as_i64() {
*value = serde_json::Value::String(signed.to_string());
}
}
serde_json::Value::Array(values) => {
for child in values {
stringify_wide_int_value(child);
}
}
_ => {}
}
}
impl Frame {
pub fn entity(&self) -> &str {
&self.export
}
pub fn key(&self) -> &str {
&self.key
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_frame_entity_key_accessors() {
let frame = Frame {
mode: Mode::List,
export: "SettlementGame/list".to_string(),
op: "upsert",
key: "123".to_string(),
data: serde_json::json!({}),
append: vec![],
seq: None,
};
assert_eq!(frame.entity(), "SettlementGame/list");
assert_eq!(frame.key(), "123");
}
#[test]
fn test_frame_serialization() {
let frame = Frame {
mode: Mode::List,
export: "SettlementGame/list".to_string(),
op: "upsert",
key: "123".to_string(),
data: serde_json::json!({"gameId": "123"}),
append: vec![],
seq: None,
};
let json = serde_json::to_value(&frame).unwrap();
assert_eq!(json["op"], "upsert");
assert_eq!(json["mode"], "list");
assert_eq!(json["entity"], "SettlementGame/list");
assert_eq!(json["key"], "123");
}
#[test]
fn test_frame_with_seq() {
let frame = Frame {
mode: Mode::List,
export: "SettlementGame/list".to_string(),
op: "upsert",
key: "123".to_string(),
data: serde_json::json!({"gameId": "123"}),
append: vec![],
seq: Some("123456789:000000000042".to_string()),
};
let json = serde_json::to_value(&frame).unwrap();
assert_eq!(json["op"], "upsert");
assert_eq!(json["seq"], "123456789:000000000042");
}
#[test]
fn test_frame_seq_skipped_when_none() {
let frame = Frame {
mode: Mode::List,
export: "SettlementGame/list".to_string(),
op: "upsert",
key: "123".to_string(),
data: serde_json::json!({"gameId": "123"}),
append: vec![],
seq: None,
};
let json = serde_json::to_value(&frame).unwrap();
assert!(json.get("seq").is_none());
}
#[test]
fn test_snapshot_frame_complete_serialization() {
let frame = SnapshotFrame {
mode: Mode::List,
export: "tokens/list".to_string(),
op: "snapshot",
data: vec![SnapshotEntity {
key: "abc".to_string(),
data: serde_json::json!({"id": "abc"}),
}],
complete: false,
};
let json = serde_json::to_value(&frame).unwrap();
assert_eq!(json["complete"], false);
assert_eq!(json["op"], "snapshot");
}
#[test]
fn test_snapshot_frame_complete_defaults_to_true_on_deserialize() {
#[derive(Debug, Deserialize)]
struct TestSnapshotFrame {
#[allow(dead_code)]
mode: Mode,
#[allow(dead_code)]
#[serde(rename = "entity")]
export: String,
#[allow(dead_code)]
op: String,
#[allow(dead_code)]
data: Vec<SnapshotEntity>,
#[serde(default = "super::default_complete")]
complete: bool,
}
let json_without_complete = serde_json::json!({
"mode": "list",
"entity": "tokens/list",
"op": "snapshot",
"data": []
});
let frame: TestSnapshotFrame = serde_json::from_value(json_without_complete).unwrap();
assert!(frame.complete);
}
#[test]
fn test_snapshot_frame_batching_fields() {
let first_batch = SnapshotFrame {
mode: Mode::List,
export: "tokens/list".to_string(),
op: "snapshot",
data: vec![],
complete: false,
};
let final_batch = SnapshotFrame {
mode: Mode::List,
export: "tokens/list".to_string(),
op: "snapshot",
data: vec![],
complete: true,
};
assert!(!first_batch.complete);
assert!(final_batch.complete);
}
#[test]
fn wire_format_stringifies_marked_wide_int_paths() {
let wire_format = WireFormat {
wide_int_paths: vec![
vec!["amount".to_string()],
vec!["nested".to_string(), "timestamp".to_string()],
vec!["values".to_string()],
vec!["positions".to_string(), "liquidity".to_string()],
],
};
let mut value = serde_json::json!({
"amount": 42,
"nested": { "timestamp": -7 },
"values": [1, 2, 3],
"positions": [
{ "liquidity": 9, "label": "a" },
{ "liquidity": 11, "label": "b" }
],
"small": 5,
});
apply_wire_format(&mut value, &wire_format);
assert_eq!(value["amount"], serde_json::Value::String("42".to_string()));
assert_eq!(
value["nested"]["timestamp"],
serde_json::Value::String("-7".to_string())
);
assert_eq!(
value["values"][0],
serde_json::Value::String("1".to_string())
);
assert_eq!(
value["positions"][1]["liquidity"],
serde_json::Value::String("11".to_string())
);
assert_eq!(value["small"], serde_json::json!(5));
}
}