use super::elapsed_timer::{reset_timer_field, ElapsedTimer};
use crate::{BehaviorNode, Context, NodeStatus, TreeStatusSnapshot};
use async_trait::async_trait;
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::time::Duration;
macro_rules! from_json_with_duration {
($config:ident) => {
pub fn from_json(config: Value) -> anyhow::Result<Self> {
let cfg: $config = serde_json::from_value(config)?;
Ok(Self {
duration: Duration::from_secs_f64(cfg.duration_secs),
timer: ElapsedTimer::default(),
last_status: None,
})
}
};
}
#[derive(Debug, Default)]
pub struct TimerNode {
duration: Duration,
timer: ElapsedTimer,
last_status: Option<NodeStatus>,
}
#[derive(Debug, Serialize, Deserialize)]
struct TimerConfig {
duration_secs: f64,
}
impl TimerNode {
from_json_with_duration!(TimerConfig);
}
#[async_trait]
impl BehaviorNode for TimerNode {
async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
let status = if self.timer.elapsed_since_start(self.duration) {
NodeStatus::Success
} else {
NodeStatus::Running
};
self.last_status = Some(status);
Ok(status)
}
reset_timer_field!(timer);
fn name(&self) -> &str {
"TimerNode"
}
fn status_snapshot(&self) -> TreeStatusSnapshot {
TreeStatusSnapshot {
name: self.name().to_string(),
status: self.last_status,
active_capability: self.active_capability().map(String::from),
children: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct IdleNode {
last_status: Option<NodeStatus>,
}
impl IdleNode {
pub fn from_json(_config: Value) -> anyhow::Result<Self> {
Ok(Self::default())
}
}
#[async_trait]
impl BehaviorNode for IdleNode {
async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
self.last_status = Some(NodeStatus::Success);
Ok(NodeStatus::Success)
}
fn name(&self) -> &str {
"IdleNode"
}
fn status_snapshot(&self) -> TreeStatusSnapshot {
TreeStatusSnapshot {
name: self.name().to_string(),
status: self.last_status,
active_capability: self.active_capability().map(String::from),
children: Vec::new(),
}
}
}
#[derive(Debug, Default)]
pub struct PauseNode {
duration: Duration,
timer: ElapsedTimer,
last_status: Option<NodeStatus>,
}
#[derive(Debug, Serialize, Deserialize)]
struct PauseConfig {
duration_secs: f64,
}
impl PauseNode {
from_json_with_duration!(PauseConfig);
}
#[async_trait]
impl BehaviorNode for PauseNode {
async fn tick(&mut self, _ctx: &Context) -> anyhow::Result<NodeStatus> {
if !self.timer.is_running() {
tracing::debug!("PauseNode: starting pause for {:?}", self.duration);
}
let status = if self.timer.elapsed_since_start(self.duration) {
tracing::debug!("PauseNode: pause complete");
NodeStatus::Success
} else {
NodeStatus::Running
};
self.last_status = Some(status);
Ok(status)
}
reset_timer_field!(timer);
fn name(&self) -> &str {
"PauseNode"
}
fn status_snapshot(&self) -> TreeStatusSnapshot {
TreeStatusSnapshot {
name: self.name().to_string(),
status: self.last_status,
active_capability: self.active_capability().map(String::from),
children: Vec::new(),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn timer_node_status_snapshot_is_none_before_any_tick() {
let node = TimerNode::from_json(serde_json::json!({ "duration_secs": 60.0 })).unwrap();
let snapshot = node.status_snapshot();
assert_eq!(snapshot.status, None);
assert!(snapshot.children.is_empty());
}
#[tokio::test]
#[ignore = "Requires Redis infrastructure"]
async fn timer_node_status_snapshot_matches_the_last_tick_result() {
let ctx = crate::test_helpers::create_test_context("timer_node_status_snapshot").await;
let mut node = TimerNode::from_json(serde_json::json!({ "duration_secs": 0.0 })).unwrap();
let status = node.tick(&ctx).await.unwrap();
assert_eq!(status, NodeStatus::Success);
let snapshot = node.status_snapshot();
assert_eq!(snapshot.status, Some(status));
assert_eq!(snapshot.name, "TimerNode");
assert!(snapshot.children.is_empty());
}
}