hyperstack_sdk/
frame.rs

1use flate2::read::GzDecoder;
2use serde::{Deserialize, Serialize};
3use std::io::Read;
4
5const GZIP_MAGIC: [u8; 2] = [0x1f, 0x8b];
6
7fn is_gzip(data: &[u8]) -> bool {
8    data.len() >= 2 && data[0] == GZIP_MAGIC[0] && data[1] == GZIP_MAGIC[1]
9}
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum Mode {
14    State,
15    Append,
16    List,
17}
18
19#[derive(Debug, Clone, Copy, PartialEq, Eq)]
20pub enum Operation {
21    Upsert,
22    Patch,
23    Delete,
24    Create,
25    Snapshot,
26}
27
28impl std::str::FromStr for Operation {
29    type Err = std::convert::Infallible;
30
31    fn from_str(s: &str) -> Result<Self, Self::Err> {
32        Ok(match s {
33            "upsert" => Operation::Upsert,
34            "patch" => Operation::Patch,
35            "delete" => Operation::Delete,
36            "create" => Operation::Create,
37            "snapshot" => Operation::Snapshot,
38            _ => Operation::Upsert,
39        })
40    }
41}
42
43#[derive(Debug, Clone, Serialize, Deserialize)]
44pub struct Frame {
45    pub mode: Mode,
46    #[serde(rename = "entity")]
47    pub entity: String,
48    pub op: String,
49    #[serde(default)]
50    pub key: String,
51    pub data: serde_json::Value,
52    #[serde(default)]
53    pub append: Vec<String>,
54}
55
56impl Frame {
57    pub fn entity_name(&self) -> &str {
58        &self.entity
59    }
60
61    pub fn operation(&self) -> Operation {
62        self.op.parse().unwrap()
63    }
64
65    pub fn is_snapshot(&self) -> bool {
66        self.op == "snapshot"
67    }
68}
69
70#[derive(Debug, Clone, Serialize, Deserialize)]
71pub struct SnapshotEntity {
72    pub key: String,
73    pub data: serde_json::Value,
74}
75
76fn decompress_gzip(data: &[u8]) -> Result<String, Box<dyn std::error::Error>> {
77    let mut decoder = GzDecoder::new(data);
78    let mut decompressed = String::new();
79    decoder.read_to_string(&mut decompressed)?;
80    Ok(decompressed)
81}
82
83pub fn parse_frame(bytes: &[u8]) -> Result<Frame, serde_json::Error> {
84    if is_gzip(bytes) {
85        if let Ok(decompressed) = decompress_gzip(bytes) {
86            return serde_json::from_str(&decompressed);
87        }
88    }
89
90    let text = String::from_utf8_lossy(bytes);
91    serde_json::from_str(&text)
92}
93
94pub fn parse_snapshot_entities(data: &serde_json::Value) -> Vec<SnapshotEntity> {
95    match data {
96        serde_json::Value::Array(arr) => arr
97            .iter()
98            .filter_map(|v| serde_json::from_value(v.clone()).ok())
99            .collect(),
100        _ => Vec::new(),
101    }
102}
103
104#[cfg(test)]
105mod tests {
106    use super::*;
107    use flate2::{write::GzEncoder, Compression};
108    use std::io::Write;
109
110    #[test]
111    fn test_parse_uncompressed_frame() {
112        let frame_json = r#"{"mode":"list","entity":"test/list","op":"snapshot","key":"","data":[{"key":"1","data":{"id":1}}]}"#;
113        let frame = parse_frame(frame_json.as_bytes()).unwrap();
114        assert_eq!(frame.op, "snapshot");
115        assert_eq!(frame.entity, "test/list");
116    }
117
118    #[test]
119    fn test_parse_raw_gzip_frame() {
120        let original = r#"{"mode":"list","entity":"test/list","op":"snapshot","key":"","data":[{"key":"1","data":{"id":1}}]}"#;
121
122        let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
123        encoder.write_all(original.as_bytes()).unwrap();
124        let compressed = encoder.finish().unwrap();
125
126        assert!(is_gzip(&compressed));
127
128        let frame = parse_frame(&compressed).unwrap();
129        assert_eq!(frame.op, "snapshot");
130        assert_eq!(frame.entity, "test/list");
131    }
132
133    #[test]
134    fn test_gzip_magic_detection() {
135        assert!(is_gzip(&[0x1f, 0x8b, 0x08]));
136        assert!(!is_gzip(&[0x7b, 0x22]));
137        assert!(!is_gzip(&[0x1f]));
138        assert!(!is_gzip(&[]));
139    }
140}