add support for durations and timestamps
This commit is contained in:
12
index.html
12
index.html
@@ -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">
|
||||||
|
|||||||
109
src/lib.rs
109
src/lib.rs
@@ -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)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user