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