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