rust_rule_engine/plugins/
collection_utils.rs

1#![allow(clippy::collapsible_match)]
2
3use crate::engine::plugin::{PluginHealth, PluginMetadata, PluginState, RulePlugin};
4use crate::engine::RustRuleEngine;
5use crate::errors::{Result, RuleEngineError};
6use crate::types::Value;
7use std::collections::HashMap;
8
9/// Built-in plugin for collection operations
10pub struct CollectionUtilsPlugin {
11    metadata: PluginMetadata,
12}
13
14impl Default for CollectionUtilsPlugin {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl CollectionUtilsPlugin {
21    pub fn new() -> Self {
22        Self {
23            metadata: PluginMetadata {
24                name: "collection_utils".to_string(),
25                version: "1.0.0".to_string(),
26                description: "Collection manipulation utilities".to_string(),
27                author: "Rust Rule Engine Team".to_string(),
28                state: PluginState::Loaded,
29                health: PluginHealth::Healthy,
30                actions: vec![
31                    "ArrayLength".to_string(),
32                    "ArrayPush".to_string(),
33                    "ArrayPop".to_string(),
34                    "ArraySort".to_string(),
35                    "ArrayFilter".to_string(),
36                    "ArrayMap".to_string(),
37                    "ArrayFind".to_string(),
38                    "ObjectKeys".to_string(),
39                    "ObjectValues".to_string(),
40                    "ObjectMerge".to_string(),
41                ],
42                functions: vec![
43                    "length".to_string(),
44                    "contains".to_string(),
45                    "first".to_string(),
46                    "last".to_string(),
47                    "reverse".to_string(),
48                    "join".to_string(),
49                    "slice".to_string(),
50                    "keys".to_string(),
51                    "values".to_string(),
52                ],
53                dependencies: vec![],
54            },
55        }
56    }
57}
58
59impl RulePlugin for CollectionUtilsPlugin {
60    fn get_metadata(&self) -> &PluginMetadata {
61        &self.metadata
62    }
63
64    fn register_actions(&self, engine: &mut RustRuleEngine) -> Result<()> {
65        // ArrayLength - Get array length
66        engine.register_action_handler("ArrayLength", |params, facts| {
67            let input = get_string_param(params, "input", "0")?;
68            let output = get_string_param(params, "output", "1")?;
69
70            if let Some(value) = facts.get(&input) {
71                let length = match value {
72                    Value::Array(arr) => arr.len(),
73                    Value::String(s) => s.len(),
74                    Value::Object(obj) => obj.len(),
75                    _ => 0,
76                };
77                facts.set_nested(&output, Value::Integer(length as i64))?;
78            }
79            Ok(())
80        });
81
82        // ArrayPush - Add element to array
83        engine.register_action_handler("ArrayPush", |params, facts| {
84            let array_path = get_string_param(params, "array", "0")?;
85            let element = get_value_param(params, facts, "element", "1")?;
86
87            if let Some(value) = facts.get(&array_path) {
88                if let Value::Array(mut arr) = value.clone() {
89                    arr.push(element);
90                    facts.set_nested(&array_path, Value::Array(arr))?;
91                } else {
92                    return Err(RuleEngineError::ActionError {
93                        message: "Target must be an array".to_string(),
94                    });
95                }
96            } else {
97                // Create new array with the element
98                facts.set_nested(&array_path, Value::Array(vec![element]))?;
99            }
100            Ok(())
101        });
102
103        // ArrayPop - Remove and return last element
104        engine.register_action_handler("ArrayPop", |params, facts| {
105            let array_path = get_string_param(params, "array", "0")?;
106            let output = get_string_param(params, "output", "1")?;
107
108            if let Some(value) = facts.get(&array_path) {
109                if let Value::Array(mut arr) = value.clone() {
110                    if let Some(popped) = arr.pop() {
111                        facts.set_nested(&array_path, Value::Array(arr))?;
112                        facts.set_nested(&output, popped)?;
113                    } else {
114                        facts.set_nested(&output, Value::Null)?;
115                    }
116                }
117            }
118            Ok(())
119        });
120
121        // ArraySort - Sort array
122        engine.register_action_handler("ArraySort", |params, facts| {
123            let array_path = get_string_param(params, "array", "0")?;
124            let ascending = get_optional_bool_param(params, "ascending").unwrap_or(true);
125
126            if let Some(value) = facts.get(&array_path) {
127                if let Value::Array(mut arr) = value.clone() {
128                    arr.sort_by(|a, b| {
129                        let order = compare_values(a, b);
130                        if ascending {
131                            order
132                        } else {
133                            order.reverse()
134                        }
135                    });
136                    facts.set_nested(&array_path, Value::Array(arr))?;
137                }
138            }
139            Ok(())
140        });
141
142        // ArrayFilter - Filter array elements
143        engine.register_action_handler("ArrayFilter", |params, facts| {
144            let input = get_string_param(params, "input", "0")?;
145            let predicate_field = get_string_param(params, "field", "1")?;
146            let predicate_value = get_value_param(params, facts, "value", "2")?;
147            let output = get_string_param(params, "output", "3")?;
148
149            if let Some(value) = facts.get(&input) {
150                if let Value::Array(arr) = value {
151                    let filtered: Vec<Value> = arr
152                        .iter()
153                        .filter(|item| filter_predicate(item, &predicate_field, &predicate_value))
154                        .cloned()
155                        .collect();
156                    facts.set_nested(&output, Value::Array(filtered))?;
157                }
158            }
159            Ok(())
160        });
161
162        // ArrayFind - Find first matching element
163        engine.register_action_handler("ArrayFind", |params, facts| {
164            let input = get_string_param(params, "input", "0")?;
165            let predicate_field = get_string_param(params, "field", "1")?;
166            let predicate_value = get_value_param(params, facts, "value", "2")?;
167            let output = get_string_param(params, "output", "3")?;
168
169            if let Some(value) = facts.get(&input) {
170                if let Value::Array(arr) = value {
171                    let found = arr
172                        .iter()
173                        .find(|item| filter_predicate(item, &predicate_field, &predicate_value))
174                        .cloned()
175                        .unwrap_or(Value::Null);
176                    facts.set_nested(&output, found)?;
177                }
178            }
179            Ok(())
180        });
181
182        // ObjectKeys - Get object keys
183        engine.register_action_handler("ObjectKeys", |params, facts| {
184            let input = get_string_param(params, "input", "0")?;
185            let output = get_string_param(params, "output", "1")?;
186
187            if let Some(value) = facts.get(&input) {
188                if let Value::Object(obj) = value {
189                    let keys: Vec<Value> = obj.keys().map(|k| Value::String(k.clone())).collect();
190                    facts.set_nested(&output, Value::Array(keys))?;
191                }
192            }
193            Ok(())
194        });
195
196        // ObjectValues - Get object values
197        engine.register_action_handler("ObjectValues", |params, facts| {
198            let input = get_string_param(params, "input", "0")?;
199            let output = get_string_param(params, "output", "1")?;
200
201            if let Some(value) = facts.get(&input) {
202                if let Value::Object(obj) = value {
203                    let values: Vec<Value> = obj.values().cloned().collect();
204                    facts.set_nested(&output, Value::Array(values))?;
205                }
206            }
207            Ok(())
208        });
209
210        // ObjectMerge - Merge two objects
211        engine.register_action_handler("ObjectMerge", |params, facts| {
212            let source1 = get_string_param(params, "source1", "0")?;
213            let source2 = get_string_param(params, "source2", "1")?;
214            let output = get_string_param(params, "output", "2")?;
215
216            let obj1 = facts
217                .get(&source1)
218                .and_then(|v| {
219                    if let Value::Object(obj) = v {
220                        Some(obj.clone())
221                    } else {
222                        None
223                    }
224                })
225                .unwrap_or_default();
226
227            let obj2 = facts
228                .get(&source2)
229                .and_then(|v| {
230                    if let Value::Object(obj) = v {
231                        Some(obj.clone())
232                    } else {
233                        None
234                    }
235                })
236                .unwrap_or_default();
237
238            let mut merged = obj1;
239            for (key, value) in obj2 {
240                merged.insert(key, value);
241            }
242
243            facts.set_nested(&output, Value::Object(merged))?;
244            Ok(())
245        });
246
247        Ok(())
248    }
249
250    fn register_functions(&self, engine: &mut RustRuleEngine) -> Result<()> {
251        // length - Get collection length
252        engine.register_function("length", |args, _facts| {
253            if args.len() != 1 {
254                return Err(RuleEngineError::EvaluationError {
255                    message: "length requires exactly 1 argument".to_string(),
256                });
257            }
258
259            let length = match &args[0] {
260                Value::Array(arr) => arr.len(),
261                Value::String(s) => s.len(),
262                Value::Object(obj) => obj.len(),
263                _ => 0,
264            };
265            Ok(Value::Integer(length as i64))
266        });
267
268        // contains - Check if collection contains value
269        engine.register_function("contains", |args, _facts| {
270            if args.len() != 2 {
271                return Err(RuleEngineError::EvaluationError {
272                    message: "contains requires exactly 2 arguments: collection, value".to_string(),
273                });
274            }
275
276            let contains = match (&args[0], &args[1]) {
277                (Value::Array(arr), value) => arr.contains(value),
278                (Value::String(s), Value::String(search)) => s.contains(search),
279                (Value::Object(obj), Value::String(key)) => obj.contains_key(key),
280                _ => false,
281            };
282            Ok(Value::Boolean(contains))
283        });
284
285        // first - Get first element
286        engine.register_function("first", |args, _facts| {
287            if args.len() != 1 {
288                return Err(RuleEngineError::EvaluationError {
289                    message: "first requires exactly 1 argument".to_string(),
290                });
291            }
292
293            let first = match &args[0] {
294                Value::Array(arr) => arr.first().cloned().unwrap_or(Value::Null),
295                Value::String(s) => {
296                    if s.is_empty() {
297                        Value::Null
298                    } else {
299                        Value::String(s.chars().next().unwrap().to_string())
300                    }
301                }
302                _ => Value::Null,
303            };
304            Ok(first)
305        });
306
307        // last - Get last element
308        engine.register_function("last", |args, _facts| {
309            if args.len() != 1 {
310                return Err(RuleEngineError::EvaluationError {
311                    message: "last requires exactly 1 argument".to_string(),
312                });
313            }
314
315            let last = match &args[0] {
316                Value::Array(arr) => arr.last().cloned().unwrap_or(Value::Null),
317                Value::String(s) => {
318                    if s.is_empty() {
319                        Value::Null
320                    } else {
321                        Value::String(s.chars().last().unwrap().to_string())
322                    }
323                }
324                _ => Value::Null,
325            };
326            Ok(last)
327        });
328
329        // reverse - Reverse array or string
330        engine.register_function("reverse", |args, _facts| {
331            if args.len() != 1 {
332                return Err(RuleEngineError::EvaluationError {
333                    message: "reverse requires exactly 1 argument".to_string(),
334                });
335            }
336
337            let reversed = match &args[0] {
338                Value::Array(arr) => {
339                    let mut rev = arr.clone();
340                    rev.reverse();
341                    Value::Array(rev)
342                }
343                Value::String(s) => Value::String(s.chars().rev().collect()),
344                _ => args[0].clone(),
345            };
346            Ok(reversed)
347        });
348
349        // join - Join array elements
350        engine.register_function("join", |args, _facts| {
351            if args.len() != 2 {
352                return Err(RuleEngineError::EvaluationError {
353                    message: "join requires exactly 2 arguments: array, separator".to_string(),
354                });
355            }
356
357            match (&args[0], &args[1]) {
358                (Value::Array(arr), Value::String(sep)) => {
359                    let strings: Vec<String> = arr
360                        .iter()
361                        .map(|v| value_to_string(v).unwrap_or_default())
362                        .collect();
363                    Ok(Value::String(strings.join(sep)))
364                }
365                _ => Err(RuleEngineError::EvaluationError {
366                    message: "join requires array and string separator".to_string(),
367                }),
368            }
369        });
370
371        // slice - Get slice of array
372        engine.register_function("slice", |args, _facts| {
373            if args.len() < 2 || args.len() > 3 {
374                return Err(RuleEngineError::EvaluationError {
375                    message: "slice requires 2-3 arguments: array, start, [end]".to_string(),
376                });
377            }
378
379            match &args[0] {
380                Value::Array(arr) => {
381                    let start = value_to_number(&args[1])? as usize;
382                    let end = if args.len() == 3 {
383                        value_to_number(&args[2])? as usize
384                    } else {
385                        arr.len()
386                    };
387
388                    let start = start.min(arr.len());
389                    let end = end.min(arr.len());
390
391                    if start <= end {
392                        Ok(Value::Array(arr[start..end].to_vec()))
393                    } else {
394                        Ok(Value::Array(vec![]))
395                    }
396                }
397                _ => Err(RuleEngineError::EvaluationError {
398                    message: "slice requires array as first argument".to_string(),
399                }),
400            }
401        });
402
403        // keys - Get object keys
404        engine.register_function("keys", |args, _facts| {
405            if args.len() != 1 {
406                return Err(RuleEngineError::EvaluationError {
407                    message: "keys requires exactly 1 argument".to_string(),
408                });
409            }
410
411            match &args[0] {
412                Value::Object(obj) => {
413                    let keys: Vec<Value> = obj.keys().map(|k| Value::String(k.clone())).collect();
414                    Ok(Value::Array(keys))
415                }
416                _ => Ok(Value::Array(vec![])),
417            }
418        });
419
420        // values - Get object values
421        engine.register_function("values", |args, _facts| {
422            if args.len() != 1 {
423                return Err(RuleEngineError::EvaluationError {
424                    message: "values requires exactly 1 argument".to_string(),
425                });
426            }
427
428            match &args[0] {
429                Value::Object(obj) => Ok(Value::Array(obj.values().cloned().collect())),
430                _ => Ok(Value::Array(vec![])),
431            }
432        });
433
434        Ok(())
435    }
436
437    fn unload(&mut self) -> Result<()> {
438        self.metadata.state = PluginState::Unloaded;
439        Ok(())
440    }
441
442    fn health_check(&mut self) -> PluginHealth {
443        match self.metadata.state {
444            PluginState::Loaded => PluginHealth::Healthy,
445            PluginState::Loading => PluginHealth::Warning("Plugin is loading".to_string()),
446            PluginState::Error => PluginHealth::Error("Plugin is in error state".to_string()),
447            PluginState::Unloaded => PluginHealth::Warning("Plugin is unloaded".to_string()),
448        }
449    }
450}
451
452// Helper functions
453fn get_string_param(params: &HashMap<String, Value>, name: &str, pos: &str) -> Result<String> {
454    let value = params
455        .get(name)
456        .or_else(|| params.get(pos))
457        .ok_or_else(|| RuleEngineError::ActionError {
458            message: format!("Missing parameter: {}", name),
459        })?;
460
461    match value {
462        Value::String(s) => Ok(s.clone()),
463        _ => Err(RuleEngineError::ActionError {
464            message: format!("Parameter {} must be string", name),
465        }),
466    }
467}
468
469fn get_value_param(
470    params: &HashMap<String, Value>,
471    facts: &crate::Facts,
472    name: &str,
473    pos: &str,
474) -> Result<Value> {
475    let value = params
476        .get(name)
477        .or_else(|| params.get(pos))
478        .ok_or_else(|| RuleEngineError::ActionError {
479            message: format!("Missing parameter: {}", name),
480        })?;
481
482    if let Value::String(s) = value {
483        if s.contains('.') {
484            if let Some(fact_value) = facts.get(s) {
485                return Ok(fact_value.clone());
486            }
487        }
488    }
489
490    Ok(value.clone())
491}
492
493fn get_optional_bool_param(params: &HashMap<String, Value>, name: &str) -> Option<bool> {
494    params.get(name).and_then(|v| match v {
495        Value::Boolean(b) => Some(*b),
496        Value::String(s) => s.parse().ok(),
497        _ => None,
498    })
499}
500
501fn value_to_string(value: &Value) -> Result<String> {
502    match value {
503        Value::String(s) => Ok(s.clone()),
504        Value::Integer(i) => Ok(i.to_string()),
505        Value::Number(f) => Ok(f.to_string()),
506        Value::Boolean(b) => Ok(b.to_string()),
507        _ => Err(RuleEngineError::ActionError {
508            message: "Value cannot be converted to string".to_string(),
509        }),
510    }
511}
512
513fn value_to_number(value: &Value) -> Result<f64> {
514    match value {
515        Value::Number(f) => Ok(*f),
516        Value::Integer(i) => Ok(*i as f64),
517        Value::String(s) => s.parse::<f64>().map_err(|_| RuleEngineError::ActionError {
518            message: format!("Cannot convert '{}' to number", s),
519        }),
520        _ => Err(RuleEngineError::ActionError {
521            message: "Value cannot be converted to number".to_string(),
522        }),
523    }
524}
525
526fn compare_values(a: &Value, b: &Value) -> std::cmp::Ordering {
527    use std::cmp::Ordering;
528
529    match (a, b) {
530        (Value::Integer(a), Value::Integer(b)) => a.cmp(b),
531        (Value::Number(a), Value::Number(b)) => a.partial_cmp(b).unwrap_or(Ordering::Equal),
532        (Value::String(a), Value::String(b)) => a.cmp(b),
533        (Value::Boolean(a), Value::Boolean(b)) => a.cmp(b),
534        _ => Ordering::Equal,
535    }
536}
537
538fn filter_predicate(item: &Value, field: &str, expected: &Value) -> bool {
539    if field == "_value" {
540        return item == expected;
541    }
542
543    if let Value::Object(obj) = item {
544        if let Some(field_value) = obj.get(field) {
545            return field_value == expected;
546        }
547    }
548
549    false
550}