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