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 once_cell::sync::Lazy;
6use regex::Regex;
7
8static 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
14pub 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 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 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 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 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 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 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 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 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 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 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 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 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 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
275fn 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 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
352 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}