use serde::{Deserialize, Serialize};
use serde_json::{Map, Value};
pub type ContextTuple = Vec<String>;
pub const INTERRUPT_KEY: &str = "__interrupt__";
#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq, Eq)]
pub struct Interrupt {
#[serde(default)]
pub op: String,
#[serde(default)]
pub ctx: ContextTuple,
pub ctx_to_cancel: ContextTuple,
#[serde(default)]
pub reason: String,
}
impl Interrupt {
pub fn new(ctx_to_cancel: impl Into<ContextTuple>, reason: impl Into<String>) -> Self {
Self {
op: String::new(),
ctx: Vec::new(),
ctx_to_cancel: ctx_to_cancel.into(),
reason: reason.into(),
}
}
pub fn into_frame_value(self) -> Value {
let inner = serde_json::to_value(&self).unwrap_or(Value::Null);
let mut wrapper = Map::new();
wrapper.insert(INTERRUPT_KEY.into(), inner);
Value::Object(wrapper)
}
pub fn from_frame_value(value: &Value) -> Option<Self> {
let inner = value.as_object()?.get(INTERRUPT_KEY)?;
serde_json::from_value(inner.clone()).ok()
}
}
impl From<Interrupt> for Value {
fn from(i: Interrupt) -> Self {
i.into_frame_value()
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn interrupt_round_trips_through_canonical_json() {
let irq = Interrupt::new(vec!["main".to_string(), "turn_1".into()], "user spoke");
let v: Value = irq.clone().into();
assert_eq!(
v,
json!({
"__interrupt__": {
"op": "",
"ctx": [],
"ctx_to_cancel": ["main", "turn_1"],
"reason": "user spoke"
}
})
);
let parsed = Interrupt::from_frame_value(&v).expect("parse round-trip");
assert_eq!(parsed, irq);
}
#[test]
fn from_frame_value_returns_none_on_non_interrupt_shapes() {
assert!(Interrupt::from_frame_value(&json!({"other": 1})).is_none());
assert!(Interrupt::from_frame_value(&json!("not an object")).is_none());
assert!(Interrupt::from_frame_value(&json!({"__interrupt__": "not a map"})).is_none());
}
#[test]
fn from_frame_value_handles_minimal_shape() {
let v = json!({"__interrupt__": {"ctx_to_cancel": ["x"]}});
let irq = Interrupt::from_frame_value(&v).expect("parse minimal");
assert_eq!(irq.ctx_to_cancel, vec!["x".to_string()]);
assert_eq!(irq.reason, "");
assert_eq!(irq.op, "");
}
}