add support for durations and timestamps

This commit is contained in:
2025-08-09 20:55:57 +02:00
parent f73f4c8ae4
commit 96a4494768
2 changed files with 119 additions and 2 deletions

View File

@@ -199,6 +199,18 @@ name + " is " + string(age) + " years old and " + (active ? "active" : "inactive
true}}}</code true}}}</code
> >
</div> </div>
<div class="example" onclick="loadExample(this)">
<h4>Time and Date Operations</h4>
<code
data-expression="some_date < now - old ? 'old' : 'recent'"
data-context='{"some_date": "2023-10-01T12:00:00Z", "now": "2023-10-03T12:00:00Z", "old": "24h"}'
>Expression: some_date < now - old ? 'old' : 'recent'</code
>
<code
>Context: {"some_date": "2023-10-01T12:00:00Z", "now":
"2023-10-03T12:00:00Z", "old": "24h"}</code
>
</div> </div>
<script type="module"> <script type="module">

View File

@@ -1,4 +1,5 @@
use cel::{Context, Program, Value}; use cel::{Context, Program, Value};
use chrono::Duration as ChronoDuration;
use js_sys; use js_sys;
use serde::Serialize; use serde::Serialize;
use std::collections::HashMap; use std::collections::HashMap;
@@ -20,6 +21,59 @@ macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string())) ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
} }
/// Parse an ISO 8601 timestamp string into a CEL Timestamp
fn parse_timestamp(timestamp_str: &str) -> Result<Value, String> {
Ok(Value::Timestamp(
timestamp_str
.parse()
.map_err(|e| format!("Invalid timestamp format: {}", e))?,
))
}
/// Parse a duration string (e.g., "1h30m", "45s", "2.5h") into a CEL Duration
fn parse_duration(duration_str: &str) -> Result<Value, String> {
// Parse duration strings like "1h30m45s", "2.5h", "300s", etc.
let duration_str = duration_str.trim();
// Simple parser for common duration formats
let mut total_seconds = 0.0;
let mut current_number = String::new();
for ch in duration_str.chars() {
if ch.is_numeric() || ch == '.' {
current_number.push(ch);
} else {
if !current_number.is_empty() {
let value: f64 = current_number
.parse()
.map_err(|_| format!("Invalid number in duration: {}", current_number))?;
let multiplier = match ch {
's' => 1.0,
'm' => 60.0,
'h' => 3600.0,
'd' => 86400.0,
_ => return Err(format!("Unknown duration unit: {}", ch)),
};
total_seconds += value * multiplier;
current_number.clear();
}
}
}
// Handle case where string ends with a number (assume seconds)
if !current_number.is_empty() {
let value: f64 = current_number
.parse()
.map_err(|_| format!("Invalid number in duration: {}", current_number))?;
total_seconds += value;
}
let duration = ChronoDuration::seconds(total_seconds as i64);
Ok(Value::Duration(duration))
}
/// Convert a JavaScript value to a CEL Value recursively /// Convert a JavaScript value to a CEL Value recursively
fn js_value_to_cel_value(js_val: &JsValue) -> Result<Value, String> { fn js_value_to_cel_value(js_val: &JsValue) -> Result<Value, String> {
if js_val.is_null() || js_val.is_undefined() { if js_val.is_null() || js_val.is_undefined() {
@@ -34,7 +88,26 @@ fn js_value_to_cel_value(js_val: &JsValue) -> Result<Value, String> {
Ok(Value::Float(n)) Ok(Value::Float(n))
} }
} else if let Some(s) = js_val.as_string() { } else if let Some(s) = js_val.as_string() {
Ok(Value::String(s.into())) // Check if this string represents a timestamp or duration
if s.contains('T') && (s.contains('Z') || s.contains('+') || s.contains('-')) {
// Looks like an ISO 8601 timestamp
match parse_timestamp(&s) {
Ok(ts) => Ok(ts),
Err(_) => Ok(Value::String(s.into())), // Fall back to string if parsing fails
}
} else if s
.chars()
.any(|c| c == 'h' || c == 'm' || c == 's' || c == 'd')
&& s.chars().any(|c| c.is_numeric())
{
// Looks like a duration string
match parse_duration(&s) {
Ok(dur) => Ok(dur),
Err(_) => Ok(Value::String(s.into())), // Fall back to string if parsing fails
}
} else {
Ok(Value::String(s.into()))
}
} else if js_val.is_array() { } else if js_val.is_array() {
// Handle arrays // Handle arrays
let array = js_sys::Array::from(js_val); let array = js_sys::Array::from(js_val);
@@ -70,7 +143,6 @@ fn js_value_to_cel_value(js_val: &JsValue) -> Result<Value, String> {
} }
} }
// Simple wrapper to make Value serializable for JS conversion
#[derive(Serialize)] #[derive(Serialize)]
#[serde(untagged)] #[serde(untagged)]
enum SerializableValue { enum SerializableValue {
@@ -81,6 +153,8 @@ enum SerializableValue {
String(String), String(String),
List(Vec<SerializableValue>), List(Vec<SerializableValue>),
Map(HashMap<String, SerializableValue>), Map(HashMap<String, SerializableValue>),
Timestamp(String), // Serialize timestamps as ISO strings
Duration(String), // Serialize durations as strings like "1h30m45s"
Other(String), Other(String),
} }
@@ -101,6 +175,37 @@ fn cel_value_to_serializable(cel_val: &Value) -> SerializableValue {
.map(|(k, v)| (k.to_string(), cel_value_to_serializable(v))) .map(|(k, v)| (k.to_string(), cel_value_to_serializable(v)))
.collect(), .collect(),
), ),
Value::Timestamp(ts) => SerializableValue::Timestamp(ts.to_string()),
Value::Duration(dur) => {
// Convert duration back to a readable string format
let total_seconds = dur.as_seconds_f64();
let hours = (total_seconds / 3600.0).floor() as u64;
let minutes = ((total_seconds % 3600.0) / 60.0).floor() as u64;
let seconds = total_seconds % 60.0;
if hours > 0 {
if seconds.fract() == 0.0 {
SerializableValue::Duration(format!(
"{}h{}m{}s",
hours, minutes, seconds as u64
))
} else {
SerializableValue::Duration(format!("{}h{}m{:.3}s", hours, minutes, seconds))
}
} else if minutes > 0 {
if seconds.fract() == 0.0 {
SerializableValue::Duration(format!("{}m{}s", minutes, seconds as u64))
} else {
SerializableValue::Duration(format!("{}m{:.3}s", minutes, seconds))
}
} else {
if seconds.fract() == 0.0 {
SerializableValue::Duration(format!("{}s", seconds as u64))
} else {
SerializableValue::Duration(format!("{:.3}s", seconds))
}
}
}
_ => SerializableValue::Other(format!("{:?}", cel_val)), _ => SerializableValue::Other(format!("{:?}", cel_val)),
} }
} }