ankurah_core/value/
cast.rs

1use crate::{
2    property::PropertyError,
3    value::{Value, ValueType},
4};
5use ankurah_proto::EntityId;
6
7#[derive(Debug, Clone, PartialEq)]
8pub enum CastError {
9    /// Cannot cast from source type to target type
10    IncompatibleTypes { from: ValueType, to: ValueType },
11    /// Invalid format for the target type (e.g., invalid EntityId string)
12    InvalidFormat { value: String, target_type: ValueType },
13    /// Numeric overflow when casting between numeric types
14    NumericOverflow { value: String, target_type: ValueType },
15}
16
17impl std::fmt::Display for CastError {
18    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
19        match self {
20            CastError::IncompatibleTypes { from, to } => {
21                write!(f, "Cannot cast from {:?} to {:?}", from, to)
22            }
23            CastError::InvalidFormat { value, target_type } => {
24                write!(f, "Invalid format '{}' for type {:?}", value, target_type)
25            }
26            CastError::NumericOverflow { value, target_type } => {
27                write!(f, "Numeric overflow: '{}' cannot fit in {:?}", value, target_type)
28            }
29        }
30    }
31}
32
33impl From<CastError> for PropertyError {
34    fn from(err: CastError) -> Self { PropertyError::CastError(err) }
35}
36
37impl std::error::Error for CastError {}
38
39impl Value {
40    /// Cast this value to the specified target type
41    pub fn cast_to(&self, target_type: ValueType) -> Result<Value, CastError> {
42        let source_type = ValueType::of(self);
43
44        // If already the target type, return clone
45        if source_type == target_type {
46            return Ok(self.clone());
47        }
48
49        match (self, target_type) {
50            // String to EntityId conversion
51            (Value::String(s), ValueType::EntityId) => match EntityId::from_base64(s) {
52                Ok(entity_id) => Ok(Value::EntityId(entity_id)),
53                Err(_) => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::EntityId }),
54            },
55
56            // EntityId to String conversion
57            (Value::EntityId(entity_id), ValueType::String) => Ok(Value::String(entity_id.to_base64())),
58
59            // Numeric conversions
60            (Value::I16(n), ValueType::I32) => Ok(Value::I32(*n as i32)),
61            (Value::I16(n), ValueType::I64) => Ok(Value::I64(*n as i64)),
62            (Value::I16(n), ValueType::F64) => Ok(Value::F64(*n as f64)),
63
64            (Value::I32(n), ValueType::I16) => {
65                if *n >= i16::MIN as i32 && *n <= i16::MAX as i32 {
66                    Ok(Value::I16(*n as i16))
67                } else {
68                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I16 })
69                }
70            }
71            (Value::I32(n), ValueType::I64) => Ok(Value::I64(*n as i64)),
72            (Value::I32(n), ValueType::F64) => Ok(Value::F64(*n as f64)),
73
74            (Value::I64(n), ValueType::I16) => {
75                if *n >= i16::MIN as i64 && *n <= i16::MAX as i64 {
76                    Ok(Value::I16(*n as i16))
77                } else {
78                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I16 })
79                }
80            }
81            (Value::I64(n), ValueType::I32) => {
82                if *n >= i32::MIN as i64 && *n <= i32::MAX as i64 {
83                    Ok(Value::I32(*n as i32))
84                } else {
85                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I32 })
86                }
87            }
88            (Value::I64(n), ValueType::F64) => Ok(Value::F64(*n as f64)),
89
90            (Value::F64(n), ValueType::I16) => {
91                if n.is_finite() && *n >= i16::MIN as f64 && *n <= i16::MAX as f64 {
92                    Ok(Value::I16(*n as i16))
93                } else {
94                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I16 })
95                }
96            }
97            (Value::F64(n), ValueType::I32) => {
98                if n.is_finite() && *n >= i32::MIN as f64 && *n <= i32::MAX as f64 {
99                    Ok(Value::I32(*n as i32))
100                } else {
101                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I32 })
102                }
103            }
104            (Value::F64(n), ValueType::I64) => {
105                if n.is_finite() && *n >= i64::MIN as f64 && *n <= i64::MAX as f64 {
106                    Ok(Value::I64(*n as i64))
107                } else {
108                    Err(CastError::NumericOverflow { value: n.to_string(), target_type: ValueType::I64 })
109                }
110            }
111
112            // String to numeric conversions
113            (Value::String(s), ValueType::I16) => match s.parse::<i16>() {
114                Ok(n) => Ok(Value::I16(n)),
115                Err(_) => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::I16 }),
116            },
117            (Value::String(s), ValueType::I32) => match s.parse::<i32>() {
118                Ok(n) => Ok(Value::I32(n)),
119                Err(_) => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::I32 }),
120            },
121            (Value::String(s), ValueType::I64) => match s.parse::<i64>() {
122                Ok(n) => Ok(Value::I64(n)),
123                Err(_) => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::I64 }),
124            },
125            (Value::String(s), ValueType::F64) => match s.parse::<f64>() {
126                Ok(n) => Ok(Value::F64(n)),
127                Err(_) => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::F64 }),
128            },
129            (Value::String(s), ValueType::Bool) => match s.to_lowercase().as_str() {
130                "true" | "1" | "yes" | "on" => Ok(Value::Bool(true)),
131                "false" | "0" | "no" | "off" => Ok(Value::Bool(false)),
132                _ => Err(CastError::InvalidFormat { value: s.clone(), target_type: ValueType::Bool }),
133            },
134
135            // Numeric to string conversions
136            (Value::I16(n), ValueType::String) => Ok(Value::String(n.to_string())),
137            (Value::I32(n), ValueType::String) => Ok(Value::String(n.to_string())),
138            (Value::I64(n), ValueType::String) => Ok(Value::String(n.to_string())),
139            (Value::F64(n), ValueType::String) => Ok(Value::String(n.to_string())),
140            (Value::Bool(b), ValueType::String) => Ok(Value::String(b.to_string())),
141
142            // Bool to numeric conversions (for IndexedDB compatibility where bool stored as 0/1)
143            (Value::Bool(b), ValueType::I16) => Ok(Value::I16(if *b { 1 } else { 0 })),
144            (Value::Bool(b), ValueType::I32) => Ok(Value::I32(if *b { 1 } else { 0 })),
145            (Value::Bool(b), ValueType::I64) => Ok(Value::I64(if *b { 1 } else { 0 })),
146            (Value::Bool(b), ValueType::F64) => Ok(Value::F64(if *b { 1.0 } else { 0.0 })),
147
148            // Numeric to bool conversions (0 = false, non-zero = true)
149            (Value::I16(n), ValueType::Bool) => Ok(Value::Bool(*n != 0)),
150            (Value::I32(n), ValueType::Bool) => Ok(Value::Bool(*n != 0)),
151            (Value::I64(n), ValueType::Bool) => Ok(Value::Bool(*n != 0)),
152            (Value::F64(f), ValueType::Bool) => Ok(Value::Bool(*f != 0.0)),
153
154            // Cast TO Json: wrap scalar values as JSON, preserving their original type tag
155            // This is used for JSON subfield indexing where type must be preserved
156            (Value::String(s), ValueType::Json) => Ok(Value::Json(serde_json::Value::String(s.clone()))),
157            (Value::I64(n), ValueType::Json) => Ok(Value::Json(serde_json::json!(*n))),
158            (Value::I32(n), ValueType::Json) => Ok(Value::Json(serde_json::json!(*n as i64))),
159            (Value::I16(n), ValueType::Json) => Ok(Value::Json(serde_json::json!(*n as i64))),
160            (Value::F64(n), ValueType::Json) => Ok(Value::Json(serde_json::json!(*n))),
161            (Value::Bool(b), ValueType::Json) => Ok(Value::Json(serde_json::Value::Bool(*b))),
162
163            // Cast FROM Json: extract value if types match
164            (Value::Json(json), ValueType::String) => match json {
165                serde_json::Value::String(s) => Ok(Value::String(s.clone())),
166                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
167            },
168            (Value::Json(json), ValueType::I64) => match json {
169                serde_json::Value::Number(n) if n.is_i64() => Ok(Value::I64(n.as_i64().unwrap())),
170                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
171            },
172            (Value::Json(json), ValueType::I32) => match json {
173                serde_json::Value::Number(n) if n.is_i64() => {
174                    let i = n.as_i64().unwrap();
175                    if i >= i32::MIN as i64 && i <= i32::MAX as i64 {
176                        Ok(Value::I32(i as i32))
177                    } else {
178                        Err(CastError::NumericOverflow { value: i.to_string(), target_type: ValueType::I32 })
179                    }
180                }
181                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
182            },
183            (Value::Json(json), ValueType::I16) => match json {
184                serde_json::Value::Number(n) if n.is_i64() => {
185                    let i = n.as_i64().unwrap();
186                    if i >= i16::MIN as i64 && i <= i16::MAX as i64 {
187                        Ok(Value::I16(i as i16))
188                    } else {
189                        Err(CastError::NumericOverflow { value: i.to_string(), target_type: ValueType::I16 })
190                    }
191                }
192                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
193            },
194            (Value::Json(json), ValueType::F64) => match json {
195                serde_json::Value::Number(n) => Ok(Value::F64(n.as_f64().unwrap_or(0.0))),
196                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
197            },
198            (Value::Json(json), ValueType::Bool) => match json {
199                serde_json::Value::Bool(b) => Ok(Value::Bool(*b)),
200                _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
201            },
202
203            // All other combinations are incompatible
204            _ => Err(CastError::IncompatibleTypes { from: source_type, to: target_type }),
205        }
206    }
207
208    /// Try to cast this value to the specified target type, returning None if the cast fails
209    pub fn try_cast_to(&self, target_type: ValueType) -> Option<Value> { self.cast_to(target_type).ok() }
210}
211
212#[cfg(test)]
213mod tests {
214    use super::*;
215
216    #[test]
217    fn test_string_to_entity_id() {
218        let entity_id = EntityId::new();
219        let base64_str = entity_id.to_base64();
220        let value = Value::String(base64_str.clone());
221
222        let result = value.cast_to(ValueType::EntityId).unwrap();
223        match result {
224            Value::EntityId(parsed_id) => assert_eq!(parsed_id, entity_id),
225            _ => panic!("Expected EntityId variant"),
226        }
227    }
228
229    #[test]
230    fn test_entity_id_to_string() {
231        let entity_id = EntityId::new();
232        let value = Value::EntityId(entity_id.clone());
233
234        let result = value.cast_to(ValueType::String).unwrap();
235        match result {
236            Value::String(s) => assert_eq!(s, entity_id.to_base64()),
237            _ => panic!("Expected String variant"),
238        }
239    }
240
241    #[test]
242    fn test_invalid_entity_id_string() {
243        let value = Value::String("invalid-entity-id".to_string());
244        let result = value.cast_to(ValueType::EntityId);
245
246        assert!(matches!(result, Err(CastError::InvalidFormat { .. })));
247    }
248
249    #[test]
250    fn test_numeric_upcasting() {
251        let value = Value::I16(42);
252
253        assert_eq!(value.cast_to(ValueType::I32).unwrap(), Value::I32(42));
254        assert_eq!(value.cast_to(ValueType::I64).unwrap(), Value::I64(42));
255        assert_eq!(value.cast_to(ValueType::F64).unwrap(), Value::F64(42.0));
256    }
257
258    #[test]
259    fn test_numeric_downcasting() {
260        let value = Value::I32(42);
261        assert_eq!(value.cast_to(ValueType::I16).unwrap(), Value::I16(42));
262
263        let large_value = Value::I32(100000);
264        assert!(matches!(large_value.cast_to(ValueType::I16), Err(CastError::NumericOverflow { .. })));
265    }
266
267    #[test]
268    fn test_string_to_numeric() {
269        let value = Value::String("42".to_string());
270
271        assert_eq!(value.cast_to(ValueType::I16).unwrap(), Value::I16(42));
272        assert_eq!(value.cast_to(ValueType::I32).unwrap(), Value::I32(42));
273        assert_eq!(value.cast_to(ValueType::I64).unwrap(), Value::I64(42));
274        assert_eq!(value.cast_to(ValueType::F64).unwrap(), Value::F64(42.0));
275    }
276
277    #[test]
278    fn test_string_to_bool() {
279        assert_eq!(Value::String("true".to_string()).cast_to(ValueType::Bool).unwrap(), Value::Bool(true));
280        assert_eq!(Value::String("false".to_string()).cast_to(ValueType::Bool).unwrap(), Value::Bool(false));
281        assert_eq!(Value::String("1".to_string()).cast_to(ValueType::Bool).unwrap(), Value::Bool(true));
282        assert_eq!(Value::String("0".to_string()).cast_to(ValueType::Bool).unwrap(), Value::Bool(false));
283
284        assert!(matches!(Value::String("maybe".to_string()).cast_to(ValueType::Bool), Err(CastError::InvalidFormat { .. })));
285    }
286
287    #[test]
288    fn test_incompatible_types() {
289        // Binary to I32 is truly incompatible
290        let value = Value::Binary(vec![1, 2, 3]);
291        let result = value.cast_to(ValueType::I32);
292
293        assert!(matches!(result, Err(CastError::IncompatibleTypes { .. })));
294    }
295
296    #[test]
297    fn test_same_type_cast() {
298        let value = Value::I32(42);
299        let result = value.cast_to(ValueType::I32).unwrap();
300
301        assert_eq!(result, value);
302    }
303}