unity_asset_core/
dynamic_access.rs

1//! Dynamic property access for Unity classes
2//!
3//! This module provides Python-like dynamic property access for Unity objects,
4//! similar to the reference library's behavior.
5
6use crate::{Result, UnityAssetError, UnityValue};
7use std::collections::HashMap;
8
9/// Trait for dynamic property access
10pub trait DynamicAccess {
11    /// Get a property value with automatic type conversion
12    fn get_dynamic(&self, key: &str) -> Option<DynamicValue>;
13
14    /// Set a property value with automatic type conversion
15    fn set_dynamic(&mut self, key: &str, value: DynamicValue) -> Result<()>;
16
17    /// Check if a property exists
18    fn has_dynamic(&self, key: &str) -> bool;
19
20    /// Get all property names
21    fn keys_dynamic(&self) -> Vec<String>;
22}
23
24/// Dynamic value wrapper that supports Python-like operations
25#[derive(Debug, Clone, PartialEq)]
26pub enum DynamicValue {
27    /// String value
28    String(String),
29    /// Integer value
30    Integer(i64),
31    /// Float value
32    Float(f64),
33    /// Boolean value
34    Bool(bool),
35    /// Array value
36    Array(Vec<DynamicValue>),
37    /// Bytes value
38    Bytes(Vec<u8>),
39    /// Object value
40    Object(HashMap<String, DynamicValue>),
41    /// Null value
42    Null,
43}
44
45impl DynamicValue {
46    /// Convert from UnityValue
47    pub fn from_unity_value(value: &UnityValue) -> Self {
48        match value {
49            UnityValue::String(s) => DynamicValue::String(s.clone()),
50            UnityValue::Integer(i) => DynamicValue::Integer(*i),
51            UnityValue::Float(f) => DynamicValue::Float(*f),
52            UnityValue::Bool(b) => DynamicValue::Bool(*b),
53            UnityValue::Array(arr) => {
54                let converted: Vec<DynamicValue> =
55                    arr.iter().map(DynamicValue::from_unity_value).collect();
56                DynamicValue::Array(converted)
57            }
58            UnityValue::Bytes(b) => DynamicValue::Bytes(b.clone()),
59            UnityValue::Object(obj) => {
60                let converted: HashMap<String, DynamicValue> = obj
61                    .iter()
62                    .map(|(k, v)| (k.clone(), DynamicValue::from_unity_value(v)))
63                    .collect();
64                DynamicValue::Object(converted)
65            }
66            UnityValue::Null => DynamicValue::Null,
67        }
68    }
69
70    /// Convert to UnityValue
71    pub fn to_unity_value(&self) -> UnityValue {
72        match self {
73            DynamicValue::String(s) => UnityValue::String(s.clone()),
74            DynamicValue::Integer(i) => UnityValue::Integer(*i),
75            DynamicValue::Float(f) => UnityValue::Float(*f),
76            DynamicValue::Bool(b) => UnityValue::Bool(*b),
77            DynamicValue::Array(arr) => {
78                let converted: Vec<UnityValue> =
79                    arr.iter().map(DynamicValue::to_unity_value).collect();
80                UnityValue::Array(converted)
81            }
82            DynamicValue::Bytes(b) => UnityValue::Bytes(b.clone()),
83            DynamicValue::Object(obj) => {
84                let converted: indexmap::IndexMap<String, UnityValue> = obj
85                    .iter()
86                    .map(|(k, v)| (k.clone(), v.to_unity_value()))
87                    .collect();
88                UnityValue::Object(converted)
89            }
90            DynamicValue::Null => UnityValue::Null,
91        }
92    }
93
94    /// Get as string
95    pub fn as_string(&self) -> Option<&str> {
96        match self {
97            DynamicValue::String(s) => Some(s),
98            _ => None,
99        }
100    }
101
102    /// Get as integer
103    pub fn as_integer(&self) -> Option<i64> {
104        match self {
105            DynamicValue::Integer(i) => Some(*i),
106            DynamicValue::Float(f) => Some(*f as i64),
107            DynamicValue::Bool(b) => Some(if *b { 1 } else { 0 }),
108            _ => None,
109        }
110    }
111
112    /// Get as float
113    pub fn as_float(&self) -> Option<f64> {
114        match self {
115            DynamicValue::Float(f) => Some(*f),
116            DynamicValue::Integer(i) => Some(*i as f64),
117            _ => None,
118        }
119    }
120
121    /// Get as boolean
122    pub fn as_bool(&self) -> Option<bool> {
123        match self {
124            DynamicValue::Bool(b) => Some(*b),
125            DynamicValue::Integer(i) => Some(*i != 0),
126            _ => None,
127        }
128    }
129
130    /// Get as array
131    pub fn as_array(&self) -> Option<&Vec<DynamicValue>> {
132        match self {
133            DynamicValue::Array(arr) => Some(arr),
134            _ => None,
135        }
136    }
137
138    /// Get as bytes
139    pub fn as_bytes(&self) -> Option<&[u8]> {
140        match self {
141            DynamicValue::Bytes(b) => Some(b.as_slice()),
142            _ => None,
143        }
144    }
145
146    /// Get as mutable array
147    pub fn as_array_mut(&mut self) -> Option<&mut Vec<DynamicValue>> {
148        match self {
149            DynamicValue::Array(arr) => Some(arr),
150            _ => None,
151        }
152    }
153
154    /// Get as object
155    pub fn as_object(&self) -> Option<&HashMap<String, DynamicValue>> {
156        match self {
157            DynamicValue::Object(obj) => Some(obj),
158            _ => None,
159        }
160    }
161
162    /// Get as mutable object
163    pub fn as_object_mut(&mut self) -> Option<&mut HashMap<String, DynamicValue>> {
164        match self {
165            DynamicValue::Object(obj) => Some(obj),
166            _ => None,
167        }
168    }
169
170    /// Check if value is null
171    pub fn is_null(&self) -> bool {
172        matches!(self, DynamicValue::Null)
173    }
174
175    /// Array indexing (similar to Python list access)
176    pub fn get_index(&self, index: usize) -> Option<&DynamicValue> {
177        match self {
178            DynamicValue::Array(arr) => arr.get(index),
179            _ => None,
180        }
181    }
182
183    /// Mutable array indexing
184    pub fn get_index_mut(&mut self, index: usize) -> Option<&mut DynamicValue> {
185        match self {
186            DynamicValue::Array(arr) => arr.get_mut(index),
187            _ => None,
188        }
189    }
190
191    /// Object property access (similar to Python dict access)
192    pub fn get_property(&self, key: &str) -> Option<&DynamicValue> {
193        match self {
194            DynamicValue::Object(obj) => obj.get(key),
195            _ => None,
196        }
197    }
198
199    /// Mutable object property access
200    pub fn get_property_mut(&mut self, key: &str) -> Option<&mut DynamicValue> {
201        match self {
202            DynamicValue::Object(obj) => obj.get_mut(key),
203            _ => None,
204        }
205    }
206
207    /// Set object property
208    pub fn set_property(&mut self, key: String, value: DynamicValue) -> Result<()> {
209        match self {
210            DynamicValue::Object(obj) => {
211                obj.insert(key, value);
212                Ok(())
213            }
214            _ => Err(UnityAssetError::format(
215                "Cannot set property on non-object value",
216            )),
217        }
218    }
219
220    /// Add to array
221    pub fn push(&mut self, value: DynamicValue) -> Result<()> {
222        match self {
223            DynamicValue::Array(arr) => {
224                arr.push(value);
225                Ok(())
226            }
227            _ => Err(UnityAssetError::format("Cannot push to non-array value")),
228        }
229    }
230
231    /// String concatenation (Python-like += for strings)
232    pub fn concat_string(&mut self, other: &str) -> Result<()> {
233        match self {
234            DynamicValue::String(s) => {
235                s.push_str(other);
236                Ok(())
237            }
238            _ => Err(UnityAssetError::format(
239                "Cannot concatenate to non-string value",
240            )),
241        }
242    }
243
244    /// Numeric addition (Python-like += for numbers)
245    pub fn add_numeric(&mut self, other: f64) -> Result<()> {
246        match self {
247            DynamicValue::Integer(i) => {
248                *i += other as i64;
249                Ok(())
250            }
251            DynamicValue::Float(f) => {
252                *f += other;
253                Ok(())
254            }
255            _ => Err(UnityAssetError::format("Cannot add to non-numeric value")),
256        }
257    }
258}
259
260impl std::fmt::Display for DynamicValue {
261    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
262        match self {
263            DynamicValue::String(s) => write!(f, "\"{}\"", s),
264            DynamicValue::Integer(i) => write!(f, "{}", i),
265            DynamicValue::Float(fl) => write!(f, "{}", fl),
266            DynamicValue::Bool(b) => write!(f, "{}", b),
267            DynamicValue::Array(arr) => {
268                write!(f, "[")?;
269                for (i, item) in arr.iter().enumerate() {
270                    if i > 0 {
271                        write!(f, ", ")?;
272                    }
273                    write!(f, "{}", item)?;
274                }
275                write!(f, "]")
276            }
277            DynamicValue::Bytes(b) => write!(f, "<bytes len={}>", b.len()),
278            DynamicValue::Object(obj) => {
279                write!(f, "{{")?;
280                for (i, (key, value)) in obj.iter().enumerate() {
281                    if i > 0 {
282                        write!(f, ", ")?;
283                    }
284                    write!(f, "\"{}\": {}", key, value)?;
285                }
286                write!(f, "}}")
287            }
288            DynamicValue::Null => write!(f, "null"),
289        }
290    }
291}
292
293#[cfg(test)]
294mod tests {
295    use super::*;
296
297    #[test]
298    fn test_dynamic_value_conversion() {
299        let unity_val = UnityValue::String("test".to_string());
300        let dynamic_val = DynamicValue::from_unity_value(&unity_val);
301
302        assert_eq!(dynamic_val, DynamicValue::String("test".to_string()));
303        assert_eq!(dynamic_val.to_unity_value(), unity_val);
304    }
305
306    #[test]
307    fn test_dynamic_value_access() {
308        let mut val = DynamicValue::String("hello".to_string());
309        val.concat_string(" world").unwrap();
310
311        assert_eq!(val.as_string(), Some("hello world"));
312    }
313
314    #[test]
315    fn test_numeric_operations() {
316        let mut val = DynamicValue::Integer(10);
317        val.add_numeric(5.0).unwrap();
318
319        assert_eq!(val.as_integer(), Some(15));
320    }
321}