Skip to main content

ankurah_core/value/
mod.rs

1mod cast;
2pub mod cast_predicate;
3mod collatable;
4#[cfg(feature = "wasm")]
5mod wasm;
6
7pub use cast::CastError;
8
9use ankurah_proto as proto;
10use serde::{Deserialize, Serialize};
11use std::fmt::Display;
12
13/// Custom serialization for serde_json::Value that stores as bytes.
14/// This is needed because bincode doesn't support deserialize_any.
15mod json_as_bytes {
16    use serde::{Deserialize, Deserializer, Serialize, Serializer};
17
18    pub fn serialize<S>(value: &serde_json::Value, serializer: S) -> Result<S::Ok, S::Error>
19    where S: Serializer {
20        let bytes = serde_json::to_vec(value).map_err(serde::ser::Error::custom)?;
21        bytes.serialize(serializer)
22    }
23
24    pub fn deserialize<'de, D>(deserializer: D) -> Result<serde_json::Value, D::Error>
25    where D: Deserializer<'de> {
26        let bytes: Vec<u8> = Vec::deserialize(deserializer)?;
27        serde_json::from_slice(&bytes).map_err(serde::de::Error::custom)
28    }
29}
30
31#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
32pub enum Value {
33    // Numbers
34    I16(i16),
35    I32(i32),
36    I64(i64),
37    F64(f64),
38
39    Bool(bool),
40    String(String),
41    EntityId(proto::EntityId),
42    Object(Vec<u8>),
43    Binary(Vec<u8>),
44    /// JSON value - stored as jsonb in PostgreSQL for proper query support.
45    /// Serialized as bytes for bincode compatibility.
46    #[serde(with = "json_as_bytes")]
47    Json(serde_json::Value),
48}
49
50impl Value {
51    /// Create a Json value from any serializable type.
52    pub fn json<T: Serialize>(value: &T) -> Result<Self, serde_json::Error> { Ok(Value::Json(serde_json::to_value(value)?)) }
53
54    /// Parse this value as JSON into the target type.
55    /// Works for Json, Object, Binary (as bytes) and String variants.
56    /// Returns InvalidVariant error for numeric, bool, and EntityId types.
57    pub fn parse_as_json<T: serde::de::DeserializeOwned>(&self) -> Result<T, crate::property::PropertyError> {
58        match self {
59            Value::Json(json) => Ok(serde_json::from_value(json.clone())?),
60            Value::Object(bytes) | Value::Binary(bytes) => Ok(serde_json::from_slice(bytes)?),
61            Value::String(s) => Ok(serde_json::from_str(s)?),
62            other => {
63                Err(crate::property::PropertyError::InvalidVariant { given: other.clone(), ty: std::any::type_name::<T>().to_string() })
64            }
65        }
66    }
67
68    /// Parse this value as a string using FromStr.
69    /// Only works for Value::String variant.
70    /// Returns InvalidVariant error for other types.
71    pub fn parse_as_string<T: std::str::FromStr>(&self) -> Result<T, crate::property::PropertyError> {
72        match self {
73            Value::String(s) => s
74                .parse()
75                .map_err(|_| crate::property::PropertyError::InvalidValue { value: s.clone(), ty: std::any::type_name::<T>().to_string() }),
76            other => {
77                Err(crate::property::PropertyError::InvalidVariant { given: other.clone(), ty: std::any::type_name::<T>().to_string() })
78            }
79        }
80    }
81
82    /// Extract value at a sub-path within structured data.
83    /// Returns None if the path doesn't exist (missing - distinct from null).
84    /// For empty path, returns self unchanged.
85    /// Supports Json, Binary, and String (permissive for backward compat).
86    pub fn extract_at_path(&self, path: &[String]) -> Option<Value> {
87        if path.is_empty() {
88            return Some(self.clone());
89        }
90
91        match self {
92            Value::Json(json) => {
93                let mut current = json;
94                for key in path {
95                    current = current.get(key)?;
96                }
97                Some(json_value_to_value(current))
98            }
99            Value::Binary(bytes) => {
100                let json: serde_json::Value = serde_json::from_slice(bytes).ok()?;
101                let mut current = &json;
102                for key in path {
103                    current = current.get(key)?;
104                }
105                Some(json_value_to_value(current))
106            }
107            Value::String(s) => {
108                let json: serde_json::Value = serde_json::from_str(s).ok()?;
109                let mut current = &json;
110                for key in path {
111                    current = current.get(key)?;
112                }
113                Some(json_value_to_value(current))
114            }
115            _ => None,
116        }
117    }
118}
119
120/// Convert serde_json::Value to ankurah Value
121fn json_value_to_value(json: &serde_json::Value) -> Value {
122    match json {
123        serde_json::Value::Null => Value::Json(serde_json::Value::Null),
124        serde_json::Value::Bool(b) => Value::Bool(*b),
125        serde_json::Value::Number(n) => {
126            if let Some(i) = n.as_i64() {
127                Value::I64(i)
128            } else if let Some(f) = n.as_f64() {
129                Value::F64(f)
130            } else {
131                Value::String(n.to_string())
132            }
133        }
134        serde_json::Value::String(s) => Value::String(s.clone()),
135        // Arrays and objects remain as Json
136        serde_json::Value::Array(_) | serde_json::Value::Object(_) => Value::Json(json.clone()),
137    }
138}
139
140#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
141pub enum ValueType {
142    I16,
143    I32,
144    I64,
145    F64,
146    Bool,
147    String,
148    EntityId,
149    Object,
150    Binary,
151    Json,
152}
153
154impl ValueType {
155    pub fn of(v: &Value) -> Self {
156        match v {
157            Value::I16(_) => ValueType::I16,
158            Value::I32(_) => ValueType::I32,
159            Value::I64(_) => ValueType::I64,
160            Value::F64(_) => ValueType::F64,
161            Value::Bool(_) => ValueType::Bool,
162            Value::String(_) => ValueType::String,
163            Value::EntityId(_) => ValueType::EntityId,
164            Value::Object(_) => ValueType::Object,
165            Value::Binary(_) => ValueType::Binary,
166            Value::Json(_) => ValueType::Json,
167        }
168    }
169}
170
171impl PartialOrd for Value {
172    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
173        match (self, other) {
174            // Same types - compare directly
175            (Value::I16(a), Value::I16(b)) => a.partial_cmp(b),
176            (Value::I32(a), Value::I32(b)) => a.partial_cmp(b),
177            (Value::I64(a), Value::I64(b)) => a.partial_cmp(b),
178            (Value::F64(a), Value::F64(b)) => a.partial_cmp(b),
179            (Value::Bool(a), Value::Bool(b)) => a.partial_cmp(b),
180            (Value::String(a), Value::String(b)) => a.partial_cmp(b),
181            (Value::EntityId(a), Value::EntityId(b)) => a.to_bytes().partial_cmp(&b.to_bytes()),
182            (Value::Object(a), Value::Object(b)) => a.partial_cmp(b),
183            (Value::Binary(a), Value::Binary(b)) => a.partial_cmp(b),
184            // JSON values: compare by serialized form (not ideal but works for basic cases)
185            (Value::Json(a), Value::Json(b)) => a.to_string().partial_cmp(&b.to_string()),
186            // Cross-type comparison: different types are not comparable
187            _ => None,
188        }
189    }
190}
191
192// Comparison operators for Value (used in filter.rs)
193impl Value {
194    pub fn gt(&self, other: &Self) -> bool { self.partial_cmp(other) == Some(std::cmp::Ordering::Greater) }
195
196    pub fn ge(&self, other: &Self) -> bool {
197        matches!(self.partial_cmp(other), Some(std::cmp::Ordering::Greater | std::cmp::Ordering::Equal))
198    }
199
200    pub fn lt(&self, other: &Self) -> bool { self.partial_cmp(other) == Some(std::cmp::Ordering::Less) }
201
202    pub fn le(&self, other: &Self) -> bool { matches!(self.partial_cmp(other), Some(std::cmp::Ordering::Less | std::cmp::Ordering::Equal)) }
203}
204
205impl Display for Value {
206    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
207        match self {
208            Value::I16(int) => write!(f, "{:?}", int),
209            Value::I32(int) => write!(f, "{:?}", int),
210            Value::I64(int) => write!(f, "{:?}", int),
211            Value::F64(float) => write!(f, "{:?}", float),
212            Value::Bool(bool) => write!(f, "{:?}", bool),
213            Value::String(string) => write!(f, "{:?}", string),
214            Value::EntityId(entity_id) => write!(f, "{}", entity_id),
215            Value::Object(object) => write!(f, "{:?}", object),
216            Value::Binary(binary) => write!(f, "{:?}", binary),
217            Value::Json(json) => write!(f, "{}", json),
218        }
219    }
220}
221
222impl From<ankql::ast::Literal> for Value {
223    fn from(literal: ankql::ast::Literal) -> Self {
224        match literal {
225            ankql::ast::Literal::I16(i) => Value::I16(i),
226            ankql::ast::Literal::I32(i) => Value::I32(i),
227            ankql::ast::Literal::I64(i) => Value::I64(i),
228            ankql::ast::Literal::F64(f) => Value::F64(f),
229            ankql::ast::Literal::Bool(b) => Value::Bool(b),
230            ankql::ast::Literal::String(s) => Value::String(s),
231            ankql::ast::Literal::EntityId(ulid) => Value::EntityId(proto::EntityId::from_ulid(ulid)),
232            ankql::ast::Literal::Object(object) => Value::Object(object),
233            ankql::ast::Literal::Binary(binary) => Value::Binary(binary),
234            ankql::ast::Literal::Json(json) => Value::Json(json),
235        }
236    }
237}
238
239impl From<&ankql::ast::Literal> for Value {
240    fn from(literal: &ankql::ast::Literal) -> Self {
241        match literal {
242            ankql::ast::Literal::I16(i) => Value::I16(*i),
243            ankql::ast::Literal::I32(i) => Value::I32(*i),
244            ankql::ast::Literal::I64(i) => Value::I64(*i),
245            ankql::ast::Literal::F64(f) => Value::F64(*f),
246            ankql::ast::Literal::Bool(b) => Value::Bool(*b),
247            ankql::ast::Literal::String(s) => Value::String(s.clone()),
248            ankql::ast::Literal::EntityId(ulid) => Value::EntityId(proto::EntityId::from_ulid(*ulid)),
249            ankql::ast::Literal::Object(object) => Value::Object(object.clone()),
250            ankql::ast::Literal::Binary(binary) => Value::Binary(binary.clone()),
251            ankql::ast::Literal::Json(json) => Value::Json(json.clone()),
252        }
253    }
254}
255
256impl From<Value> for ankql::ast::Literal {
257    fn from(value: Value) -> Self {
258        match value {
259            Value::I16(i) => ankql::ast::Literal::I16(i),
260            Value::I32(i) => ankql::ast::Literal::I32(i),
261            Value::I64(i) => ankql::ast::Literal::I64(i),
262            Value::F64(f) => ankql::ast::Literal::F64(f),
263            Value::Bool(b) => ankql::ast::Literal::Bool(b),
264            Value::String(s) => ankql::ast::Literal::String(s),
265            Value::EntityId(entity_id) => ankql::ast::Literal::EntityId(entity_id.to_ulid()),
266            Value::Object(bytes) => ankql::ast::Literal::String(String::from_utf8_lossy(&bytes).to_string()),
267            Value::Binary(bytes) => ankql::ast::Literal::String(String::from_utf8_lossy(&bytes).to_string()),
268            Value::Json(json) => ankql::ast::Literal::Json(json),
269        }
270    }
271}
272
273impl From<&Value> for ankql::ast::Literal {
274    fn from(value: &Value) -> Self {
275        match value {
276            Value::I16(i) => ankql::ast::Literal::I16(*i),
277            Value::I32(i) => ankql::ast::Literal::I32(*i),
278            Value::I64(i) => ankql::ast::Literal::I64(*i),
279            Value::F64(f) => ankql::ast::Literal::F64(*f),
280            Value::Bool(b) => ankql::ast::Literal::Bool(*b),
281            Value::String(s) => ankql::ast::Literal::String(s.clone()),
282            Value::EntityId(entity_id) => ankql::ast::Literal::EntityId(entity_id.to_ulid()),
283            Value::Object(bytes) => ankql::ast::Literal::String(String::from_utf8_lossy(bytes).to_string()),
284            Value::Binary(bytes) => ankql::ast::Literal::String(String::from_utf8_lossy(bytes).to_string()),
285            Value::Json(json) => ankql::ast::Literal::Json(json.clone()),
286        }
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    #[test]
295    fn test_extract_at_path_empty() {
296        let value = Value::String("hello".to_string());
297        let result = value.extract_at_path(&[]);
298        assert_eq!(result, Some(Value::String("hello".to_string())));
299    }
300
301    #[test]
302    fn test_extract_at_path_json_string() {
303        let json = serde_json::json!({ "session_id": "sess123" });
304        let value = Value::Json(json);
305
306        let result = value.extract_at_path(&["session_id".to_string()]);
307        assert_eq!(result, Some(Value::String("sess123".to_string())));
308    }
309
310    #[test]
311    fn test_extract_at_path_json_number() {
312        let json = serde_json::json!({ "count": 42 });
313        let value = Value::Json(json);
314
315        let result = value.extract_at_path(&["count".to_string()]);
316        assert_eq!(result, Some(Value::I64(42)));
317    }
318
319    #[test]
320    fn test_extract_at_path_json_nested() {
321        let json = serde_json::json!({ "context": { "user": { "name": "Alice" } } });
322        let value = Value::Json(json);
323
324        let result = value.extract_at_path(&["context".to_string(), "user".to_string(), "name".to_string()]);
325        assert_eq!(result, Some(Value::String("Alice".to_string())));
326    }
327
328    #[test]
329    fn test_extract_at_path_missing() {
330        let json = serde_json::json!({ "session_id": "sess123" });
331        let value = Value::Json(json);
332
333        let result = value.extract_at_path(&["nonexistent".to_string()]);
334        assert_eq!(result, None);
335    }
336
337    #[test]
338    fn test_extract_at_path_non_json() {
339        let value = Value::String("not json".to_string());
340
341        // Non-empty path on non-JSON returns None
342        let result = value.extract_at_path(&["field".to_string()]);
343        assert_eq!(result, None);
344    }
345}