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 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 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 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 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 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 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 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 engine.register_function("now", |_args, _facts| {
125 let now = Utc::now();
126 Ok(Value::String(now.to_rfc3339()))
127 });
128
129 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 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 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 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 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
206fn 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 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}