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