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