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
9pub 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 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 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 facts.set_nested(&array_path, Value::Array(vec![element]))?;
99 }
100 Ok(())
101 });
102
103 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
452fn 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}