rust_rule_engine/plugins/
date_utils.rs

1use 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
7/// Built-in plugin for date and time operations
8pub 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        // CurrentDate - Get current date
60        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        // CurrentTime - Get current time
69        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        // FormatDate - Format date string
78        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                // Try to parse the date
87                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        // AddDays - Add days to date
95        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        // IsWeekend - Check if date is weekend
111        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        // now - Get current timestamp
130        engine.register_function("now", |_args, _facts| {
131            let now = Utc::now();
132            Ok(Value::String(now.to_rfc3339()))
133        });
134
135        // today - Get today's date
136        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        // dayOfWeek - Get day of week (1=Monday, 7=Sunday)
142        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        // year - Extract year from date
156        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        // month - Extract month from date
169        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        // day - Extract day from date
182        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
212// Helper functions
213fn 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    // Try various date formats
284    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}