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::Array(arr) => {
172 let vec: Vec<Dynamic> = arr.iter().map(value_to_dynamic).collect();
173 Dynamic::from(vec)
174 }
175 Value::Object(map) => {
176 let mut rhai_map = rhai::Map::new();
177 for (k, v) in map {
178 rhai_map.insert(k.clone().into(), value_to_dynamic(v));
179 }
180 Dynamic::from(rhai_map)
181 }
182 }
183}
184
185fn dynamic_to_value(dyn_val: Dynamic) -> Option<Value> {
187 if dyn_val.is_unit() {
188 return Some(Value::Null);
189 }
190 if let Some(b) = dyn_val.clone().try_cast::<bool>() {
191 return Some(Value::Bool(b));
192 }
193 if let Some(i) = dyn_val.clone().try_cast::<i64>() {
194 return Some(Value::Int(i));
195 }
196 if let Some(f) = dyn_val.clone().try_cast::<f64>() {
197 return Some(Value::Float(f));
198 }
199 if let Some(s) = dyn_val.clone().try_cast::<String>() {
200 return Some(Value::String(s));
201 }
202 if let Some(arr) = dyn_val.clone().try_cast::<Vec<Dynamic>>() {
203 let converted: Vec<Value> = arr.into_iter().filter_map(dynamic_to_value).collect();
204 return Some(Value::Array(converted));
205 }
206 if let Some(map) = dyn_val.try_cast::<rhai::Map>() {
207 let mut obj = HashMap::new();
208 for (k, v) in map {
209 if let Some(val) = dynamic_to_value(v) {
210 obj.insert(k.to_string(), val);
211 }
212 }
213 return Some(Value::Object(obj));
214 }
215 None
216}
217
218fn evaluate_rhai_script(script: &str, doc: &Document) -> Option<Value> {
220 let engine = Engine::new();
221 let mut scope = Scope::new();
222
223 let mut doc_map = rhai::Map::new();
225 for (key, value) in &doc.data {
226 doc_map.insert(key.clone().into(), value_to_dynamic(value));
227 }
228
229 scope.push("doc", doc_map);
231
232 match engine.eval_with_scope::<Dynamic>(&mut scope, script) {
234 Ok(result) => dynamic_to_value(result),
235 Err(_) => None, }
237}
238
239pub struct ComputedEngine {
241 engine: Arc<Engine>,
242}
243
244impl ComputedEngine {
245 pub fn new() -> Self {
247 let mut engine = Engine::new();
248
249 engine.register_fn("uppercase", |s: &str| s.to_uppercase());
251 engine.register_fn("lowercase", |s: &str| s.to_lowercase());
252 engine.register_fn("trim", |s: &str| s.trim().to_string());
253 engine.register_fn("len", |s: &str| s.len() as i64);
254
255 engine.register_fn("abs", |x: i64| x.abs());
257 engine.register_fn("abs", |x: f64| x.abs());
258 engine.register_fn("round", |x: f64| x.round());
259 engine.register_fn("floor", |x: f64| x.floor());
260 engine.register_fn("ceil", |x: f64| x.ceil());
261 engine.register_fn("min", |a: i64, b: i64| a.min(b));
262 engine.register_fn("max", |a: i64, b: i64| a.max(b));
263
264 Self {
265 engine: Arc::new(engine),
266 }
267 }
268
269 pub fn evaluate(&self, script: &str, doc: &Document) -> Option<Value> {
271 let mut scope = Scope::new();
272
273 let mut doc_map = rhai::Map::new();
275 for (key, value) in &doc.data {
276 doc_map.insert(key.clone().into(), value_to_dynamic(value));
277 }
278
279 scope.push("doc", doc_map);
280
281 match self.engine.eval_with_scope::<Dynamic>(&mut scope, script) {
282 Ok(result) => dynamic_to_value(result),
283 Err(_) => None,
284 }
285 }
286}
287
288impl Default for ComputedEngine {
289 fn default() -> Self {
290 Self::new()
291 }
292}
293
294pub struct ComputedFields {
296 fields: HashMap<String, HashMap<String, ComputedExpression>>,
298 engine: ComputedEngine,
299}
300
301impl ComputedFields {
302 pub fn new() -> Self {
303 Self {
304 fields: HashMap::new(),
305 engine: ComputedEngine::new(),
306 }
307 }
308
309 pub fn register(
311 &mut self,
312 collection: impl Into<String>,
313 field: impl Into<String>,
314 expression: ComputedExpression,
315 ) {
316 let collection = collection.into();
317 self.fields
318 .entry(collection)
319 .or_default()
320 .insert(field.into(), expression);
321 }
322
323 pub fn apply(&self, collection: &str, doc: &mut Document) -> Result<()> {
325 if let Some(computed) = self.fields.get(collection) {
326 for (field_name, expression) in computed {
327 if let Some(value) = expression.evaluate(doc) {
328 doc.data.insert(field_name.clone(), value);
329 }
330 }
331 }
332 Ok(())
333 }
334
335 pub fn get_fields(&self, collection: &str) -> Option<&HashMap<String, ComputedExpression>> {
337 self.fields.get(collection)
338 }
339
340 pub fn evaluate_script(&self, script: &str, doc: &Document) -> Option<Value> {
342 self.engine.evaluate(script, doc)
343 }
344}
345
346impl Default for ComputedFields {
347 fn default() -> Self {
348 Self::new()
349 }
350}
351
352#[cfg(test)]
353mod tests {
354 use super::*;
355
356 #[test]
357 fn test_concat_expression() {
358 let expr =
359 ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]);
360
361 let mut doc = Document::new();
362 doc.data
363 .insert("first_name".to_string(), Value::String("John".to_string()));
364 doc.data
365 .insert("last_name".to_string(), Value::String("Doe".to_string()));
366
367 let result = expr.evaluate(&doc);
368 assert_eq!(result, Some(Value::String("John Doe".to_string())));
369 }
370
371 #[test]
372 fn test_sum_expression() {
373 let expr = ComputedExpression::Sum(vec!["a".to_string(), "b".to_string(), "c".to_string()]);
374
375 let mut doc = Document::new();
376 doc.data.insert("a".to_string(), Value::Int(10));
377 doc.data.insert("b".to_string(), Value::Int(20));
378 doc.data.insert("c".to_string(), Value::Int(30));
379
380 let result = expr.evaluate(&doc);
381 assert_eq!(result, Some(Value::Int(60)));
382 }
383
384 #[test]
385 fn test_average_expression() {
386 let expr = ComputedExpression::Average(vec!["score1".to_string(), "score2".to_string()]);
387
388 let mut doc = Document::new();
389 doc.data.insert("score1".to_string(), Value::Float(85.5));
390 doc.data.insert("score2".to_string(), Value::Float(92.5));
391
392 let result = expr.evaluate(&doc);
393 assert_eq!(result, Some(Value::Float(89.0)));
394 }
395
396 #[test]
397 fn test_computed_fields_registry() {
398 let mut registry = ComputedFields::new();
399
400 registry.register(
401 "users",
402 "full_name",
403 ComputedExpression::Concat(vec!["first_name".to_string(), "last_name".to_string()]),
404 );
405
406 let mut doc = Document::new();
407 doc.data
408 .insert("first_name".to_string(), Value::String("Jane".to_string()));
409 doc.data
410 .insert("last_name".to_string(), Value::String("Smith".to_string()));
411
412 registry.apply("users", &mut doc).unwrap();
413
414 assert_eq!(
415 doc.data.get("full_name"),
416 Some(&Value::String("Jane Smith".to_string()))
417 );
418 }
419
420 #[test]
421 fn test_template_expression() {
422 let expr =
423 ComputedExpression::Template("Hello, ${name}! You are ${age} years old.".to_string());
424
425 let mut doc = Document::new();
426 doc.data
427 .insert("name".to_string(), Value::String("Alice".to_string()));
428 doc.data.insert("age".to_string(), Value::Int(30));
429
430 let result = expr.evaluate(&doc);
431 assert_eq!(
432 result,
433 Some(Value::String(
434 "Hello, Alice! You are 30 years old.".to_string()
435 ))
436 );
437 }
438
439 #[test]
440 fn test_rhai_simple_expression() {
441 let expr = ComputedExpression::Script("doc.price * doc.quantity".to_string());
442
443 let mut doc = Document::new();
444 doc.data.insert("price".to_string(), Value::Int(100));
445 doc.data.insert("quantity".to_string(), Value::Int(5));
446
447 let result = expr.evaluate(&doc);
448 assert_eq!(result, Some(Value::Int(500)));
449 }
450
451 #[test]
452 fn test_rhai_string_concat() {
453 let expr = ComputedExpression::Script(r#"doc.first + " " + doc.last"#.to_string());
454
455 let mut doc = Document::new();
456 doc.data
457 .insert("first".to_string(), Value::String("John".to_string()));
458 doc.data
459 .insert("last".to_string(), Value::String("Doe".to_string()));
460
461 let result = expr.evaluate(&doc);
462 assert_eq!(result, Some(Value::String("John Doe".to_string())));
463 }
464
465 #[test]
466 fn test_rhai_conditional() {
467 let expr = ComputedExpression::Script(
468 r#"if doc.age >= 18 { "adult" } else { "minor" }"#.to_string(),
469 );
470
471 let mut doc = Document::new();
472 doc.data.insert("age".to_string(), Value::Int(25));
473
474 let result = expr.evaluate(&doc);
475 assert_eq!(result, Some(Value::String("adult".to_string())));
476
477 doc.data.insert("age".to_string(), Value::Int(15));
478 let result = expr.evaluate(&doc);
479 assert_eq!(result, Some(Value::String("minor".to_string())));
480 }
481
482 #[test]
483 fn test_rhai_null_handling() {
484 let expr = ComputedExpression::Script("doc.missing_field".to_string());
485
486 let doc = Document::new();
487
488 let result = expr.evaluate(&doc);
490 assert_eq!(result, Some(Value::Null));
491 }
492
493 #[test]
494 fn test_computed_engine_builtin_functions() {
495 let engine = ComputedEngine::new();
496
497 let mut doc = Document::new();
498 doc.data
499 .insert("name".to_string(), Value::String("hello world".to_string()));
500 doc.data.insert("value".to_string(), Value::Float(3.7));
501
502 let result = engine.evaluate(r#"uppercase(doc.name)"#, &doc);
504 assert_eq!(result, Some(Value::String("HELLO WORLD".to_string())));
505
506 let result = engine.evaluate("round(doc.value)", &doc);
508 assert_eq!(result, Some(Value::Float(4.0)));
509 }
510}