1use crate::error::Result;
27use crate::types::{Document, Value};
28use rhai::{Dynamic, Engine, Scope};
29use serde::{Deserialize, Serialize};
30use std::collections::HashMap;
31use std::sync::Arc;
32
33#[derive(Debug, Clone, Serialize, Deserialize)]
35pub enum ComputedExpression {
36 Concat(Vec<String>),
38 Sum(Vec<String>),
40 Product(Vec<String>),
42 Average(Vec<String>),
44 Template(String),
47 Script(String),
50 #[serde(rename = "custom")]
52 Custom(String),
53}
54
55impl ComputedExpression {
56 pub fn evaluate(&self, doc: &Document) -> Option<Value> {
58 match self {
59 ComputedExpression::Concat(fields) => {
60 let mut result = String::new();
61 for field in fields {
62 if let Some(value) = doc.data.get(field)
63 && let Some(s) = value.as_str()
64 {
65 if !result.is_empty() {
66 result.push(' ');
67 }
68 result.push_str(s);
69 }
70 }
71 Some(Value::String(result))
72 }
73
74 ComputedExpression::Sum(fields) => {
75 let mut sum = 0i64;
76 for field in fields {
77 if let Some(value) = doc.data.get(field)
78 && let Some(i) = value.as_i64()
79 {
80 sum += i;
81 }
82 }
83 Some(Value::Int(sum))
84 }
85
86 ComputedExpression::Product(fields) => {
87 let mut product = 1i64;
88 for field in fields {
89 if let Some(value) = doc.data.get(field)
90 && let Some(i) = value.as_i64()
91 {
92 product *= i;
93 }
94 }
95 Some(Value::Int(product))
96 }
97
98 ComputedExpression::Average(fields) => {
99 let mut sum = 0.0;
100 let mut count = 0;
101 for field in fields {
102 if let Some(value) = doc.data.get(field) {
103 if let Some(f) = value.as_f64() {
104 sum += f;
105 count += 1;
106 } else if let Some(i) = value.as_i64() {
107 sum += i as f64;
108 count += 1;
109 }
110 }
111 }
112 if count > 0 {
113 Some(Value::Float(sum / count as f64))
114 } else {
115 None
116 }
117 }
118
119 ComputedExpression::Template(template) => {
120 Some(Value::String(interpolate_template(template, doc)))
121 }
122
123 ComputedExpression::Script(script) | ComputedExpression::Custom(script) => {
124 evaluate_rhai_script(script, doc)
125 }
126 }
127 }
128}
129
130fn interpolate_template(template: &str, doc: &Document) -> String {
133 let mut result = template.to_string();
134
135 while let Some(start) = result.find("${") {
137 if let Some(end) = result[start..].find('}') {
138 let end = start + end;
139 let field_name = &result[start + 2..end];
140
141 let replacement = doc
142 .data
143 .get(field_name)
144 .and_then(|v| match v {
145 Value::String(s) => Some(s.clone()),
146 Value::Int(i) => Some(i.to_string()),
147 Value::Float(f) => Some(f.to_string()),
148 Value::Bool(b) => Some(b.to_string()),
149 _ => None,
150 })
151 .unwrap_or_default();
152
153 result = format!("{}{}{}", &result[..start], replacement, &result[end + 1..]);
154 } else {
155 break;
156 }
157 }
158
159 result
160}
161
162fn value_to_dynamic(value: &Value) -> Dynamic {
164 match value {
165 Value::Null => Dynamic::UNIT,
166 Value::Bool(b) => Dynamic::from(*b),
167 Value::Int(i) => Dynamic::from(*i),
168 Value::Float(f) => Dynamic::from(*f),
169 Value::String(s) => Dynamic::from(s.clone()),
170 Value::Uuid(u) => Dynamic::from(u.to_string()),
171 Value::DateTime(dt) => Dynamic::from(dt.to_rfc3339()),
172 Value::Array(arr) => {
173 let vec: Vec<Dynamic> = arr.iter().map(value_to_dynamic).collect();
174 Dynamic::from(vec)
175 }
176 Value::Object(map) => {
177 let mut rhai_map = rhai::Map::new();
178 for (k, v) in map {
179 rhai_map.insert(k.clone().into(), value_to_dynamic(v));
180 }
181 Dynamic::from(rhai_map)
182 }
183 }
184}
185
186fn dynamic_to_value(dyn_val: Dynamic) -> Option<Value> {
188 if dyn_val.is_unit() {
189 return Some(Value::Null);
190 }
191 if let Some(b) = dyn_val.clone().try_cast::<bool>() {
192 return Some(Value::Bool(b));
193 }
194 if let Some(i) = dyn_val.clone().try_cast::<i64>() {
195 return Some(Value::Int(i));
196 }
197 if let Some(f) = dyn_val.clone().try_cast::<f64>() {
198 return Some(Value::Float(f));
199 }
200 if let Some(s) = dyn_val.clone().try_cast::<String>() {
201 if let Ok(dt) = chrono::DateTime::parse_from_rfc3339(&s) {
203 return Some(Value::DateTime(dt.with_timezone(&chrono::Utc)));
204 }
205 return Some(Value::String(s));
206 }
207 if let Some(arr) = dyn_val.clone().try_cast::<Vec<Dynamic>>() {
208 let converted: Vec<Value> = arr.into_iter().filter_map(dynamic_to_value).collect();
209 return Some(Value::Array(converted));
210 }
211 if let Some(map) = dyn_val.try_cast::<rhai::Map>() {
212 let mut obj = HashMap::new();
213 for (k, v) in map {
214 if let Some(val) = dynamic_to_value(v) {
215 obj.insert(k.to_string(), val);
216 }
217 }
218 return Some(Value::Object(obj));
219 }
220 None
221}
222
223fn evaluate_rhai_script(script: &str, doc: &Document) -> Option<Value> {
225 let engine = Engine::new();
226 let mut scope = Scope::new();
227
228 let mut doc_map = rhai::Map::new();
230 for (key, value) in &doc.data {
231 doc_map.insert(key.clone().into(), value_to_dynamic(value));
232 }
233
234 scope.push("doc", doc_map);
236
237 match engine.eval_with_scope::<Dynamic>(&mut scope, script) {
239 Ok(result) => dynamic_to_value(result),
240 Err(_) => None, }
242}
243
244pub struct ComputedEngine {
246 engine: Arc<Engine>,
247}
248
249impl ComputedEngine {
250 pub fn new() -> Self {
252 let mut engine = Engine::new();
253
254 engine.register_fn("uppercase", |s: &str| s.to_uppercase());
256 engine.register_fn("lowercase", |s: &str| s.to_lowercase());
257 engine.register_fn("trim", |s: &str| s.trim().to_string());
258 engine.register_fn("len", |s: &str| s.len() as i64);
259
260 engine.register_fn("abs", |x: i64| x.abs());
262 engine.register_fn("abs", |x: f64| x.abs());
263 engine.register_fn("round", |x: f64| x.round());
264 engine.register_fn("floor", |x: f64| x.floor());
265 engine.register_fn("ceil", |x: f64| x.ceil());
266 engine.register_fn("min", |a: i64, b: i64| a.min(b));
267 engine.register_fn("max", |a: i64, b: i64| a.max(b));
268
269 Self {
270 engine: Arc::new(engine),
271 }
272 }
273
274 pub fn evaluate(&self, script: &str, doc: &Document) -> Option<Value> {
276 let mut scope = Scope::new();
277
278 let mut doc_map = rhai::Map::new();
280 for (key, value) in &doc.data {
281 doc_map.insert(key.clone().into(), value_to_dynamic(value));
282 }
283
284 scope.push("doc", doc_map);
285
286 match self.engine.eval_with_scope::<Dynamic>(&mut scope, script) {
287 Ok(result) => dynamic_to_value(result),
288 Err(_) => None,
289 }
290 }
291}
292
293impl Default for ComputedEngine {
294 fn default() -> Self {
295 Self::new()
296 }
297}
298
299pub struct ComputedFields {
301 fields: HashMap<String, HashMap<String, ComputedExpression>>,
303 engine: ComputedEngine,
304}
305
306impl ComputedFields {
307 pub fn new() -> Self {
308 Self {
309 fields: HashMap::new(),
310 engine: ComputedEngine::new(),
311 }
312 }
313
314 pub fn register(
316 &mut self,
317 collection: impl Into<String>,
318 field: impl Into<String>,
319 expression: ComputedExpression,
320 ) {
321 let collection = collection.into();
322 self.fields
323 .entry(collection)
324 .or_default()
325 .insert(field.into(), expression);
326 }
327
328 pub fn apply(&self, collection: &str, doc: &mut Document) -> Result<()> {
330 if let Some(computed) = self.fields.get(collection) {
331 for (field_name, expression) in computed {
332 if let Some(value) = expression.evaluate(doc) {
333 doc.data.insert(field_name.clone(), value);
334 }
335 }
336 }
337 Ok(())
338 }
339
340 pub fn get_fields(&self, collection: &str) -> Option<&HashMap<String, ComputedExpression>> {
342 self.fields.get(collection)
343 }
344
345 pub fn evaluate_script(&self, script: &str, doc: &Document) -> Option<Value> {
347 self.engine.evaluate(script, doc)
348 }
349}
350
351impl Default for ComputedFields {
352 fn default() -> Self {
353 Self::new()
354 }
355}
356
357#[cfg(test)]
358mod tests {
359 use super::*;
360
361 #[test]
362 fn test_concat_expression() {
363 let expr =
364 ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]);
365
366 let mut doc = Document::new();
367 doc.data
368 .insert("first_name".to_string(), Value::String("John".to_string()));
369 doc.data
370 .insert("last_name".to_string(), Value::String("Doe".to_string()));
371
372 let result = expr.evaluate(&doc);
373 assert_eq!(result, Some(Value::String("John Doe".to_string())));
374 }
375
376 #[test]
377 fn test_sum_expression() {
378 let expr = ComputedExpression::Sum(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
379
380 let mut doc = Document::new();
381 doc.data.insert("a".to_string(), Value::Int(10));
382 doc.data.insert("b".to_string(), Value::Int(20));
383 doc.data.insert("c".to_string(), Value::Int(30));
384
385 let result = expr.evaluate(&doc);
386 assert_eq!(result, Some(Value::Int(60)));
387 }
388
389 #[test]
390 fn test_average_expression() {
391 let expr = ComputedExpression::Average(vec!["score1".to_string(), "score2".to_string()]);
392
393 let mut doc = Document::new();
394 doc.data.insert("score1".to_string(), Value::Float(85.5));
395 doc.data.insert("score2".to_string(), Value::Float(92.5));
396
397 let result = expr.evaluate(&doc);
398 assert_eq!(result, Some(Value::Float(89.0)));
399 }
400
401 #[test]
402 fn test_computed_fields_registry() {
403 let mut registry = ComputedFields::new();
404
405 registry.register(
406 "users",
407 "full_name",
408 ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]),
409 );
410
411 let mut doc = Document::new();
412 doc.data
413 .insert("first_name".to_string(), Value::String("Jane".to_string()));
414 doc.data
415 .insert("last_name".to_string(), Value::String("Smith".to_string()));
416
417 registry.apply("users", &mut doc).unwrap();
418
419 assert_eq!(
420 doc.data.get("full_name"),
421 Some(&Value::String("Jane Smith".to_string()))
422 );
423 }
424
425 #[test]
426 fn test_template_expression() {
427 let expr =
428 ComputedExpression::Template("Hello, ${name}! You are ${age} years old.".to_string());
429
430 let mut doc = Document::new();
431 doc.data
432 .insert("name".to_string(), Value::String("Alice".to_string()));
433 doc.data.insert("age".to_string(), Value::Int(30));
434
435 let result = expr.evaluate(&doc);
436 assert_eq!(
437 result,
438 Some(Value::String(
439 "Hello, Alice! You are 30 years old.".to_string()
440 ))
441 );
442 }
443
444 #[test]
445 fn test_rhai_simple_expression() {
446 let expr = ComputedExpression::Script("doc.price * doc.quantity".to_string());
447
448 let mut doc = Document::new();
449 doc.data.insert("price".to_string(), Value::Int(100));
450 doc.data.insert("quantity".to_string(), Value::Int(5));
451
452 let result = expr.evaluate(&doc);
453 assert_eq!(result, Some(Value::Int(500)));
454 }
455
456 #[test]
457 fn test_rhai_string_concat() {
458 let expr = ComputedExpression::Script(r#"doc.first + " " + doc.last"#.to_string());
459
460 let mut doc = Document::new();
461 doc.data
462 .insert("first".to_string(), Value::String("John".to_string()));
463 doc.data
464 .insert("last".to_string(), Value::String("Doe".to_string()));
465
466 let result = expr.evaluate(&doc);
467 assert_eq!(result, Some(Value::String("John Doe".to_string())));
468 }
469
470 #[test]
471 fn test_rhai_conditional() {
472 let expr = ComputedExpression::Script(
473 r#"if doc.age >= 18 { "adult" } else { "minor" }"#.to_string(),
474 );
475
476 let mut doc = Document::new();
477 doc.data.insert("age".to_string(), Value::Int(25));
478
479 let result = expr.evaluate(&doc);
480 assert_eq!(result, Some(Value::String("adult".to_string())));
481
482 doc.data.insert("age".to_string(), Value::Int(15));
483 let result = expr.evaluate(&doc);
484 assert_eq!(result, Some(Value::String("minor".to_string())));
485 }
486
487 #[test]
488 fn test_rhai_null_handling() {
489 let expr = ComputedExpression::Script("doc.missing_field".to_string());
490
491 let doc = Document::new();
492
493 let result = expr.evaluate(&doc);
495 assert_eq!(result, Some(Value::Null));
496 }
497
498 #[test]
499 fn test_computed_engine_builtin_functions() {
500 let engine = ComputedEngine::new();
501
502 let mut doc = Document::new();
503 doc.data
504 .insert("name".to_string(), Value::String("hello world".to_string()));
505 doc.data.insert("value".to_string(), Value::Float(3.7));
506
507 let result = engine.evaluate(r#"uppercase(doc.name)"#, &doc);
509 assert_eq!(result, Some(Value::String("HELLO WORLD".to_string())));
510
511 let result = engine.evaluate("round(doc.value)", &doc);
513 assert_eq!(result, Some(Value::Float(4.0)));
514 }
515
516 #[test]
517 fn test_datetime_roundtrip_through_script() {
518 use chrono::TimeZone;
519 let ts = chrono::Utc.with_ymd_and_hms(2024, 6, 15, 12, 0, 0).unwrap();
520
521 let expr = ComputedExpression::Script("doc.created_at".to_string());
522
523 let mut doc = Document::new();
524 doc.data
525 .insert("created_at".to_string(), Value::DateTime(ts));
526
527 let result = expr.evaluate(&doc);
528 assert_eq!(
529 result,
530 Some(Value::DateTime(ts)),
531 "DateTime should survive a round-trip through value_to_dynamic / dynamic_to_value"
532 );
533 }
534}