1use std::collections::HashSet;
22use std::future::Future;
23use std::pin::Pin;
24
25use async_trait::async_trait;
26use chrono::{DateTime, Duration, SecondsFormat, Utc};
27use serde::{Deserialize, Serialize};
28use serde_json::Value;
29
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "snake_case")]
33pub enum Root {
34 Context,
36 Event,
38 Result,
40 Error,
42 Config,
44 Steps,
46}
47
48#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
50#[serde(rename_all = "snake_case")]
51pub enum Seg {
52 Key(String),
54 Index(usize),
56}
57
58#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
60#[serde(rename_all = "snake_case")]
61pub enum BinOp {
62 Eq,
64 Ne,
66 Lt,
68 Le,
70 Gt,
72 Ge,
74 And,
76 Or,
78 Add,
80 Sub,
82 Mul,
84 Div,
86 Mod,
88}
89
90#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
92#[serde(rename_all = "snake_case")]
93pub enum Expr {
94 Lit(Value),
96 Dur(i64),
98 Now,
100 Path {
102 root: Root,
104 path: Vec<Seg>,
106 },
107 Not(Box<Expr>),
109 Neg(Box<Expr>),
111 Bin {
113 op: BinOp,
115 l: Box<Expr>,
117 r: Box<Expr>,
119 },
120 Coalesce(Box<Expr>, Box<Expr>),
122 If {
124 c: Box<Expr>,
126 t: Box<Expr>,
128 e: Box<Expr>,
130 },
131 Array(Vec<Expr>),
133 In {
135 l: Box<Expr>,
137 r: Box<Expr>,
139 negate: bool,
141 },
142 Method {
144 obj: Box<Expr>,
146 name: String,
148 args: Vec<Expr>,
150 },
151 Call {
153 name: String,
155 args: Vec<Expr>,
157 },
158}
159
160#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
162#[serde(rename_all = "snake_case")]
163pub enum Part {
164 Text(String),
166 Expr(Expr),
168}
169
170#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
173pub struct Template(pub Vec<Part>);
174
175#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
177#[serde(rename_all = "snake_case")]
178pub enum Tpl {
179 Lit(Value),
181 Template(Template),
183 Map(Vec<(String, Tpl)>),
185 List(Vec<Tpl>),
187}
188
189#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
191#[serde(rename_all = "snake_case")]
192pub enum Dur {
193 Secs(i64),
195 Tpl(Template),
197}
198
199pub struct Scope<'a> {
201 pub context: &'a Value,
203 pub event: &'a Value,
205 pub result: &'a Value,
207 pub error: &'a Value,
209 pub config: &'a Value,
211 pub completed: &'a HashSet<String>,
213 pub now: DateTime<Utc>,
215}
216
217#[async_trait]
219pub trait Funcs: Send + Sync {
220 async fn call(&self, name: &str, args: &[Value]) -> Result<Value, String>;
222}
223
224pub struct NoFuncs;
226
227#[async_trait]
228impl Funcs for NoFuncs {
229 async fn call(&self, name: &str, _args: &[Value]) -> Result<Value, String> {
230 Err(format!("function `{name}` is not declared"))
231 }
232}
233
234type Eval<'a> = Pin<Box<dyn Future<Output = Result<Value, String>> + Send + 'a>>;
235
236pub fn truthy(v: &Value) -> bool {
238 match v {
239 Value::Null => false,
240 Value::Bool(b) => *b,
241 Value::Number(n) => n.as_f64().map(|f| f != 0.0).unwrap_or(true),
242 Value::String(s) => !s.is_empty(),
243 Value::Array(_) | Value::Object(_) => true,
244 }
245}
246
247pub fn eval<'a>(expr: &'a Expr, scope: &'a Scope<'a>, funcs: &'a dyn Funcs) -> Eval<'a> {
249 Box::pin(async move {
250 match expr {
251 Expr::Lit(v) => Ok(v.clone()),
252 Expr::Dur(s) => Ok(Value::from(*s)),
253 Expr::Now => Ok(Value::String(time_text(scope.now))),
254 Expr::Path { root, path } => Ok(read_path(*root, path, scope)),
255 Expr::Not(e) => Ok(Value::Bool(!truthy(&eval(e, scope, funcs).await?))),
256 Expr::Neg(e) => {
257 let v = eval(e, scope, funcs).await?;
258 number(&v)
259 .map(|n| num_value(-n))
260 .ok_or_else(|| "`-` needs a number".to_string())
261 }
262 Expr::Bin {
263 op: BinOp::And,
264 l,
265 r,
266 } => {
267 let left = eval(l, scope, funcs).await?;
268 if !truthy(&left) {
269 return Ok(Value::Bool(false));
270 }
271 Ok(Value::Bool(truthy(&eval(r, scope, funcs).await?)))
272 }
273 Expr::Bin {
274 op: BinOp::Or,
275 l,
276 r,
277 } => {
278 let left = eval(l, scope, funcs).await?;
279 if truthy(&left) {
280 return Ok(Value::Bool(true));
281 }
282 Ok(Value::Bool(truthy(&eval(r, scope, funcs).await?)))
283 }
284 Expr::Bin { op, l, r } => {
285 let left = eval(l, scope, funcs).await?;
286 let right = eval(r, scope, funcs).await?;
287 binary(*op, &left, &right, r)
288 }
289 Expr::Coalesce(l, r) => {
290 let left = eval(l, scope, funcs).await?;
291 if left.is_null() {
292 eval(r, scope, funcs).await
293 } else {
294 Ok(left)
295 }
296 }
297 Expr::If { c, t, e } => {
298 if truthy(&eval(c, scope, funcs).await?) {
299 eval(t, scope, funcs).await
300 } else {
301 eval(e, scope, funcs).await
302 }
303 }
304 Expr::Array(items) => {
305 let mut out = Vec::with_capacity(items.len());
306 for item in items {
307 out.push(eval(item, scope, funcs).await?);
308 }
309 Ok(Value::Array(out))
310 }
311 Expr::In { l, r, negate } => {
312 let needle = eval(l, scope, funcs).await?;
313 let hay = eval(r, scope, funcs).await?;
314 Ok(Value::Bool(contains(&hay, &needle) != *negate))
315 }
316 Expr::Method { obj, name, args } => {
317 let recv = eval(obj, scope, funcs).await?;
318 let mut vals = Vec::with_capacity(args.len());
319 for a in args {
320 vals.push(eval(a, scope, funcs).await?);
321 }
322 method(&recv, name, &vals)
323 }
324 Expr::Call { name, args } => {
325 let mut vals = Vec::with_capacity(args.len());
326 for a in args {
327 vals.push(eval(a, scope, funcs).await?);
328 }
329 match name.as_str() {
330 "new_uuid" => Ok(Value::String(uuid::Uuid::new_v4().to_string())),
331 "coalesce" => Ok(vals
332 .into_iter()
333 .find(|v| !v.is_null())
334 .unwrap_or(Value::Null)),
335 "len" => method(vals.first().unwrap_or(&Value::Null), "length", &[]),
336 other => funcs.call(other, &vals).await,
337 }
338 }
339 }
340 })
341}
342
343pub async fn holds(expr: &Expr, scope: &Scope<'_>, funcs: &dyn Funcs) -> Result<bool, String> {
345 Ok(truthy(&eval(expr, scope, funcs).await?))
346}
347
348pub async fn render_template(
350 t: &Template,
351 scope: &Scope<'_>,
352 funcs: &dyn Funcs,
353) -> Result<Value, String> {
354 if let [Part::Expr(e)] = t.0.as_slice() {
355 return eval(e, scope, funcs).await;
356 }
357 let mut out = String::new();
358 for part in &t.0 {
359 match part {
360 Part::Text(s) => out.push_str(s),
361 Part::Expr(e) => out.push_str(&text_of(&eval(e, scope, funcs).await?)),
362 }
363 }
364 Ok(Value::String(out))
365}
366
367pub fn render<'a>(tpl: &'a Tpl, scope: &'a Scope<'a>, funcs: &'a dyn Funcs) -> Eval<'a> {
369 Box::pin(async move {
370 match tpl {
371 Tpl::Lit(v) => Ok(v.clone()),
372 Tpl::Template(t) => render_template(t, scope, funcs).await,
373 Tpl::Map(entries) => {
374 let mut map = serde_json::Map::new();
375 for (k, v) in entries {
376 map.insert(k.clone(), render(v, scope, funcs).await?);
377 }
378 Ok(Value::Object(map))
379 }
380 Tpl::List(items) => {
381 let mut out = Vec::with_capacity(items.len());
382 for item in items {
383 out.push(render(item, scope, funcs).await?);
384 }
385 Ok(Value::Array(out))
386 }
387 }
388 })
389}
390
391pub async fn render_duration(d: &Dur, scope: &Scope<'_>, funcs: &dyn Funcs) -> Result<i64, String> {
393 match d {
394 Dur::Secs(s) => Ok(*s),
395 Dur::Tpl(t) => {
396 let v = render_template(t, scope, funcs).await?;
397 match &v {
398 Value::Number(n) => n
399 .as_i64()
400 .ok_or_else(|| format!("`{n}` is not a whole number of seconds")),
401 Value::String(s) => parse_duration(s)
402 .ok_or_else(|| format!("`{s}` is not a duration (use 30s, 15m, 24h, 7d)")),
403 other => Err(format!("`{other}` is not a duration")),
404 }
405 }
406 }
407}
408
409pub fn parse_duration(text: &str) -> Option<i64> {
411 let t = text.trim();
412 if !t.is_empty() && t.chars().all(|c| c.is_ascii_digit()) {
413 return t.parse().ok();
414 }
415 let split = t.find(|c: char| !c.is_ascii_digit())?;
416 let (digits, unit) = t.split_at(split);
417 let n: i64 = digits.parse().ok()?;
418 let secs = match unit {
419 "s" => 1,
420 "m" => 60,
421 "h" => 3600,
422 "d" => 86_400,
423 "w" => 604_800,
424 _ => return None,
425 };
426 n.checked_mul(secs)
427}
428
429pub fn wait_key(values: &[Value]) -> String {
433 let parts: Vec<String> = values.iter().map(canonical).collect();
434 serde_json::to_string(&parts).unwrap_or_default()
435}
436
437pub fn canonical(v: &Value) -> String {
440 match v {
441 Value::String(s) => s.clone(),
442 Value::Number(n) => match n.as_f64() {
443 Some(f) if f.fract() == 0.0 && f.abs() < 9e15 => format!("{}", f as i64),
444 _ => n.to_string(),
445 },
446 other => other.to_string(),
447 }
448}
449
450fn read_path(root: Root, path: &[Seg], scope: &Scope<'_>) -> Value {
451 if root == Root::Steps {
452 return match path {
453 [Seg::Key(name), Seg::Key(field)] if field == "completed" => {
454 Value::Bool(scope.completed.contains(name))
455 }
456 _ => Value::Null,
457 };
458 }
459 let mut cur = match root {
460 Root::Context => scope.context,
461 Root::Event => scope.event,
462 Root::Result => scope.result,
463 Root::Error => scope.error,
464 Root::Config => scope.config,
465 Root::Steps => unreachable!(),
466 };
467 for seg in path {
468 cur = match (seg, cur) {
469 (Seg::Key(k), Value::Object(m)) => match m.get(k) {
470 Some(v) => v,
471 None => return Value::Null,
472 },
473 (Seg::Index(i), Value::Array(a)) => match a.get(*i) {
474 Some(v) => v,
475 None => return Value::Null,
476 },
477 _ => return Value::Null,
478 };
479 }
480 cur.clone()
481}
482
483fn time_text(t: DateTime<Utc>) -> String {
484 t.to_rfc3339_opts(SecondsFormat::Micros, true)
485}
486
487fn parse_time(v: &Value) -> Option<DateTime<Utc>> {
488 v.as_str()
489 .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
490 .map(|d| d.with_timezone(&Utc))
491}
492
493fn number(v: &Value) -> Option<f64> {
494 match v {
495 Value::Number(n) => n.as_f64(),
496 _ => None,
497 }
498}
499
500fn num_value(f: f64) -> Value {
501 if f.fract() == 0.0 && f.abs() < 9e15 {
502 Value::from(f as i64)
503 } else {
504 serde_json::Number::from_f64(f)
505 .map(Value::Number)
506 .unwrap_or(Value::Null)
507 }
508}
509
510fn json_eq(a: &Value, b: &Value) -> bool {
511 match (a, b) {
512 (Value::Number(x), Value::Number(y)) => x.as_f64() == y.as_f64(),
513 _ => a == b,
514 }
515}
516
517fn contains(hay: &Value, needle: &Value) -> bool {
518 match hay {
519 Value::Array(items) => items.iter().any(|i| json_eq(i, needle)),
520 Value::String(s) => needle.as_str().is_some_and(|n| s.contains(n)),
521 _ => false,
522 }
523}
524
525fn method(recv: &Value, name: &str, args: &[Value]) -> Result<Value, String> {
526 match name {
527 "length" | "len" => Ok(match recv {
528 Value::String(s) => Value::from(s.chars().count() as i64),
529 Value::Array(a) => Value::from(a.len() as i64),
530 Value::Object(m) => Value::from(m.len() as i64),
531 _ => Value::from(0),
532 }),
533 "contains" | "includes" => Ok(Value::Bool(contains(
534 recv,
535 args.first().unwrap_or(&Value::Null),
536 ))),
537 "starts_with" => Ok(Value::Bool(
538 match (recv.as_str(), args.first().and_then(Value::as_str)) {
539 (Some(s), Some(p)) => s.starts_with(p),
540 _ => false,
541 },
542 )),
543 "ends_with" => Ok(Value::Bool(
544 match (recv.as_str(), args.first().and_then(Value::as_str)) {
545 (Some(s), Some(p)) => s.ends_with(p),
546 _ => false,
547 },
548 )),
549 other => Err(format!(
550 "method `.{other}` is not part of the workflow expression set"
551 )),
552 }
553}
554
555fn binary(op: BinOp, l: &Value, r: &Value, right_expr: &Expr) -> Result<Value, String> {
556 match op {
557 BinOp::Eq => Ok(Value::Bool(json_eq(l, r))),
558 BinOp::Ne => Ok(Value::Bool(!json_eq(l, r))),
559 BinOp::Lt | BinOp::Le | BinOp::Gt | BinOp::Ge => {
560 let ord = if let (Some(x), Some(y)) = (number(l), number(r)) {
561 x.partial_cmp(&y)
562 } else if let (Some(x), Some(y)) = (parse_time(l), parse_time(r)) {
563 Some(x.cmp(&y))
564 } else if let (Some(x), Some(y)) = (l.as_str(), r.as_str()) {
565 Some(x.cmp(y))
566 } else {
567 None
568 };
569 Ok(Value::Bool(match (ord, op) {
570 (None, _) => false,
571 (Some(o), BinOp::Lt) => o.is_lt(),
572 (Some(o), BinOp::Le) => o.is_le(),
573 (Some(o), BinOp::Gt) => o.is_gt(),
574 (Some(o), _) => o.is_ge(),
575 }))
576 }
577 BinOp::Add | BinOp::Sub => {
578 if let (Some(t), Expr::Dur(secs)) = (parse_time(l), right_expr) {
580 let delta = Duration::seconds(if op == BinOp::Add { *secs } else { -*secs });
581 return Ok(Value::String(time_text(t + delta)));
582 }
583 if op == BinOp::Add {
584 if let (Value::String(a), Value::String(b)) = (l, r) {
585 return Ok(Value::String(format!("{a}{b}")));
586 }
587 }
588 match (number(l), number(r)) {
589 (Some(x), Some(y)) => Ok(num_value(if op == BinOp::Add { x + y } else { x - y })),
590 _ => Err(format!(
591 "`{}` needs two numbers",
592 if op == BinOp::Add { "+" } else { "-" }
593 )),
594 }
595 }
596 BinOp::Mul | BinOp::Div | BinOp::Mod => match (number(l), number(r)) {
597 (Some(x), Some(y)) => {
598 if op != BinOp::Mul && y == 0.0 {
599 return Err("division by zero".into());
600 }
601 Ok(num_value(match op {
602 BinOp::Mul => x * y,
603 BinOp::Div => x / y,
604 _ => x % y,
605 }))
606 }
607 _ => Err("arithmetic needs two numbers".into()),
608 },
609 BinOp::And | BinOp::Or => unreachable!("short-circuited by eval"),
610 }
611}
612
613pub fn text_of(v: &Value) -> String {
615 match v {
616 Value::Null => String::new(),
617 Value::String(s) => s.clone(),
618 other => other.to_string(),
619 }
620}
621
622#[cfg(test)]
623mod tests {
624 use super::*;
625 use serde_json::json;
626
627 fn path(root: Root, keys: &[&str]) -> Expr {
628 Expr::Path {
629 root,
630 path: keys.iter().map(|k| Seg::Key(k.to_string())).collect(),
631 }
632 }
633
634 fn lit(v: Value) -> Box<Expr> {
635 Box::new(Expr::Lit(v))
636 }
637
638 async fn run(e: &Expr, context: Value) -> Value {
639 let done: HashSet<String> = ["a".to_string()].into();
640 let scope = Scope {
641 context: &context,
642 event: &json!({ "user_id": "u1" }),
643 result: &json!({ "id": 7 }),
644 error: &Value::Null,
645 config: &json!({}),
646 completed: &done,
647 now: DateTime::parse_from_rfc3339("2026-01-01T00:00:00Z")
648 .unwrap()
649 .with_timezone(&Utc),
650 };
651 eval(e, &scope, &NoFuncs).await.unwrap()
652 }
653
654 #[tokio::test]
655 async fn paths_read_the_scope_and_a_missing_key_is_null() {
656 let ctx = json!({ "user": { "id": "u1", "roles": ["a", "b"] } });
657 assert_eq!(
658 run(&path(Root::Context, &["user", "id"]), ctx.clone()).await,
659 json!("u1")
660 );
661 assert_eq!(
662 run(
663 &path(Root::Context, &["user", "nope", "deeper"]),
664 ctx.clone()
665 )
666 .await,
667 Value::Null
668 );
669 assert_eq!(
670 run(&path(Root::Event, &["user_id"]), ctx.clone()).await,
671 json!("u1")
672 );
673 assert_eq!(
674 run(&path(Root::Result, &["id"]), ctx.clone()).await,
675 json!(7)
676 );
677 let idx = Expr::Path {
678 root: Root::Context,
679 path: vec![
680 Seg::Key("user".into()),
681 Seg::Key("roles".into()),
682 Seg::Index(1),
683 ],
684 };
685 assert_eq!(run(&idx, ctx.clone()).await, json!("b"));
686 assert_eq!(
687 run(&path(Root::Steps, &["a", "completed"]), ctx.clone()).await,
688 json!(true)
689 );
690 assert_eq!(
691 run(&path(Root::Steps, &["b", "completed"]), ctx).await,
692 json!(false)
693 );
694 }
695
696 #[tokio::test]
697 async fn comparison_logic_and_coalesce_follow_json_truthiness() {
698 let eq = Expr::Bin {
699 op: BinOp::Eq,
700 l: lit(json!(1)),
701 r: lit(json!(1.0)),
702 };
703 assert_eq!(run(&eq, json!({})).await, json!(true), "1 equals 1.0");
704 let and = Expr::Bin {
705 op: BinOp::And,
706 l: Box::new(path(Root::Context, &["missing"])),
707 r: Box::new(Expr::Call {
708 name: "boom".into(),
709 args: vec![],
710 }),
711 };
712 assert_eq!(
713 run(&and, json!({})).await,
714 json!(false),
715 "`&&` short-circuits past a function that would fail"
716 );
717 let co = Expr::Coalesce(Box::new(path(Root::Context, &["x"])), lit(json!("d")));
718 assert_eq!(run(&co, json!({})).await, json!("d"));
719 assert_eq!(run(&co, json!({ "x": "set" })).await, json!("set"));
720 let inn = Expr::In {
721 l: lit(json!("low")),
722 r: lit(json!(["low", "medium"])),
723 negate: false,
724 };
725 assert_eq!(run(&inn, json!({})).await, json!(true));
726 }
727
728 #[tokio::test]
729 async fn time_arithmetic_and_comparison_use_instants() {
730 let plus = Expr::Bin {
731 op: BinOp::Add,
732 l: Box::new(Expr::Now),
733 r: Box::new(Expr::Dur(86_400)),
734 };
735 assert_eq!(
736 run(&plus, json!({})).await,
737 json!("2026-01-02T00:00:00.000000Z")
738 );
739 let later = Expr::Bin {
740 op: BinOp::Ge,
741 l: Box::new(Expr::Now),
742 r: lit(json!("2025-12-31T23:00:00+00:00")),
743 };
744 assert_eq!(run(&later, json!({})).await, json!(true));
745 }
746
747 #[tokio::test]
748 async fn methods_and_builtin_functions() {
749 let ctx = json!({ "mime": "image/png", "list": [1, 2, 3] });
750 let starts = Expr::Method {
751 obj: Box::new(path(Root::Context, &["mime"])),
752 name: "starts_with".into(),
753 args: vec![Expr::Lit(json!("image/"))],
754 };
755 assert_eq!(run(&starts, ctx.clone()).await, json!(true));
756 let len = Expr::Method {
757 obj: Box::new(path(Root::Context, &["list"])),
758 name: "length".into(),
759 args: vec![],
760 };
761 assert_eq!(run(&len, ctx.clone()).await, json!(3));
762 let id = run(
763 &Expr::Call {
764 name: "new_uuid".into(),
765 args: vec![],
766 },
767 ctx.clone(),
768 )
769 .await;
770 assert!(uuid::Uuid::parse_str(id.as_str().unwrap()).is_ok());
771 let co = Expr::Call {
772 name: "coalesce".into(),
773 args: vec![Expr::Lit(Value::Null), Expr::Lit(json!(5))],
774 };
775 assert_eq!(run(&co, ctx).await, json!(5));
776 }
777
778 #[tokio::test]
779 async fn a_template_of_one_hole_keeps_the_type_and_mixed_text_is_text() {
780 let context = json!({ "n": 5, "who": "ann" });
781 let scope = Scope {
782 context: &context,
783 event: &Value::Null,
784 result: &Value::Null,
785 error: &Value::Null,
786 config: &Value::Null,
787 completed: &HashSet::new(),
788 now: Utc::now(),
789 };
790 let hole = Template(vec![Part::Expr(path(Root::Context, &["n"]))]);
791 assert_eq!(
792 render_template(&hole, &scope, &NoFuncs).await.unwrap(),
793 json!(5)
794 );
795 let mixed = Template(vec![
796 Part::Text("hi ".into()),
797 Part::Expr(path(Root::Context, &["who"])),
798 Part::Text(" #".into()),
799 Part::Expr(path(Root::Context, &["n"])),
800 ]);
801 assert_eq!(
802 render_template(&mixed, &scope, &NoFuncs).await.unwrap(),
803 json!("hi ann #5")
804 );
805 let tpl = Tpl::Map(vec![
806 ("a".into(), Tpl::Template(hole)),
807 ("b".into(), Tpl::List(vec![Tpl::Lit(json!(1))])),
808 ]);
809 assert_eq!(
810 render(&tpl, &scope, &NoFuncs).await.unwrap(),
811 json!({ "a": 5, "b": [1] })
812 );
813 }
814
815 #[test]
816 fn durations_read_with_a_unit_and_wait_keys_are_canonical() {
817 assert_eq!(parse_duration("15m"), Some(900));
818 assert_eq!(parse_duration("30d"), Some(2_592_000));
819 assert_eq!(parse_duration("120"), Some(120));
820 assert_eq!(parse_duration("soon"), None);
821 assert_eq!(parse_duration("5x"), None);
822 assert_eq!(
823 wait_key(&[json!("u1"), json!(3)]),
824 wait_key(&[json!("u1"), json!(3.0)])
825 );
826 assert_ne!(wait_key(&[json!("u1")]), wait_key(&[json!("u2")]));
827 }
828
829 #[test]
830 fn compiled_expressions_survive_their_json_form() {
831 let e = Expr::If {
832 c: Box::new(path(Root::Context, &["a"])),
833 t: lit(json!("x")),
834 e: Box::new(Expr::Bin {
835 op: BinOp::Add,
836 l: Box::new(Expr::Now),
837 r: Box::new(Expr::Dur(60)),
838 }),
839 };
840 let text = serde_json::to_string(&e).unwrap();
841 assert_eq!(serde_json::from_str::<Expr>(&text).unwrap(), e);
842 }
843}