use crate::graph::NodeId;
use crate::value::Value;
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(tag = "type", content = "node")]
#[non_exhaustive]
pub enum LoopCondition {
#[default]
BodyTerminal,
WhenSignaled(NodeId),
Exhaust,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LoopSignal {
Continue,
Stop,
}
pub fn read_loop_signal(value: &Value) -> Option<LoopSignal> {
use LoopSignal::{Continue, Stop};
match value {
Value::Empty => Some(Stop),
Value::Text(s) => match s.as_ref() {
"done" | "stop" => Some(Stop),
"continue" => Some(Continue),
_ => None,
},
Value::Json(j) => {
if let Some(b) = j.as_bool() {
return Some(if b { Stop } else { Continue });
}
if let Some(s) = j.as_str() {
return match s {
"done" | "stop" => Some(Stop),
"continue" => Some(Continue),
_ => None,
};
}
j.get("done")
.and_then(|d| d.as_bool())
.map(|b| if b { Stop } else { Continue })
}
_ => None,
}
}
pub fn read_arm_selector(value: &Value) -> Option<String> {
match value {
Value::Text(s) => Some(s.to_string()),
Value::Json(j) => j
.as_str()
.map(String::from)
.or_else(|| j.as_bool().map(|b| b.to_string()))
.or_else(|| j.get("branch").and_then(|b| b.as_str()).map(String::from)),
_ => None,
}
}
pub const DEFAULT_ARM_LABELS: [&str; 2] = ["default", "else"];
pub fn is_default_arm(label: &str) -> bool {
DEFAULT_ARM_LABELS.contains(&label)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_condition_round_trips_through_json() {
for condition in [
LoopCondition::BodyTerminal,
LoopCondition::WhenSignaled("critic".into()),
LoopCondition::Exhaust,
] {
let text = serde_json::to_string(&condition).expect("serializable");
let back: LoopCondition = serde_json::from_str(&text).expect("readable");
assert_eq!(back, condition, "{text}");
}
}
#[test]
fn stop_signals() {
for v in [
Value::Empty,
Value::json(serde_json::json!(true)),
Value::json(serde_json::json!("done")),
Value::json(serde_json::json!("stop")),
Value::json(serde_json::json!({"done": true})),
Value::text("done"),
Value::text("stop"),
] {
assert_eq!(read_loop_signal(&v), Some(LoopSignal::Stop), "{v:?}");
}
}
#[test]
fn continue_signals() {
for v in [
Value::json(serde_json::json!(false)),
Value::json(serde_json::json!({"done": false})),
Value::text("continue"),
] {
assert_eq!(read_loop_signal(&v), Some(LoopSignal::Continue), "{v:?}");
}
}
#[test]
fn tensors_are_not_signals() {
assert_eq!(read_loop_signal(&Value::tensor(vec![1.0], vec![1])), None);
assert_eq!(read_loop_signal(&Value::json(serde_json::json!(42))), None);
assert_eq!(
read_loop_signal(&Value::json(serde_json::json!({"score": 0.9}))),
None
);
}
#[test]
fn arm_selectors() {
assert_eq!(
read_arm_selector(&Value::json(serde_json::json!("billing"))),
Some("billing".into())
);
assert_eq!(
read_arm_selector(&Value::json(serde_json::json!(true))),
Some("true".into())
);
assert_eq!(
read_arm_selector(&Value::json(serde_json::json!({"branch": "retry"}))),
Some("retry".into())
);
assert_eq!(read_arm_selector(&Value::text("tech")), Some("tech".into()));
}
#[test]
fn unusable_selectors_are_none() {
assert_eq!(read_arm_selector(&Value::tensor(vec![1.0], vec![1])), None);
assert_eq!(read_arm_selector(&Value::Empty), None);
assert_eq!(
read_arm_selector(&Value::json(serde_json::json!({"score": 1}))),
None
);
}
#[test]
fn default_arm_labels() {
assert!(is_default_arm("default"));
assert!(is_default_arm("else"));
assert!(!is_default_arm("billing"));
}
}