Skip to main content

sayiir_core/
loop_result.rs

1//! Loop iteration result type.
2//!
3//! [`LoopResult`] is the return type of loop body tasks. It tells the
4//! runtime whether to execute another iteration (`Again`) or exit the
5//! loop (`Done`).
6
7use crate::codec::LoopDecision;
8
9/// The result of a single loop iteration.
10///
11/// A task inside a `loop_task` must return `LoopResult<T>`:
12///
13/// - `Again(value)` — feed `value` back as input to the next iteration.
14/// - `Done(value)` — exit the loop; `value` becomes the loop's output.
15///
16/// # Example
17///
18/// ```rust
19/// use sayiir_core::LoopResult;
20///
21/// fn refine(draft: String) -> LoopResult<String> {
22///     if draft.len() > 100 {
23///         LoopResult::Done(draft)
24///     } else {
25///         LoopResult::Again(format!("{draft} ...more"))
26///     }
27/// }
28/// ```
29#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
30#[serde(tag = "_loop", content = "value")]
31#[cfg_attr(
32    feature = "rkyv",
33    derive(rkyv::Archive, rkyv::Serialize, rkyv::Deserialize)
34)]
35pub enum LoopResult<T> {
36    /// Continue looping with the wrapped value as the next iteration's input.
37    #[serde(rename = "again")]
38    Again(T),
39    /// Exit the loop with the wrapped value as the loop's output.
40    #[serde(rename = "done")]
41    Done(T),
42}
43
44/// Helper trait to extract the inner type from a `LoopResult<T>`.
45///
46/// Used by the `workflow!` macro to resolve the output type of a loop node:
47/// the loop body returns `LoopResult<T>` but the loop itself outputs `T`.
48///
49/// ```rust
50/// use sayiir_core::loop_result::{LoopResult, LoopOutput};
51///
52/// // <LoopResult<u32> as LoopOutput>::Inner == u32
53/// fn assert_inner<T: LoopOutput<Inner = u32>>() {}
54/// assert_inner::<LoopResult<u32>>();
55/// ```
56pub trait LoopOutput {
57    /// The inner type after unwrapping the `LoopResult` envelope.
58    type Inner;
59}
60
61impl<T> LoopOutput for LoopResult<T> {
62    type Inner = T;
63}
64
65impl<T> LoopResult<T> {
66    /// Returns `true` if this is a `Done` variant.
67    #[must_use]
68    pub fn is_done(&self) -> bool {
69        matches!(self, Self::Done(_))
70    }
71
72    /// Returns `true` if this is an `Again` variant.
73    #[must_use]
74    pub fn is_again(&self) -> bool {
75        matches!(self, Self::Again(_))
76    }
77
78    /// Unwrap the inner value regardless of variant.
79    #[must_use]
80    pub fn into_inner(self) -> T {
81        match self {
82            Self::Again(v) | Self::Done(v) => v,
83        }
84    }
85
86    /// Split into a [`LoopDecision`] and the inner value.
87    #[must_use]
88    pub fn into_decision(self) -> (LoopDecision, T) {
89        match self {
90            Self::Again(v) => (LoopDecision::Again, v),
91            Self::Done(v) => (LoopDecision::Done, v),
92        }
93    }
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99
100    #[test]
101    fn done_is_done() {
102        let r = LoopResult::Done(42);
103        assert!(r.is_done());
104        assert!(!r.is_again());
105    }
106
107    #[test]
108    fn again_is_again() {
109        let r = LoopResult::Again(42);
110        assert!(r.is_again());
111        assert!(!r.is_done());
112    }
113
114    #[test]
115    fn into_inner_done() {
116        assert_eq!(LoopResult::Done("hello").into_inner(), "hello");
117    }
118
119    #[test]
120    fn into_inner_again() {
121        assert_eq!(LoopResult::Again(99).into_inner(), 99);
122    }
123
124    #[test]
125    fn into_decision_done() {
126        let (decision, val) = LoopResult::Done(10).into_decision();
127        assert_eq!(decision, LoopDecision::Done);
128        assert_eq!(val, 10);
129    }
130
131    #[test]
132    fn into_decision_again() {
133        let (decision, val) = LoopResult::Again(5).into_decision();
134        assert_eq!(decision, LoopDecision::Again);
135        assert_eq!(val, 5);
136    }
137
138    #[test]
139    fn serde_round_trip_done() {
140        let r = LoopResult::Done(42u32);
141        let json = serde_json::to_string(&r).unwrap();
142        let back: LoopResult<u32> = serde_json::from_str(&json).unwrap();
143        assert!(back.is_done());
144        assert_eq!(back.into_inner(), 42);
145    }
146
147    #[test]
148    fn serde_round_trip_again() {
149        let r = LoopResult::Again("next".to_string());
150        let json = serde_json::to_string(&r).unwrap();
151        let back: LoopResult<String> = serde_json::from_str(&json).unwrap();
152        assert!(back.is_again());
153        assert_eq!(back.into_inner(), "next");
154    }
155}