rust_rule_engine/plugins/
validation.rs

1use crate::engine::plugin::{PluginHealth, PluginMetadata, PluginState, RulePlugin};
2use crate::engine::RustRuleEngine;
3use crate::errors::{Result, RuleEngineError};
4use crate::parser::literal_search::{
5    is_valid_email_literal, is_valid_phone_literal, is_valid_url_literal,
6};
7use crate::types::Value;
8
9/// Built-in plugin for data validation operations
10pub struct ValidationPlugin {
11    metadata: PluginMetadata,
12}
13
14impl Default for ValidationPlugin {
15    fn default() -> Self {
16        Self::new()
17    }
18}
19
20impl ValidationPlugin {
21    pub fn new() -> Self {
22        Self {
23            metadata: PluginMetadata {
24                name: "validation".to_string(),
25                version: "1.0.0".to_string(),
26                description: "Data validation utilities".to_string(),
27                author: "Rust Rule Engine Team".to_string(),
28                state: PluginState::Loaded,
29                health: PluginHealth::Healthy,
30                actions: vec![
31                    "ValidateEmail".to_string(),
32                    "ValidatePhone".to_string(),
33                    "ValidateUrl".to_string(),
34                    "ValidateRegex".to_string(),
35                    "ValidateRange".to_string(),
36                    "ValidateLength".to_string(),
37                    "ValidateNotEmpty".to_string(),
38                    "ValidateNumeric".to_string(),
39                ],
40                functions: vec![
41                    "isEmail".to_string(),
42                    "isPhone".to_string(),
43                    "isUrl".to_string(),
44                    "isNumeric".to_string(),
45                    "isEmpty".to_string(),
46                    "inRange".to_string(),
47                ],
48                dependencies: vec![],
49            },
50        }
51    }
52}
53
54impl RulePlugin for ValidationPlugin {
55    fn get_metadata(&self) -> &PluginMetadata {
56        &self.metadata
57    }
58
59    fn register_actions(&self, engine: &mut RustRuleEngine) -> Result<()> {
60        // ValidateEmail - Validate email format
61        engine.register_action_handler("ValidateEmail", |params, facts| {
62            let input = get_string_param(params, "input", "0")?;
63            let output = get_string_param(params, "output", "1")?;
64
65            if let Some(value) = facts.get(&input) {
66                let email = value_to_string(&value)?;
67                let is_valid = is_valid_email(&email);
68                facts.set_nested(&output, Value::Boolean(is_valid))?;
69            }
70            Ok(())
71        });
72
73        // ValidatePhone - Validate phone number format
74        engine.register_action_handler("ValidatePhone", |params, facts| {
75            let input = get_string_param(params, "input", "0")?;
76            let output = get_string_param(params, "output", "1")?;
77
78            if let Some(value) = facts.get(&input) {
79                let phone = value_to_string(&value)?;
80                let is_valid = is_valid_phone(&phone);
81                facts.set_nested(&output, Value::Boolean(is_valid))?;
82            }
83            Ok(())
84        });
85
86        // ValidateUrl - Validate URL format
87        engine.register_action_handler("ValidateUrl", |params, facts| {
88            let input = get_string_param(params, "input", "0")?;
89            let output = get_string_param(params, "output", "1")?;
90
91            if let Some(value) = facts.get(&input) {
92                let url = value_to_string(&value)?;
93                let is_valid = is_valid_url(&url);
94                facts.set_nested(&output, Value::Boolean(is_valid))?;
95            }
96            Ok(())
97        });
98
99        // ValidateRegex - Validate against regex pattern
100        // Note: Now uses literal pattern matching with Aho-Corasick for better performance
101        engine.register_action_handler("ValidateRegex", |params, facts| {
102            let input = get_string_param(params, "input", "0")?;
103            let pattern = get_string_param(params, "pattern", "1")?;
104            let output = get_string_param(params, "output", "2")?;
105
106            if let Some(value) = facts.get(&input) {
107                let text = value_to_string(&value)?;
108                // For now, use simple contains check
109                // For more complex patterns, consider using Aho-Corasick multi-pattern search
110                let is_valid = text.contains(&pattern);
111                facts.set_nested(&output, Value::Boolean(is_valid))?;
112            }
113            Ok(())
114        });
115
116        // ValidateRange - Validate number is in range
117        engine.register_action_handler("ValidateRange", |params, facts| {
118            let input = get_string_param(params, "input", "0")?;
119            let min = get_number_param(params, facts, "min", "1")?;
120            let max = get_number_param(params, facts, "max", "2")?;
121            let output = get_string_param(params, "output", "3")?;
122
123            if let Some(value) = facts.get(&input) {
124                let num = value_to_number(&value)?;
125                let is_valid = num >= min && num <= max;
126                facts.set_nested(&output, Value::Boolean(is_valid))?;
127            }
128            Ok(())
129        });
130
131        // ValidateLength - Validate string length
132        engine.register_action_handler("ValidateLength", |params, facts| {
133            let input = get_string_param(params, "input", "0")?;
134            let min_len = get_number_param(params, facts, "minLength", "1")? as usize;
135            let max_len = get_number_param(params, facts, "maxLength", "2")? as usize;
136            let output = get_string_param(params, "output", "3")?;
137
138            if let Some(value) = facts.get(&input) {
139                let text = value_to_string(&value)?;
140                let len = text.len();
141                let is_valid = len >= min_len && len <= max_len;
142                facts.set_nested(&output, Value::Boolean(is_valid))?;
143            }
144            Ok(())
145        });
146
147        // ValidateNotEmpty - Check if value is not empty
148        engine.register_action_handler("ValidateNotEmpty", |params, facts| {
149            let input = get_string_param(params, "input", "0")?;
150            let output = get_string_param(params, "output", "1")?;
151
152            if let Some(value) = facts.get(&input) {
153                let is_not_empty = match value {
154                    Value::String(s) => !s.trim().is_empty(),
155                    Value::Array(arr) => !arr.is_empty(),
156                    Value::Object(obj) => !obj.is_empty(),
157                    Value::Null => false,
158                    _ => true,
159                };
160                facts.set_nested(&output, Value::Boolean(is_not_empty))?;
161            }
162            Ok(())
163        });
164
165        Ok(())
166    }
167
168    fn register_functions(&self, engine: &mut RustRuleEngine) -> Result<()> {
169        // isEmail - Check if string is valid email
170        engine.register_function("isEmail", |args, _facts| {
171            if args.len() != 1 {
172                return Err(RuleEngineError::EvaluationError {
173                    message: "isEmail requires exactly 1 argument".to_string(),
174                });
175            }
176
177            let email = value_to_string(&args[0])?;
178            Ok(Value::Boolean(is_valid_email(&email)))
179        });
180
181        // isPhone - Check if string is valid phone
182        engine.register_function("isPhone", |args, _facts| {
183            if args.len() != 1 {
184                return Err(RuleEngineError::EvaluationError {
185                    message: "isPhone requires exactly 1 argument".to_string(),
186                });
187            }
188
189            let phone = value_to_string(&args[0])?;
190            Ok(Value::Boolean(is_valid_phone(&phone)))
191        });
192
193        // isUrl - Check if string is valid URL
194        engine.register_function("isUrl", |args, _facts| {
195            if args.len() != 1 {
196                return Err(RuleEngineError::EvaluationError {
197                    message: "isUrl requires exactly 1 argument".to_string(),
198                });
199            }
200
201            let url = value_to_string(&args[0])?;
202            Ok(Value::Boolean(is_valid_url(&url)))
203        });
204
205        // isNumeric - Check if string is numeric
206        engine.register_function("isNumeric", |args, _facts| {
207            if args.len() != 1 {
208                return Err(RuleEngineError::EvaluationError {
209                    message: "isNumeric requires exactly 1 argument".to_string(),
210                });
211            }
212
213            let text = value_to_string(&args[0])?;
214            let is_numeric = text.parse::<f64>().is_ok();
215            Ok(Value::Boolean(is_numeric))
216        });
217
218        // isEmpty - Check if value is empty
219        engine.register_function("isEmpty", |args, _facts| {
220            if args.len() != 1 {
221                return Err(RuleEngineError::EvaluationError {
222                    message: "isEmpty requires exactly 1 argument".to_string(),
223                });
224            }
225
226            let is_empty = match &args[0] {
227                Value::String(s) => s.trim().is_empty(),
228                Value::Array(arr) => arr.is_empty(),
229                Value::Object(obj) => obj.is_empty(),
230                Value::Null => true,
231                _ => false,
232            };
233            Ok(Value::Boolean(is_empty))
234        });
235
236        // inRange - Check if number is in range
237        engine.register_function("inRange", |args, _facts| {
238            if args.len() != 3 {
239                return Err(RuleEngineError::EvaluationError {
240                    message: "inRange requires exactly 3 arguments: value, min, max".to_string(),
241                });
242            }
243
244            let value = value_to_number(&args[0])?;
245            let min = value_to_number(&args[1])?;
246            let max = value_to_number(&args[2])?;
247
248            let in_range = value >= min && value <= max;
249            Ok(Value::Boolean(in_range))
250        });
251
252        Ok(())
253    }
254
255    fn unload(&mut self) -> Result<()> {
256        self.metadata.state = PluginState::Unloaded;
257        Ok(())
258    }
259
260    fn health_check(&mut self) -> PluginHealth {
261        match self.metadata.state {
262            PluginState::Loaded => PluginHealth::Healthy,
263            PluginState::Loading => PluginHealth::Warning("Plugin is loading".to_string()),
264            PluginState::Error => PluginHealth::Error("Plugin is in error state".to_string()),
265            PluginState::Unloaded => PluginHealth::Warning("Plugin is unloaded".to_string()),
266        }
267    }
268}
269
270// Helper functions
271fn get_string_param(
272    params: &std::collections::HashMap<String, Value>,
273    name: &str,
274    pos: &str,
275) -> Result<String> {
276    let value = params
277        .get(name)
278        .or_else(|| params.get(pos))
279        .ok_or_else(|| RuleEngineError::ActionError {
280            message: format!("Missing parameter: {}", name),
281        })?;
282
283    match value {
284        Value::String(s) => Ok(s.clone()),
285        _ => Err(RuleEngineError::ActionError {
286            message: format!("Parameter {} must be string", name),
287        }),
288    }
289}
290
291fn get_number_param(
292    params: &std::collections::HashMap<String, Value>,
293    facts: &crate::Facts,
294    name: &str,
295    pos: &str,
296) -> Result<f64> {
297    let value = params
298        .get(name)
299        .or_else(|| params.get(pos))
300        .ok_or_else(|| RuleEngineError::ActionError {
301            message: format!("Missing parameter: {}", name),
302        })?;
303
304    if let Value::String(s) = value {
305        if s.contains('.') {
306            if let Some(fact_value) = facts.get(s) {
307                return value_to_number(&fact_value);
308            }
309        }
310    }
311
312    value_to_number(value)
313}
314
315fn value_to_string(value: &Value) -> Result<String> {
316    match value {
317        Value::String(s) => Ok(s.clone()),
318        Value::Integer(i) => Ok(i.to_string()),
319        Value::Number(f) => Ok(f.to_string()),
320        Value::Boolean(b) => Ok(b.to_string()),
321        _ => Err(RuleEngineError::ActionError {
322            message: "Value cannot be converted to string".to_string(),
323        }),
324    }
325}
326
327fn value_to_number(value: &Value) -> Result<f64> {
328    match value {
329        Value::Number(f) => Ok(*f),
330        Value::Integer(i) => Ok(*i as f64),
331        Value::String(s) => s.parse::<f64>().map_err(|_| RuleEngineError::ActionError {
332            message: format!("Cannot convert '{}' to number", s),
333        }),
334        _ => Err(RuleEngineError::ActionError {
335            message: "Value cannot be converted to number".to_string(),
336        }),
337    }
338}
339
340fn is_valid_email(email: &str) -> bool {
341    is_valid_email_literal(email)
342}
343
344fn is_valid_phone(phone: &str) -> bool {
345    is_valid_phone_literal(phone)
346}
347
348fn is_valid_url(url: &str) -> bool {
349    is_valid_url_literal(url)
350}