Skip to main content

tempest_engine/types/
value.rs

1use std::borrow::Cow;
2
3use bytes::{Buf, BufMut, Bytes, BytesMut};
4use derive_more::Display;
5use serde::{Deserialize, Serialize};
6use tempest_core::encoding::{
7    BufGetLexicalExt, BufGetRawExt, BufPutLexicalExt, BufPutRawExt, LexicalDecodeError,
8    RawDecodeError,
9};
10
11/// A primitive or enum type tag. The `u32` inside `Enum` is the raw `TypeId`.
12#[derive(Debug, Display, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
13pub enum TempestType {
14    #[display("Int64")]
15    Int64,
16    #[display("Bool")]
17    Bool,
18    #[display("String")]
19    String,
20    /// Enum type identified by its catalog `TypeId` (stored as raw `u32`).
21    #[display("Enum({_0})")]
22    Enum(u32),
23}
24
25impl TempestType {
26    pub fn name(&self) -> &'static str {
27        match self {
28            Self::Int64 => "Int64",
29            Self::Bool => "Bool",
30            Self::String => "String",
31            Self::Enum(_) => "Enum",
32        }
33    }
34}
35
36impl std::str::FromStr for TempestType {
37    type Err = ();
38    fn from_str(s: &str) -> Result<Self, ()> {
39        match s {
40            "Int64" => Ok(Self::Int64),
41            "Bool" => Ok(Self::Bool),
42            "String" => Ok(Self::String),
43            _ => Err(()),
44        }
45    }
46}
47
48#[derive(derive_more::Debug, Display, Clone, PartialEq, Eq, PartialOrd, Ord)]
49#[repr(u8)]
50pub enum TempestValue<'a> {
51    Int64(i64) = 0,
52    Bool(bool) = 1,
53    String(Cow<'a, str>) = 2,
54    #[display("Enum({type_id}:{variant_id})")]
55    Enum { type_id: u32, variant_id: u32, fields: Vec<TempestValue<'a>> } = 3,
56}
57
58impl<'a> TempestValue<'a> {
59    pub fn ty(&self) -> TempestType {
60        match self {
61            TempestValue::Int64(_) => TempestType::Int64,
62            TempestValue::Bool(_) => TempestType::Bool,
63            TempestValue::String(_) => TempestType::String,
64            TempestValue::Enum { type_id, .. } => TempestType::Enum(*type_id),
65        }
66    }
67
68    pub fn encode(&self, buf: &mut BytesMut) {
69        match self {
70            &TempestValue::Int64(i) => buf.put_i64_raw(i),
71            &TempestValue::Bool(b) => buf.put_bool_raw(b),
72            TempestValue::String(s) => buf.put_str_raw(s),
73            TempestValue::Enum { variant_id, fields, .. } => {
74                buf.put_u32(*variant_id);
75                for field in fields {
76                    field.encode(buf);
77                }
78            }
79        }
80    }
81
82    pub fn decode(
83        buf: &mut Bytes,
84        ty: TempestType,
85    ) -> Result<TempestValue<'static>, RawDecodeError> {
86        match ty {
87            TempestType::Int64 => buf.get_i64_raw().map(TempestValue::Int64),
88            TempestType::Bool => buf.get_bool_raw().map(TempestValue::Bool),
89            TempestType::String => buf
90                .get_str_raw()
91                .map(|s| TempestValue::String(Cow::Owned(s))),
92            TempestType::Enum(type_id) => {
93                let variant_id = buf.get_u32();
94                Ok(TempestValue::Enum { type_id, variant_id, fields: vec![] })
95            }
96        }
97    }
98
99    pub fn encode_lexical(&self, buf: &mut BytesMut) {
100        match self {
101            &TempestValue::Int64(i) => buf.put_i64_lexical(i),
102            &TempestValue::Bool(b) => buf.put_bool_lexical(b),
103            TempestValue::String(s) => buf.put_str_lexical(s),
104            TempestValue::Enum { .. } => panic!("enum values cannot be used as primary key"),
105        }
106    }
107
108    pub fn decode_lexical(
109        buf: &mut Bytes,
110        ty: TempestType,
111    ) -> Result<TempestValue<'static>, LexicalDecodeError> {
112        match ty {
113            TempestType::Int64 => buf.get_i64_lexical().map(TempestValue::Int64),
114            TempestType::Bool => buf.get_bool_lexical().map(TempestValue::Bool),
115            TempestType::String => buf
116                .get_str_lexical()
117                .map(|s| TempestValue::String(Cow::Owned(s))),
118            TempestType::Enum(_) => panic!("enum values cannot be used as primary key"),
119        }
120    }
121}
122
123#[cfg(test)]
124mod tests {
125    use std::borrow::Cow;
126
127    use bytes::{Bytes, BytesMut};
128
129    use super::*;
130
131    // -- helpers --
132
133    fn roundtrip(val: TempestValue<'_>) -> TempestValue<'static> {
134        let ty = val.ty();
135        let mut buf = BytesMut::new();
136        val.encode(&mut buf);
137        TempestValue::decode(&mut buf.freeze(), ty).unwrap()
138    }
139
140    fn roundtrip_lexical(val: TempestValue<'_>) -> TempestValue<'static> {
141        let ty = val.ty();
142        let mut buf = BytesMut::new();
143        val.encode_lexical(&mut buf);
144        TempestValue::decode_lexical(&mut buf.freeze(), ty).unwrap()
145    }
146
147    fn encode_lexical_bytes(val: TempestValue<'_>) -> Bytes {
148        let mut buf = BytesMut::new();
149        val.encode_lexical(&mut buf);
150        buf.freeze()
151    }
152
153    // -- raw roundtrip --
154
155    #[test]
156    fn test_int64_roundtrip() {
157        for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
158            let result = roundtrip(TempestValue::Int64(val));
159            assert_eq!(result, TempestValue::Int64(val));
160        }
161    }
162
163    #[test]
164    fn test_bool_roundtrip() {
165        for val in [true, false] {
166            let result = roundtrip(TempestValue::Bool(val));
167            assert_eq!(result, TempestValue::Bool(val));
168        }
169    }
170
171    #[test]
172    fn test_string_roundtrip() {
173        for val in ["", "hello", "tempest", "unicode: ??", "hel\x00lo"] {
174            let result = roundtrip(TempestValue::String(Cow::Borrowed(val)));
175            assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
176        }
177    }
178
179    #[test]
180    fn test_enum_roundtrip() {
181        let val = TempestValue::Enum { type_id: 5, variant_id: 2, fields: vec![] };
182        let result = roundtrip(val.clone());
183        assert_eq!(result, val);
184    }
185
186    #[test]
187    fn test_decode_returns_owned_string() {
188        let result = roundtrip(TempestValue::String(Cow::Borrowed("hello")));
189        if let TempestValue::String(cow) = result {
190            assert!(
191                matches!(cow, Cow::Owned(_)),
192                "decoded string should be Cow::Owned"
193            );
194        } else {
195            panic!("expected String variant");
196        }
197    }
198
199    // -- lexical roundtrip --
200
201    #[test]
202    fn test_int64_lexical_roundtrip() {
203        for val in [0i64, 1, -1, i64::MIN, i64::MAX, 42, -1000] {
204            let result = roundtrip_lexical(TempestValue::Int64(val));
205            assert_eq!(result, TempestValue::Int64(val));
206        }
207    }
208
209    #[test]
210    fn test_bool_lexical_roundtrip() {
211        for val in [true, false] {
212            let result = roundtrip_lexical(TempestValue::Bool(val));
213            assert_eq!(result, TempestValue::Bool(val));
214        }
215    }
216
217    #[test]
218    fn test_string_lexical_roundtrip() {
219        for val in ["", "hello", "tempest", "hel\x00lo", "\x00\x00\x00"] {
220            let result = roundtrip_lexical(TempestValue::String(Cow::Borrowed(val)));
221            assert_eq!(result, TempestValue::String(Cow::Borrowed(val)));
222        }
223    }
224
225    // -- lexical ordering --
226
227    #[test]
228    fn test_int64_lexical_ordering() {
229        let cases = [i64::MIN, -1000, -1, 0, 1, 1000, i64::MAX];
230        for pair in cases.windows(2) {
231            let (a, b) = (pair[0], pair[1]);
232            assert!(
233                encode_lexical_bytes(TempestValue::Int64(a))
234                    < encode_lexical_bytes(TempestValue::Int64(b)),
235                "{} should encode less than {}",
236                a,
237                b
238            );
239        }
240    }
241
242    #[test]
243    fn test_bool_lexical_ordering() {
244        assert!(
245            encode_lexical_bytes(TempestValue::Bool(false))
246                < encode_lexical_bytes(TempestValue::Bool(true))
247        );
248    }
249
250    #[test]
251    fn test_string_lexical_ordering() {
252        let cases = ["", "a", "aa", "ab", "b", "z"];
253        for pair in cases.windows(2) {
254            let (a, b) = (pair[0], pair[1]);
255            assert!(
256                encode_lexical_bytes(TempestValue::String(Cow::Borrowed(a)))
257                    < encode_lexical_bytes(TempestValue::String(Cow::Borrowed(b))),
258                "{:?} should encode less than {:?}",
259                a,
260                b
261            );
262        }
263    }
264
265    // -- ty() --
266
267    #[test]
268    fn test_ty_returns_correct_discriminant() {
269        assert_eq!(TempestValue::Int64(Default::default()).ty(), TempestType::Int64);
270        assert_eq!(TempestValue::Bool(Default::default()).ty(), TempestType::Bool);
271        assert_eq!(TempestValue::String(Default::default()).ty(), TempestType::String);
272        assert_eq!(
273            TempestValue::Enum { type_id: 3, variant_id: 0, fields: vec![] }.ty(),
274            TempestType::Enum(3)
275        );
276    }
277
278    // -- raw vs lexical differ for i64 --
279
280    #[test]
281    fn test_raw_and_lexical_differ_for_negative_i64() {
282        let val = TempestValue::Int64(-1);
283        let mut raw_buf = BytesMut::new();
284        let mut lex_buf = BytesMut::new();
285        val.encode(&mut raw_buf);
286        val.encode_lexical(&mut lex_buf);
287        assert_ne!(
288            raw_buf, lex_buf,
289            "raw and lexical encodings of -1 must differ"
290        );
291    }
292
293    // -- cursor advancement --
294
295    #[test]
296    fn test_decode_advances_cursor() {
297        let mut buf = BytesMut::new();
298        TempestValue::Int64(42).encode(&mut buf);
299        TempestValue::Bool(true).encode(&mut buf);
300        TempestValue::String(Cow::Borrowed("hi")).encode(&mut buf);
301
302        let mut bytes = buf.freeze();
303        assert_eq!(
304            TempestValue::decode(&mut bytes, TempestType::Int64).unwrap(),
305            TempestValue::Int64(42)
306        );
307        assert_eq!(
308            TempestValue::decode(&mut bytes, TempestType::Bool).unwrap(),
309            TempestValue::Bool(true)
310        );
311        assert_eq!(
312            TempestValue::decode(&mut bytes, TempestType::String).unwrap(),
313            TempestValue::String(Cow::Borrowed("hi"))
314        );
315        assert!(bytes.is_empty());
316    }
317
318    #[test]
319    fn test_decode_lexical_advances_cursor() {
320        let mut buf = BytesMut::new();
321        TempestValue::Int64(-99).encode_lexical(&mut buf);
322        TempestValue::String(Cow::Borrowed("foo")).encode_lexical(&mut buf);
323        TempestValue::Bool(false).encode_lexical(&mut buf);
324
325        let mut bytes = buf.freeze();
326        assert_eq!(
327            TempestValue::decode_lexical(&mut bytes, TempestType::Int64).unwrap(),
328            TempestValue::Int64(-99)
329        );
330        assert_eq!(
331            TempestValue::decode_lexical(&mut bytes, TempestType::String).unwrap(),
332            TempestValue::String(Cow::Borrowed("foo"))
333        );
334        assert_eq!(
335            TempestValue::decode_lexical(&mut bytes, TempestType::Bool).unwrap(),
336            TempestValue::Bool(false)
337        );
338        assert!(bytes.is_empty());
339    }
340}