rust_rule_engine/plugins/
date_utils.rs1use crate::engine::plugin::{PluginHealth, PluginMetadata, PluginState, RulePlugin};
2use crate::engine::RustRuleEngine;
3use crate::errors::{Result, RuleEngineError};
4use crate::types::Value;
5use chrono::{DateTime, Datelike, Duration, Local, NaiveDateTime, TimeZone, Utc};
6
7pub struct DateUtilsPlugin {
9 metadata: PluginMetadata,
10}
11
12impl Default for DateUtilsPlugin {
13 fn default() -> Self {
14 Self::new()
15 }
16}
17
18impl DateUtilsPlugin {
19 pub fn new() -> Self {
20 Self {
21 metadata: PluginMetadata {
22 name: "date-utils".to_string(),
23 version: "1.0.0".to_string(),
24 description: "Date and time manipulation utilities".to_string(),
25 author: "Rust Rule Engine Team".to_string(),
26 state: PluginState::Loaded,
27 health: PluginHealth::Healthy,
28 actions: vec![
29 "CurrentDate".to_string(),
30 "CurrentTime".to_string(),
31 "FormatDate".to_string(),
32 "ParseDate".to_string(),
33 "AddDays".to_string(),
34 "AddHours".to_string(),
35 "DateDiff".to_string(),
36 "IsWeekend".to_string(),
37 ],
38 functions: vec![
39 "now".to_string(),
40 "today".to_string(),
41 "dayOfWeek".to_string(),
42 "dayOfYear".to_string(),
43 "year".to_string(),
44 "month".to_string(),
45 "day".to_string(),
46 ],
47 dependencies: vec![],
48 },
49 }
50 }
51}
52
53impl RulePlugin for DateUtilsPlugin {
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("CurrentDate", |params, facts| {
61 let output = get_string_param(params, "output", "0")?;
62 let now = Local::now();
63 let date_str = now.format("%Y-%m-%d").to_string();
64 facts.set_nested(&output, Value::String(date_str))?;
65 Ok(())
66 });
67
68 engine.register_action_handler("CurrentTime", |params, facts| {
70 let output = get_string_param(params, "output", "0")?;
71 let now = Local::now();
72 let time_str = now.format("%H:%M:%S").to_string();
73 facts.set_nested(&output, Value::String(time_str))?;
74 Ok(())
75 });
76
77 engine.register_action_handler("FormatDate", |params, facts| {
79 let input = get_string_param(params, "input", "0")?;
80 let format = get_string_param(params, "format", "1")?;
81 let output = get_string_param(params, "output", "2")?;
82
83 if let Some(value) = facts.get(&input) {
84 let date_str = value_to_string(&value)?;
85
86 let dt = parse_date_string(&date_str)?;
88 let formatted = dt.format(&format).to_string();
89 facts.set_nested(&output, Value::String(formatted))?;
90 }
91 Ok(())
92 });
93
94 engine.register_action_handler("AddDays", |params, facts| {
96 let input = get_string_param(params, "input", "0")?;
97 let days = get_number_param(params, facts, "days", "1")?;
98 let output = get_string_param(params, "output", "2")?;
99
100 if let Some(value) = facts.get(&input) {
101 let date_str = value_to_string(&value)?;
102 let dt = parse_date_string(&date_str)?;
103 let new_dt = dt + Duration::days(days as i64);
104 let result = new_dt.format("%Y-%m-%d").to_string();
105 facts.set_nested(&output, Value::String(result))?;
106 }
107 Ok(())
108 });
109
110 engine.register_action_handler("IsWeekend", |params, facts| {
112 let input = get_string_param(params, "input", "0")?;
113 let output = get_string_param(params, "output", "1")?;
114
115 if let Some(value) = facts.get(&input) {
116 let date_str = value_to_string(&value)?;
117 let dt = parse_date_string(&date_str)?;
118 let weekday = dt.weekday();
119 let is_weekend = weekday == chrono::Weekday::Sat || weekday == chrono::Weekday::Sun;
120 facts.set_nested(&output, Value::Boolean(is_weekend))?;
121 }
122 Ok(())
123 });
124
125 Ok(())
126 }
127
128 fn register_functions(&self, engine: &mut RustRuleEngine) -> Result<()> {
129 engine.register_function("now", |_args, _facts| {
131 let now = Utc::now();
132 Ok(Value::String(now.to_rfc3339()))
133 });
134
135 engine.register_function("today", |_args, _facts| {
137 let today = Local::now();
138 Ok(Value::String(today.format("%Y-%m-%d").to_string()))
139 });
140
141 engine.register_function("dayOfWeek", |args, _facts| {
143 if args.len() != 1 {
144 return Err(RuleEngineError::EvaluationError {
145 message: "dayOfWeek requires exactly 1 argument".to_string(),
146 });
147 }
148
149 let date_str = value_to_string(&args[0])?;
150 let dt = parse_date_string(&date_str)?;
151 let day_num = dt.weekday().number_from_monday();
152 Ok(Value::Integer(day_num as i64))
153 });
154
155 engine.register_function("year", |args, _facts| {
157 if args.len() != 1 {
158 return Err(RuleEngineError::EvaluationError {
159 message: "year requires exactly 1 argument".to_string(),
160 });
161 }
162
163 let date_str = value_to_string(&args[0])?;
164 let dt = parse_date_string(&date_str)?;
165 Ok(Value::Integer(dt.year() as i64))
166 });
167
168 engine.register_function("month", |args, _facts| {
170 if args.len() != 1 {
171 return Err(RuleEngineError::EvaluationError {
172 message: "month requires exactly 1 argument".to_string(),
173 });
174 }
175
176 let date_str = value_to_string(&args[0])?;
177 let dt = parse_date_string(&date_str)?;
178 Ok(Value::Integer(dt.month() as i64))
179 });
180
181 engine.register_function("day", |args, _facts| {
183 if args.len() != 1 {
184 return Err(RuleEngineError::EvaluationError {
185 message: "day requires exactly 1 argument".to_string(),
186 });
187 }
188
189 let date_str = value_to_string(&args[0])?;
190 let dt = parse_date_string(&date_str)?;
191 Ok(Value::Integer(dt.day() as i64))
192 });
193
194 Ok(())
195 }
196
197 fn unload(&mut self) -> Result<()> {
198 self.metadata.state = PluginState::Unloaded;
199 Ok(())
200 }
201
202 fn health_check(&mut self) -> PluginHealth {
203 match self.metadata.state {
204 PluginState::Loaded => PluginHealth::Healthy,
205 PluginState::Loading => PluginHealth::Warning("Plugin is loading".to_string()),
206 PluginState::Error => PluginHealth::Error("Plugin is in error state".to_string()),
207 PluginState::Unloaded => PluginHealth::Warning("Plugin is unloaded".to_string()),
208 }
209 }
210}
211
212fn get_string_param(
214 params: &std::collections::HashMap<String, Value>,
215 name: &str,
216 pos: &str,
217) -> Result<String> {
218 let value = params
219 .get(name)
220 .or_else(|| params.get(pos))
221 .ok_or_else(|| RuleEngineError::ActionError {
222 message: format!("Missing parameter: {}", name),
223 })?;
224
225 match value {
226 Value::String(s) => Ok(s.clone()),
227 _ => Err(RuleEngineError::ActionError {
228 message: format!("Parameter {} must be string", name),
229 }),
230 }
231}
232
233fn get_number_param(
234 params: &std::collections::HashMap<String, Value>,
235 facts: &crate::Facts,
236 name: &str,
237 pos: &str,
238) -> Result<f64> {
239 let value = params
240 .get(name)
241 .or_else(|| params.get(pos))
242 .ok_or_else(|| RuleEngineError::ActionError {
243 message: format!("Missing parameter: {}", name),
244 })?;
245
246 if let Value::String(s) = value {
247 if s.contains('.') {
248 if let Some(fact_value) = facts.get(s) {
249 return value_to_number(&fact_value);
250 }
251 }
252 }
253
254 value_to_number(value)
255}
256
257fn value_to_string(value: &Value) -> Result<String> {
258 match value {
259 Value::String(s) => Ok(s.clone()),
260 Value::Integer(i) => Ok(i.to_string()),
261 Value::Number(f) => Ok(f.to_string()),
262 Value::Boolean(b) => Ok(b.to_string()),
263 _ => Err(RuleEngineError::ActionError {
264 message: "Value cannot be converted to string".to_string(),
265 }),
266 }
267}
268
269fn value_to_number(value: &Value) -> Result<f64> {
270 match value {
271 Value::Number(f) => Ok(*f),
272 Value::Integer(i) => Ok(*i as f64),
273 Value::String(s) => s.parse::<f64>().map_err(|_| RuleEngineError::ActionError {
274 message: format!("Cannot convert '{}' to number", s),
275 }),
276 _ => Err(RuleEngineError::ActionError {
277 message: "Value cannot be converted to number".to_string(),
278 }),
279 }
280}
281
282fn parse_date_string(date_str: &str) -> Result<DateTime<Local>> {
283 let formats = vec![
285 "%Y-%m-%d",
286 "%Y-%m-%d %H:%M:%S",
287 "%Y/%m/%d",
288 "%d/%m/%Y",
289 "%m/%d/%Y",
290 ];
291
292 for format in formats {
293 if let Ok(naive_dt) = NaiveDateTime::parse_from_str(date_str, format) {
294 return Local
295 .from_local_datetime(&naive_dt)
296 .single()
297 .ok_or_else(|| RuleEngineError::ActionError {
298 message: "Invalid datetime".to_string(),
299 });
300 }
301
302 if let Ok(naive_date) = chrono::NaiveDate::parse_from_str(date_str, format) {
303 let naive_dt =
304 naive_date
305 .and_hms_opt(0, 0, 0)
306 .ok_or_else(|| RuleEngineError::ActionError {
307 message: "Invalid date".to_string(),
308 })?;
309 return Local
310 .from_local_datetime(&naive_dt)
311 .single()
312 .ok_or_else(|| RuleEngineError::ActionError {
313 message: "Invalid datetime".to_string(),
314 });
315 }
316 }
317
318 Err(RuleEngineError::ActionError {
319 message: format!("Cannot parse date: {}", date_str),
320 })
321}