xerv_core/
value.rs

1//! Dynamic value type for condition evaluation.
2//!
3//! Provides a flexible value type for field access and comparison
4//! in flow control nodes (switch, loop, etc.).
5
6use crate::error::{Result, XervError};
7use serde::{Deserialize, Serialize};
8use serde_json::Value as JsonValue;
9
10/// Dynamic value for field access and condition evaluation.
11///
12/// Wraps serde_json::Value to provide type-safe field extraction
13/// and comparison operations used by flow control nodes.
14#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
15#[serde(transparent)]
16pub struct Value(pub JsonValue);
17
18impl Value {
19    /// Create a null value.
20    pub fn null() -> Self {
21        Self(JsonValue::Null)
22    }
23
24    /// Create a boolean value.
25    pub fn bool(v: bool) -> Self {
26        Self(JsonValue::Bool(v))
27    }
28
29    /// Create an integer value.
30    pub fn int(v: i64) -> Self {
31        Self(JsonValue::Number(v.into()))
32    }
33
34    /// Create a floating-point value.
35    pub fn float(v: f64) -> Self {
36        Self(serde_json::Number::from_f64(v).map_or(JsonValue::Null, JsonValue::Number))
37    }
38
39    /// Create a string value.
40    pub fn string(v: impl Into<String>) -> Self {
41        Self(JsonValue::String(v.into()))
42    }
43
44    /// Create a value from JSON bytes.
45    pub fn from_bytes(bytes: &[u8]) -> Result<Self> {
46        if bytes.is_empty() {
47            return Ok(Self::null());
48        }
49        serde_json::from_slice(bytes)
50            .map(Self)
51            .map_err(|e| XervError::Serialization(format!("Failed to parse value: {}", e)))
52    }
53
54    /// Serialize to JSON bytes.
55    pub fn to_bytes(&self) -> Result<Vec<u8>> {
56        serde_json::to_vec(&self.0)
57            .map_err(|e| XervError::Serialization(format!("Failed to serialize value: {}", e)))
58    }
59
60    /// Check if the value is null.
61    pub fn is_null(&self) -> bool {
62        self.0.is_null()
63    }
64
65    /// Get a field by path (dot notation or JSONPath-like).
66    ///
67    /// Supports:
68    /// - Simple field access: "field"
69    /// - Dot notation: "parent.child.value"
70    /// - JSONPath prefix: "$.parent.child" -> "parent.child"
71    ///
72    /// Returns None if the field doesn't exist.
73    pub fn get_field(&self, path: &str) -> Option<Value> {
74        // Strip JSONPath prefix if present
75        let path = path.strip_prefix("$.").unwrap_or(path);
76
77        let mut current = &self.0;
78        for part in path.split('.') {
79            // Handle array index notation: field[0]
80            if let Some((field, idx_str)) = part.split_once('[') {
81                current = current.get(field)?;
82                let idx_str = idx_str.strip_suffix(']')?;
83                let idx: usize = idx_str.parse().ok()?;
84                current = current.get(idx)?;
85            } else {
86                current = current.get(part)?;
87            }
88        }
89        Some(Value(current.clone()))
90    }
91
92    /// Get a field as a string.
93    pub fn get_string(&self, path: &str) -> Option<String> {
94        self.get_field(path).and_then(|v| v.as_string())
95    }
96
97    /// Get a field as an f64.
98    pub fn get_f64(&self, path: &str) -> Option<f64> {
99        self.get_field(path).and_then(|v| v.as_f64())
100    }
101
102    /// Get a field as a bool.
103    pub fn get_bool(&self, path: &str) -> Option<bool> {
104        self.get_field(path).and_then(|v| v.as_bool())
105    }
106
107    /// Convert to string if possible.
108    pub fn as_string(&self) -> Option<String> {
109        match &self.0 {
110            JsonValue::String(s) => Some(s.clone()),
111            JsonValue::Number(n) => Some(n.to_string()),
112            JsonValue::Bool(b) => Some(b.to_string()),
113            JsonValue::Null => None,
114            _ => Some(self.0.to_string()),
115        }
116    }
117
118    /// Convert to f64 if possible.
119    pub fn as_f64(&self) -> Option<f64> {
120        match &self.0 {
121            JsonValue::Number(n) => n.as_f64(),
122            JsonValue::String(s) => s.parse().ok(),
123            _ => None,
124        }
125    }
126
127    /// Convert to bool if possible.
128    pub fn as_bool(&self) -> Option<bool> {
129        match &self.0 {
130            JsonValue::Bool(b) => Some(*b),
131            JsonValue::String(s) => match s.to_lowercase().as_str() {
132                "true" | "1" | "yes" => Some(true),
133                "false" | "0" | "no" => Some(false),
134                _ => None,
135            },
136            JsonValue::Number(n) => Some(n.as_f64().map_or(false, |v| v != 0.0)),
137            JsonValue::Null => Some(false),
138            _ => None,
139        }
140    }
141
142    /// Check equality with a string value.
143    pub fn equals_str(&self, other: &str) -> bool {
144        self.as_string().map_or(false, |s| s == other)
145    }
146
147    /// Check if a field equals a value (string comparison).
148    pub fn field_equals(&self, path: &str, value: &str) -> bool {
149        self.get_field(path).map_or(false, |v| v.equals_str(value))
150    }
151
152    /// Check if a field is greater than a threshold (numeric comparison).
153    pub fn field_greater_than(&self, path: &str, threshold: f64) -> bool {
154        self.get_f64(path).map_or(false, |v| v > threshold)
155    }
156
157    /// Check if a field is less than a threshold (numeric comparison).
158    pub fn field_less_than(&self, path: &str, threshold: f64) -> bool {
159        self.get_f64(path).map_or(false, |v| v < threshold)
160    }
161
162    /// Check if a field matches a regex pattern.
163    pub fn field_matches(&self, path: &str, pattern: &str) -> bool {
164        let Some(field_value) = self.get_string(path) else {
165            return false;
166        };
167        regex::Regex::new(pattern)
168            .map(|re| re.is_match(&field_value))
169            .unwrap_or(false)
170    }
171
172    /// Check if a boolean field is true.
173    pub fn field_is_true(&self, path: &str) -> bool {
174        self.get_bool(path).unwrap_or(false)
175    }
176
177    /// Check if a boolean field is false.
178    pub fn field_is_false(&self, path: &str) -> bool {
179        self.get_bool(path).map_or(false, |b| !b)
180    }
181
182    /// Access the inner serde_json::Value.
183    pub fn inner(&self) -> &JsonValue {
184        &self.0
185    }
186
187    /// Convert into the inner serde_json::Value.
188    pub fn into_inner(self) -> JsonValue {
189        self.0
190    }
191}
192
193impl Default for Value {
194    fn default() -> Self {
195        Self::null()
196    }
197}
198
199impl From<JsonValue> for Value {
200    fn from(v: JsonValue) -> Self {
201        Self(v)
202    }
203}
204
205impl From<Value> for JsonValue {
206    fn from(v: Value) -> Self {
207        v.0
208    }
209}
210
211impl From<&str> for Value {
212    fn from(s: &str) -> Self {
213        Self::string(s)
214    }
215}
216
217impl From<String> for Value {
218    fn from(s: String) -> Self {
219        Self::string(s)
220    }
221}
222
223impl From<i64> for Value {
224    fn from(v: i64) -> Self {
225        Self::int(v)
226    }
227}
228
229impl From<f64> for Value {
230    fn from(v: f64) -> Self {
231        Self::float(v)
232    }
233}
234
235impl From<bool> for Value {
236    fn from(v: bool) -> Self {
237        Self::bool(v)
238    }
239}
240
241#[cfg(test)]
242mod tests {
243    use super::*;
244    use serde_json::json;
245
246    #[test]
247    fn value_from_bytes() {
248        let bytes = br#"{"name": "test", "score": 0.95}"#;
249        let value = Value::from_bytes(bytes).unwrap();
250
251        assert_eq!(value.get_string("name"), Some("test".to_string()));
252        assert_eq!(value.get_f64("score"), Some(0.95));
253    }
254
255    #[test]
256    fn value_nested_field_access() {
257        let value = Value(json!({
258            "result": {
259                "status": "success",
260                "data": {
261                    "count": 42
262                }
263            }
264        }));
265
266        assert_eq!(
267            value.get_string("result.status"),
268            Some("success".to_string())
269        );
270        assert_eq!(value.get_f64("result.data.count"), Some(42.0));
271    }
272
273    #[test]
274    fn value_jsonpath_prefix() {
275        let value = Value(json!({"score": 0.9}));
276
277        // Both should work
278        assert_eq!(value.get_f64("score"), Some(0.9));
279        assert_eq!(value.get_f64("$.score"), Some(0.9));
280    }
281
282    #[test]
283    fn value_array_access() {
284        let value = Value(json!({
285            "items": [
286                {"name": "first"},
287                {"name": "second"}
288            ]
289        }));
290
291        assert_eq!(value.get_string("items[0].name"), Some("first".to_string()));
292        assert_eq!(
293            value.get_string("items[1].name"),
294            Some("second".to_string())
295        );
296    }
297
298    #[test]
299    fn field_equals() {
300        let value = Value(json!({"status": "active"}));
301        assert!(value.field_equals("status", "active"));
302        assert!(!value.field_equals("status", "inactive"));
303    }
304
305    #[test]
306    fn field_greater_than() {
307        let value = Value(json!({"score": 0.85}));
308        assert!(value.field_greater_than("score", 0.8));
309        assert!(!value.field_greater_than("score", 0.9));
310    }
311
312    #[test]
313    fn field_less_than() {
314        let value = Value(json!({"temperature": 25.5}));
315        assert!(value.field_less_than("temperature", 30.0));
316        assert!(!value.field_less_than("temperature", 20.0));
317    }
318
319    #[test]
320    fn field_matches() {
321        let value = Value(json!({"email": "user@example.com"}));
322        assert!(value.field_matches("email", r"^[\w.+-]+@[\w.-]+\.\w+$"));
323        assert!(!value.field_matches("email", r"^invalid"));
324    }
325
326    #[test]
327    fn field_bool_checks() {
328        let value = Value(json!({"success": true, "failed": false}));
329        assert!(value.field_is_true("success"));
330        assert!(!value.field_is_true("failed"));
331        assert!(value.field_is_false("failed"));
332        assert!(!value.field_is_false("success"));
333    }
334
335    #[test]
336    fn empty_bytes_returns_null() {
337        let value = Value::from_bytes(&[]).unwrap();
338        assert!(value.is_null());
339    }
340
341    #[test]
342    fn missing_field_returns_none() {
343        let value = Value(json!({"a": 1}));
344        assert!(value.get_field("missing").is_none());
345        assert!(value.get_f64("missing").is_none());
346    }
347}