1use crate::ctx::Ctx;
2use std::fmt;
3
4pub type StepResult<S> = Result<(S, Outcome), StepError>;
6
7pub trait Agent<S>: Send + 'static {
12 fn name(&self) -> &'static str;
14
15 fn run(&mut self, state: S, ctx: &mut Ctx) -> StepResult<S>;
18}
19
20#[derive(Debug, Clone)]
22pub enum Outcome {
23 Continue,
25
26 Done,
28 Next(&'static str),
30 Retry(RetryHint),
32 Wait(std::time::Duration),
34 Fail(String),
36}
37
38#[derive(Debug, Clone)]
41pub struct RetryHint {
42 pub reason: String,
44}
45
46impl RetryHint {
47 pub fn new(reason: impl Into<String>) -> Self {
49 Self {
50 reason: reason.into(),
51 }
52 }
53}
54
55#[derive(Debug)]
58pub enum StepError {
59 Invalid(String),
61 Transient(String),
63 Failed(String),
65 Other(String),
67}
68
69impl From<ureq::Error> for StepError {
70 fn from(e: ureq::Error) -> Self {
71 StepError::Transient(e.to_string())
72 }
73}
74
75impl From<std::io::Error> for StepError {
76 fn from(e: std::io::Error) -> Self {
77 StepError::Other(e.to_string())
78 }
79}
80
81impl StepError {
82 pub fn invalid(msg: impl Into<String>) -> Self {
84 StepError::Invalid(msg.into())
85 }
86
87 pub fn other(msg: impl Into<String>) -> Self {
89 StepError::Other(msg.into())
90 }
91
92 pub fn transient(msg: impl Into<String>) -> Self {
94 StepError::Transient(msg.into())
95 }
96}
97
98impl fmt::Display for StepError {
99 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
100 match self {
101 Self::Invalid(msg) => write!(f, "invalid: {msg}"),
102 Self::Other(msg) => write!(f, "{msg}"),
103 Self::Transient(msg) => write!(f, "transient: {msg}"),
104 Self::Failed(msg) => write!(f, "failed: {msg}"),
105 }
106 }
107}
108
109impl std::error::Error for StepError {}
110
111#[cfg(test)]
112mod tests {
113 use super::*;
114
115 #[test]
118 fn invalid_constructor() {
119 let err = StepError::invalid("bad input");
120 assert!(matches!(err, StepError::Invalid(msg) if msg == "bad input"));
121 }
122
123 #[test]
124 fn other_constructor() {
125 let err = StepError::other("something");
126 assert!(matches!(err, StepError::Other(msg) if msg == "something"));
127 }
128
129 #[test]
130 fn transient_constructor() {
131 let err = StepError::transient("timeout");
132 assert!(matches!(err, StepError::Transient(msg) if msg == "timeout"));
133 }
134
135 #[test]
138 fn display_invalid() {
139 let err = StepError::Invalid("bad input".into());
140 assert_eq!(err.to_string(), "invalid: bad input");
141 }
142
143 #[test]
144 fn display_other() {
145 let err = StepError::Other("something".into());
146 assert_eq!(err.to_string(), "something");
147 }
148
149 #[test]
150 fn display_transient() {
151 let err = StepError::Transient("timeout".into());
152 assert_eq!(err.to_string(), "transient: timeout");
153 }
154
155 #[test]
156 fn display_failed() {
157 let err = StepError::Failed("nope".into());
158 assert_eq!(err.to_string(), "failed: nope");
159 }
160
161 #[test]
164 fn from_io_error() {
165 let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
166 let step_err: StepError = io_err.into();
167 assert!(matches!(step_err, StepError::Other(msg) if msg.contains("file missing")));
168 }
169
170 #[test]
173 fn retry_hint_new() {
174 let hint = RetryHint::new("reason");
175 assert_eq!(hint.reason, "reason");
176 }
177}