rtmp_rs/amf/
value.rs

1//! AMF value types
2//!
3//! Both AMF0 and AMF3 share a common value representation. This enum
4//! provides a unified type that can be serialized to either format.
5
6use std::collections::HashMap;
7
8/// Unified AMF value representation
9///
10/// This enum represents all value types supported by AMF0 and AMF3.
11/// Some types (like ByteArray, Dictionary) are AMF3-only but included
12/// for completeness.
13#[derive(Debug, Clone, PartialEq)]
14pub enum AmfValue {
15    /// Null value (AMF0: 0x05, AMF3: 0x01)
16    Null,
17
18    /// Undefined value (AMF0: 0x06, AMF3: 0x00)
19    Undefined,
20
21    /// Boolean value (AMF0: 0x01, AMF3: 0x02/0x03)
22    Boolean(bool),
23
24    /// IEEE 754 double-precision floating point (AMF0: 0x00, AMF3: 0x05)
25    Number(f64),
26
27    /// UTF-8 string (AMF0: 0x02, AMF3: 0x06)
28    String(String),
29
30    /// Ordered array with optional associative portion
31    /// In AMF0 this is either StrictArray (0x0A) or ECMAArray (0x08)
32    /// In AMF3 this is Array (0x09)
33    Array(Vec<AmfValue>),
34
35    /// Key-value object (AMF0: 0x03, AMF3: 0x0A)
36    /// Keys are always strings in AMF
37    Object(HashMap<String, AmfValue>),
38
39    /// Typed object with class name
40    TypedObject {
41        class_name: String,
42        properties: HashMap<String, AmfValue>,
43    },
44
45    /// Date value as milliseconds since Unix epoch
46    /// (AMF0: 0x0B, AMF3: 0x08)
47    Date(f64),
48
49    /// XML document (AMF0: 0x0F, AMF3: 0x07/0x0B)
50    Xml(String),
51
52    /// Raw byte array (AMF3 only: 0x0C)
53    ByteArray(Vec<u8>),
54
55    /// Integer (AMF3 only: 0x04, 29-bit signed)
56    Integer(i32),
57
58    /// ECMA Array - associative array with dense and sparse parts
59    /// Stored as (dense_length, properties)
60    EcmaArray(HashMap<String, AmfValue>),
61}
62
63impl AmfValue {
64    /// Try to get this value as a string reference
65    pub fn as_str(&self) -> Option<&str> {
66        match self {
67            AmfValue::String(s) => Some(s),
68            _ => None,
69        }
70    }
71
72    /// Try to get this value as a number
73    pub fn as_number(&self) -> Option<f64> {
74        match self {
75            AmfValue::Number(n) => Some(*n),
76            AmfValue::Integer(i) => Some(*i as f64),
77            _ => None,
78        }
79    }
80
81    /// Try to get this value as a boolean
82    pub fn as_bool(&self) -> Option<bool> {
83        match self {
84            AmfValue::Boolean(b) => Some(*b),
85            _ => None,
86        }
87    }
88
89    /// Try to get this value as an object reference
90    pub fn as_object(&self) -> Option<&HashMap<String, AmfValue>> {
91        match self {
92            AmfValue::Object(m) => Some(m),
93            AmfValue::EcmaArray(m) => Some(m),
94            AmfValue::TypedObject { properties, .. } => Some(properties),
95            _ => None,
96        }
97    }
98
99    /// Try to get this value as a mutable object reference
100    pub fn as_object_mut(&mut self) -> Option<&mut HashMap<String, AmfValue>> {
101        match self {
102            AmfValue::Object(m) => Some(m),
103            AmfValue::EcmaArray(m) => Some(m),
104            AmfValue::TypedObject { properties, .. } => Some(properties),
105            _ => None,
106        }
107    }
108
109    /// Try to get this value as an array reference
110    pub fn as_array(&self) -> Option<&Vec<AmfValue>> {
111        match self {
112            AmfValue::Array(a) => Some(a),
113            _ => None,
114        }
115    }
116
117    /// Check if this value is null or undefined
118    pub fn is_null_or_undefined(&self) -> bool {
119        matches!(self, AmfValue::Null | AmfValue::Undefined)
120    }
121
122    /// Get a property from an object value
123    pub fn get(&self, key: &str) -> Option<&AmfValue> {
124        self.as_object()?.get(key)
125    }
126
127    /// Get a string property from an object value
128    pub fn get_string(&self, key: &str) -> Option<&str> {
129        self.get(key)?.as_str()
130    }
131
132    /// Get a number property from an object value
133    pub fn get_number(&self, key: &str) -> Option<f64> {
134        self.get(key)?.as_number()
135    }
136}
137
138impl Default for AmfValue {
139    fn default() -> Self {
140        AmfValue::Null
141    }
142}
143
144impl From<bool> for AmfValue {
145    fn from(v: bool) -> Self {
146        AmfValue::Boolean(v)
147    }
148}
149
150impl From<f64> for AmfValue {
151    fn from(v: f64) -> Self {
152        AmfValue::Number(v)
153    }
154}
155
156impl From<i32> for AmfValue {
157    fn from(v: i32) -> Self {
158        AmfValue::Number(v as f64)
159    }
160}
161
162impl From<u32> for AmfValue {
163    fn from(v: u32) -> Self {
164        AmfValue::Number(v as f64)
165    }
166}
167
168impl From<String> for AmfValue {
169    fn from(v: String) -> Self {
170        AmfValue::String(v)
171    }
172}
173
174impl From<&str> for AmfValue {
175    fn from(v: &str) -> Self {
176        AmfValue::String(v.to_string())
177    }
178}
179
180impl<V: Into<AmfValue>> From<Vec<V>> for AmfValue {
181    fn from(v: Vec<V>) -> Self {
182        AmfValue::Array(v.into_iter().map(|x| x.into()).collect())
183    }
184}
185
186impl<V: Into<AmfValue>> From<HashMap<String, V>> for AmfValue {
187    fn from(v: HashMap<String, V>) -> Self {
188        AmfValue::Object(v.into_iter().map(|(k, v)| (k, v.into())).collect())
189    }
190}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_value_accessors() {
198        let s = AmfValue::String("test".into());
199        assert_eq!(s.as_str(), Some("test"));
200        assert_eq!(s.as_number(), None);
201
202        let n = AmfValue::Number(42.0);
203        assert_eq!(n.as_number(), Some(42.0));
204        assert_eq!(n.as_str(), None);
205
206        let mut obj = HashMap::new();
207        obj.insert("key".to_string(), AmfValue::String("value".into()));
208        let o = AmfValue::Object(obj);
209        assert_eq!(o.get_string("key"), Some("value"));
210    }
211
212    #[test]
213    fn test_from_conversions() {
214        let v: AmfValue = "test".into();
215        assert!(matches!(v, AmfValue::String(_)));
216
217        let v: AmfValue = 42.0.into();
218        assert!(matches!(v, AmfValue::Number(_)));
219
220        let v: AmfValue = true.into();
221        assert!(matches!(v, AmfValue::Boolean(true)));
222    }
223
224    #[test]
225    fn test_as_bool() {
226        assert_eq!(AmfValue::Boolean(true).as_bool(), Some(true));
227        assert_eq!(AmfValue::Boolean(false).as_bool(), Some(false));
228        assert_eq!(AmfValue::Number(1.0).as_bool(), None);
229        assert_eq!(AmfValue::Null.as_bool(), None);
230    }
231
232    #[test]
233    fn test_as_array() {
234        let arr = AmfValue::Array(vec![AmfValue::Number(1.0), AmfValue::Number(2.0)]);
235        assert!(arr.as_array().is_some());
236        assert_eq!(arr.as_array().unwrap().len(), 2);
237
238        assert!(AmfValue::Null.as_array().is_none());
239        assert!(AmfValue::Object(HashMap::new()).as_array().is_none());
240    }
241
242    #[test]
243    fn test_as_object_mut() {
244        let mut obj = AmfValue::Object(HashMap::new());
245        if let Some(map) = obj.as_object_mut() {
246            map.insert("key".to_string(), AmfValue::String("value".into()));
247        }
248
249        assert_eq!(obj.get_string("key"), Some("value"));
250    }
251
252    #[test]
253    fn test_is_null_or_undefined() {
254        assert!(AmfValue::Null.is_null_or_undefined());
255        assert!(AmfValue::Undefined.is_null_or_undefined());
256        assert!(!AmfValue::Boolean(false).is_null_or_undefined());
257        assert!(!AmfValue::Number(0.0).is_null_or_undefined());
258        assert!(!AmfValue::String(String::new()).is_null_or_undefined());
259    }
260
261    #[test]
262    fn test_get_number() {
263        let mut obj = HashMap::new();
264        obj.insert("count".to_string(), AmfValue::Number(42.0));
265        obj.insert("name".to_string(), AmfValue::String("test".into()));
266        let amf = AmfValue::Object(obj);
267
268        assert_eq!(amf.get_number("count"), Some(42.0));
269        assert_eq!(amf.get_number("name"), None);
270        assert_eq!(amf.get_number("missing"), None);
271    }
272
273    #[test]
274    fn test_integer_as_number() {
275        let integer = AmfValue::Integer(100);
276        assert_eq!(integer.as_number(), Some(100.0));
277    }
278
279    #[test]
280    fn test_default_value() {
281        let default = AmfValue::default();
282        assert_eq!(default, AmfValue::Null);
283    }
284
285    #[test]
286    fn test_from_i32() {
287        let v: AmfValue = 42i32.into();
288        assert_eq!(v, AmfValue::Number(42.0));
289    }
290
291    #[test]
292    fn test_from_u32() {
293        let v: AmfValue = 1000u32.into();
294        assert_eq!(v, AmfValue::Number(1000.0));
295    }
296
297    #[test]
298    fn test_from_string_owned() {
299        let s = String::from("owned");
300        let v: AmfValue = s.into();
301        assert_eq!(v.as_str(), Some("owned"));
302    }
303
304    #[test]
305    fn test_from_vec() {
306        let vec: Vec<f64> = vec![1.0, 2.0, 3.0];
307        let v: AmfValue = vec.into();
308        if let AmfValue::Array(arr) = v {
309            assert_eq!(arr.len(), 3);
310        } else {
311            panic!("Expected Array");
312        }
313    }
314
315    #[test]
316    fn test_from_hashmap() {
317        let mut map = HashMap::new();
318        map.insert("a".to_string(), 1.0f64);
319        map.insert("b".to_string(), 2.0f64);
320
321        let v: AmfValue = map.into();
322        if let AmfValue::Object(obj) = v {
323            assert_eq!(obj.len(), 2);
324        } else {
325            panic!("Expected Object");
326        }
327    }
328
329    #[test]
330    fn test_as_object_with_typed_object() {
331        let mut props = HashMap::new();
332        props.insert("x".to_string(), AmfValue::Number(10.0));
333
334        let typed = AmfValue::TypedObject {
335            class_name: "Point".to_string(),
336            properties: props,
337        };
338
339        // as_object should work on TypedObject
340        assert!(typed.as_object().is_some());
341        assert_eq!(typed.get_number("x"), Some(10.0));
342    }
343
344    #[test]
345    fn test_as_object_with_ecma_array() {
346        let mut props = HashMap::new();
347        props.insert("key".to_string(), AmfValue::String("value".into()));
348
349        let ecma = AmfValue::EcmaArray(props);
350
351        // as_object should work on EcmaArray
352        assert!(ecma.as_object().is_some());
353        assert_eq!(ecma.get_string("key"), Some("value"));
354    }
355
356    #[test]
357    fn test_get_on_non_object() {
358        assert!(AmfValue::Null.get("key").is_none());
359        assert!(AmfValue::Number(42.0).get("key").is_none());
360        assert!(AmfValue::Array(vec![]).get("0").is_none());
361    }
362
363    #[test]
364    fn test_amf_value_clone() {
365        let original = AmfValue::Object({
366            let mut m = HashMap::new();
367            m.insert(
368                "nested".to_string(),
369                AmfValue::Array(vec![AmfValue::Number(1.0), AmfValue::String("test".into())]),
370            );
371            m
372        });
373
374        let cloned = original.clone();
375        assert_eq!(original, cloned);
376    }
377
378    #[test]
379    fn test_amf_value_partial_eq() {
380        assert_eq!(AmfValue::Null, AmfValue::Null);
381        assert_ne!(AmfValue::Null, AmfValue::Undefined);
382        assert_eq!(AmfValue::Number(1.0), AmfValue::Number(1.0));
383        assert_ne!(AmfValue::Number(1.0), AmfValue::Number(2.0));
384        assert_eq!(
385            AmfValue::String("test".into()),
386            AmfValue::String("test".into())
387        );
388    }
389}