rust_rule_engine/plugins/
validation.rs1use 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
8static 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
18pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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
279fn 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 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
356 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}