Skip to main content

solana_dynamic_events/
lib.rs

1//! Schema-compatible dynamic key-value events for Solana programs.
2
3use std::collections::BTreeMap;
4
5use base64::{engine::general_purpose::STANDARD, Engine};
6use thiserror::Error;
7use wincode_derive::{SchemaRead, SchemaWrite};
8
9/// Errors produced during serialization, deserialization, or base64 encoding/decoding.
10#[derive(Debug, Error)]
11pub enum DynamicEventError {
12    /// Wincode serialization failed.
13    #[error(transparent)]
14    Serialize(#[from] wincode::WriteError),
15    /// Wincode deserialization failed.
16    #[error(transparent)]
17    Deserialize(#[from] wincode::ReadError),
18    /// Base64 decoding failed.
19    #[error(transparent)]
20    Base64Decode(#[from] base64::DecodeError),
21}
22
23/// Schema-compatible key-value event backed by a deterministically-ordered `BTreeMap`.
24#[derive(SchemaWrite, SchemaRead, PartialEq, Debug, Default)]
25pub struct DynamicEvent {
26    /// Ordered key-value store holding the event payload.
27    pub data: BTreeMap<String, String>,
28}
29
30impl DynamicEvent {
31    /// Creates an empty event.
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Inserts a key-value pair, returning the previous value if the key already existed.
37    pub fn insert(&mut self, key: impl Into<String>, value: impl Into<String>) -> Option<String> {
38        self.data.insert(key.into(), value.into())
39    }
40
41    /// Removes a key, returning its value if it was present.
42    pub fn remove(&mut self, key: &str) -> Option<String> {
43        self.data.remove(key)
44    }
45
46    /// Wincode-serializes the event into bytes.
47    pub fn serialize(&self) -> Result<Vec<u8>, DynamicEventError> {
48        Ok(wincode::serialize(self)?)
49    }
50
51    /// Wincode-deserializes an event from bytes.
52    pub fn deserialize(bytes: &[u8]) -> Result<Self, DynamicEventError> {
53        Ok(wincode::deserialize(bytes)?)
54    }
55
56    /// Serializes the event and base64-encodes the result.
57    pub fn to_base64(&self) -> Result<String, DynamicEventError> {
58        let bytes = self.serialize()?;
59        Ok(STANDARD.encode(&bytes))
60    }
61
62    /// Base64-decodes a string and deserializes the resulting bytes into an event.
63    pub fn from_base64(encoded: &str) -> Result<Self, DynamicEventError> {
64        let bytes = STANDARD.decode(encoded)?;
65        Self::deserialize(&bytes)
66    }
67}
68
69#[cfg(test)]
70mod tests {
71    use super::*;
72
73    #[test]
74    fn insert_and_remove() -> Result<(), Box<dyn std::error::Error>> {
75        let mut event = DynamicEvent::new();
76
77        let prev = event.insert("hello", "world");
78        assert!(prev.is_none());
79
80        let prev = event.insert("hello", "updated");
81        assert_eq!(prev.as_deref(), Some("world"));
82
83        let removed = event.remove("hello");
84        assert_eq!(removed.as_deref(), Some("updated"));
85
86        let removed = event.remove("hello");
87        assert!(removed.is_none());
88
89        Ok(())
90    }
91
92    #[test]
93    fn serialize_deserialize_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
94        let mut event = DynamicEvent::new();
95        event.insert("hello", "world");
96        event.insert("foo", "bar");
97
98        let bytes = event.serialize()?;
99        let restored = DynamicEvent::deserialize(&bytes)?;
100
101        assert_eq!(event, restored);
102        Ok(())
103    }
104
105    #[test]
106    fn base64_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
107        let mut event = DynamicEvent::new();
108        event.insert("key1", "value1");
109        event.insert("key2", "value2");
110
111        let encoded = event.to_base64()?;
112        let restored = DynamicEvent::from_base64(&encoded)?;
113
114        assert_eq!(event, restored);
115        Ok(())
116    }
117
118    #[test]
119    fn empty_event_roundtrip() -> Result<(), Box<dyn std::error::Error>> {
120        let event = DynamicEvent::new();
121
122        let bytes = event.serialize()?;
123        let restored = DynamicEvent::deserialize(&bytes)?;
124        assert_eq!(event, restored);
125
126        let encoded = event.to_base64()?;
127        let restored = DynamicEvent::from_base64(&encoded)?;
128        assert_eq!(event, restored);
129
130        Ok(())
131    }
132
133    #[test]
134    fn deserialize_invalid_bytes() {
135        let garbage = &[0xFF, 0xFE, 0xFD, 0xFC, 0xFB];
136        let result = DynamicEvent::deserialize(garbage);
137        assert!(result.is_err());
138    }
139
140    #[test]
141    fn from_base64_invalid_encoding() {
142        let result = DynamicEvent::from_base64("not-valid-base64!!!");
143        assert!(result.is_err());
144    }
145
146    #[test]
147    fn numeric_key_value_permutations() -> Result<(), Box<dyn std::error::Error>> {
148        let u64_val: u64 = 1_000_000;
149        let u8_val: u8 = 255;
150        let f64_val: f64 = 3.14159;
151
152        let mut event = DynamicEvent::new();
153
154        event.insert(u64_val.to_string(), u64_val.to_string());
155        event.insert(u8_val.to_string(), u64_val.to_string());
156        event.insert(f64_val.to_string(), u64_val.to_string());
157
158        event.insert(u64_val.to_string(), u8_val.to_string());
159        event.insert(u8_val.to_string(), u8_val.to_string());
160        event.insert(f64_val.to_string(), u8_val.to_string());
161
162        event.insert(u64_val.to_string(), f64_val.to_string());
163        event.insert(u8_val.to_string(), f64_val.to_string());
164        event.insert(f64_val.to_string(), f64_val.to_string());
165
166        assert_eq!(event.data.len(), 3);
167
168        let encoded = event.to_base64()?;
169        let restored = DynamicEvent::from_base64(&encoded)?;
170        assert_eq!(event, restored);
171
172        Ok(())
173    }
174}