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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
269fn 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 let digits: String = phone.chars().filter(|c| c.is_ascii_digit()).collect();
346 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}