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