Skip to main content

starlang_core/
message.rs

1//! Term serialization trait.
2//!
3//! The [`Term`] trait provides a common interface for encoding and decoding
4//! Erlang-like terms. Any type that implements `Serialize + DeserializeOwned`
5//! can be used as a Term, similar to how Erlang treats all values as terms.
6//!
7//! Terms are used throughout Starlang for:
8//! - Messages sent between processes
9//! - Registry keys
10//! - GenServer calls, casts, and replies
11//! - Any data that needs to be serialized
12//!
13//! Uses `postcard` for compact binary serialization.
14
15use serde::{Serialize, de::DeserializeOwned};
16use thiserror::Error;
17
18/// Error type for term decoding failures.
19#[derive(Debug, Error)]
20pub enum DecodeError {
21    /// Failed to deserialize the term bytes.
22    #[error("failed to decode term: {0}")]
23    Deserialize(#[from] postcard::Error),
24}
25
26/// A trait for Erlang-like terms that can be serialized and sent between processes.
27///
28/// This trait is automatically implemented for any type that implements
29/// `Serialize + DeserializeOwned + Send + 'static`. The serialization uses
30/// `postcard` for compact, efficient binary encoding.
31///
32/// In Erlang/Elixir, everything is a "term" - atoms, tuples, lists, maps, etc.
33/// In Starlang, any serializable Rust type can be a Term, giving you the same
34/// flexibility with Rust's type safety.
35///
36/// # Examples
37///
38/// ```
39/// use starlang_core::Term;
40/// use serde::{Serialize, Deserialize};
41///
42/// #[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
43/// struct Ping {
44///     id: u32,
45/// }
46///
47/// let ping = Ping { id: 42 };
48/// let bytes = ping.encode();
49/// let decoded = Ping::decode(&bytes).unwrap();
50/// assert_eq!(ping, decoded);
51/// ```
52///
53/// Terms can be any serializable type:
54///
55/// ```
56/// use starlang_core::Term;
57///
58/// // Primitives
59/// let n: u64 = 42;
60/// assert_eq!(u64::decode(&n.encode()).unwrap(), 42);
61///
62/// // Strings
63/// let s = "hello".to_string();
64/// assert_eq!(String::decode(&s.encode()).unwrap(), "hello");
65///
66/// // Tuples (like Erlang tuples)
67/// let t = ("room".to_string(), "lobby".to_string(), 123u32);
68/// let decoded: (String, String, u32) = Term::decode(&t.encode()).unwrap();
69/// assert_eq!(t, decoded);
70/// ```
71pub trait Term: Sized + Send + 'static {
72    /// Encodes this term into bytes.
73    ///
74    /// # Panics
75    ///
76    /// Panics if serialization fails. This should not happen for well-formed
77    /// types that properly implement `Serialize`.
78    fn encode(&self) -> Vec<u8>;
79
80    /// Decodes a term from bytes.
81    ///
82    /// # Errors
83    ///
84    /// Returns `DecodeError` if the bytes cannot be deserialized into this type.
85    fn decode(bytes: &[u8]) -> Result<Self, DecodeError>;
86
87    /// Encodes this term, returning `None` on failure instead of panicking.
88    fn try_encode(&self) -> Option<Vec<u8>>;
89}
90
91impl<T> Term for T
92where
93    T: Serialize + DeserializeOwned + Send + 'static,
94{
95    fn encode(&self) -> Vec<u8> {
96        postcard::to_allocvec(self).expect("term serialization failed")
97    }
98
99    fn decode(bytes: &[u8]) -> Result<Self, DecodeError> {
100        postcard::from_bytes(bytes).map_err(DecodeError::from)
101    }
102
103    fn try_encode(&self) -> Option<Vec<u8>> {
104        postcard::to_allocvec(self).ok()
105    }
106}
107
108/// Type alias for backward compatibility.
109///
110/// `Message` is now called `Term` to better reflect its Erlang-like nature.
111/// This alias allows existing code to continue working.
112#[deprecated(since = "0.2.0", note = "Use `Term` instead of `Message`")]
113pub trait Message: Term {}
114
115#[allow(deprecated)]
116impl<T: Term> Message for T {}
117
118#[cfg(test)]
119mod tests {
120    use super::*;
121    use serde::{Deserialize, Serialize};
122
123    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
124    struct TestTerm {
125        id: u32,
126        name: String,
127    }
128
129    #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
130    enum TestEnum {
131        Ping(u64),
132        Pong { value: String },
133    }
134
135    #[test]
136    fn test_encode_decode_struct() {
137        let term = TestTerm {
138            id: 42,
139            name: "hello".to_string(),
140        };
141        let bytes = term.encode();
142        let decoded = TestTerm::decode(&bytes).unwrap();
143        assert_eq!(term, decoded);
144    }
145
146    #[test]
147    fn test_encode_decode_enum() {
148        let term = TestEnum::Ping(123);
149        let bytes = term.encode();
150        let decoded = TestEnum::decode(&bytes).unwrap();
151        assert_eq!(term, decoded);
152
153        let term = TestEnum::Pong {
154            value: "world".to_string(),
155        };
156        let bytes = term.encode();
157        let decoded = TestEnum::decode(&bytes).unwrap();
158        assert_eq!(term, decoded);
159    }
160
161    #[test]
162    fn test_decode_error() {
163        let bad_bytes = vec![0xFF, 0xFF, 0xFF];
164        let result = TestTerm::decode(&bad_bytes);
165        assert!(result.is_err());
166    }
167
168    #[test]
169    fn test_try_encode() {
170        let term = TestTerm {
171            id: 1,
172            name: "test".to_string(),
173        };
174        let bytes = term.try_encode();
175        assert!(bytes.is_some());
176    }
177
178    #[test]
179    fn test_primitive_types() {
180        // u32
181        let num: u32 = 42;
182        let bytes = num.encode();
183        let decoded = u32::decode(&bytes).unwrap();
184        assert_eq!(num, decoded);
185
186        // String
187        let s = "hello world".to_string();
188        let bytes = s.encode();
189        let decoded = String::decode(&bytes).unwrap();
190        assert_eq!(s, decoded);
191
192        // Vec<u8>
193        let v: Vec<u8> = vec![1, 2, 3, 4];
194        let bytes = v.encode();
195        let decoded = Vec::<u8>::decode(&bytes).unwrap();
196        assert_eq!(v, decoded);
197    }
198
199    #[test]
200    fn test_option_types() {
201        let some: Option<u32> = Some(42);
202        let bytes = some.encode();
203        let decoded = Option::<u32>::decode(&bytes).unwrap();
204        assert_eq!(some, decoded);
205
206        let none: Option<u32> = None;
207        let bytes = none.encode();
208        let decoded = Option::<u32>::decode(&bytes).unwrap();
209        assert_eq!(none, decoded);
210    }
211
212    #[test]
213    fn test_tuple_types() {
214        // Tuples work like Erlang tuples - great for registry keys!
215        let tuple: (u32, String, bool) = (42, "test".to_string(), true);
216        let bytes = tuple.encode();
217        let decoded = <(u32, String, bool)>::decode(&bytes).unwrap();
218        assert_eq!(tuple, decoded);
219    }
220
221    #[test]
222    fn test_unit_type() {
223        let unit: () = ();
224        let bytes = unit.encode();
225        <()>::decode(&bytes).unwrap();
226        assert_eq!(unit, ());
227    }
228
229    #[test]
230    fn test_tuple_as_registry_key() {
231        // This demonstrates using tuples as registry keys like Elixir
232        // e.g., {:room, "lobby"} or {:user, user_id}
233        let key = ("room".to_string(), "lobby".to_string());
234        let bytes = key.encode();
235        let decoded: (String, String) = Term::decode(&bytes).unwrap();
236        assert_eq!(key, decoded);
237
238        // More complex key
239        let key = ("game".to_string(), 123u64, "player1".to_string());
240        let bytes = key.encode();
241        let decoded: (String, u64, String) = Term::decode(&bytes).unwrap();
242        assert_eq!(key, decoded);
243    }
244}