use crate::ctx::Ctx;
use std::fmt;
pub type StepResult<S> = Result<(S, Outcome), StepError>;
pub trait Agent<S>: Send + 'static {
fn name(&self) -> &'static str;
fn run(&mut self, state: S, ctx: &mut Ctx) -> StepResult<S>;
}
#[derive(Debug, Clone)]
pub enum Outcome {
Continue,
Done,
Next(&'static str),
Retry(RetryHint),
Wait(std::time::Duration),
Fail(String),
}
#[derive(Debug, Clone)]
pub struct RetryHint {
pub reason: String,
}
impl RetryHint {
pub fn new(reason: impl Into<String>) -> Self {
Self {
reason: reason.into(),
}
}
}
#[derive(Debug)]
pub enum StepError {
Invalid(String),
Transient(String),
Failed(String),
Other(String),
}
impl From<ureq::Error> for StepError {
fn from(e: ureq::Error) -> Self {
StepError::Transient(e.to_string())
}
}
impl From<std::io::Error> for StepError {
fn from(e: std::io::Error) -> Self {
StepError::Other(e.to_string())
}
}
impl StepError {
pub fn invalid(msg: impl Into<String>) -> Self {
StepError::Invalid(msg.into())
}
pub fn other(msg: impl Into<String>) -> Self {
StepError::Other(msg.into())
}
pub fn transient(msg: impl Into<String>) -> Self {
StepError::Transient(msg.into())
}
}
impl fmt::Display for StepError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::Invalid(msg) => write!(f, "invalid: {msg}"),
Self::Other(msg) => write!(f, "{msg}"),
Self::Transient(msg) => write!(f, "transient: {msg}"),
Self::Failed(msg) => write!(f, "failed: {msg}"),
}
}
}
impl std::error::Error for StepError {}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn invalid_constructor() {
let err = StepError::invalid("bad input");
assert!(matches!(err, StepError::Invalid(msg) if msg == "bad input"));
}
#[test]
fn other_constructor() {
let err = StepError::other("something");
assert!(matches!(err, StepError::Other(msg) if msg == "something"));
}
#[test]
fn transient_constructor() {
let err = StepError::transient("timeout");
assert!(matches!(err, StepError::Transient(msg) if msg == "timeout"));
}
#[test]
fn display_invalid() {
let err = StepError::Invalid("bad input".into());
assert_eq!(err.to_string(), "invalid: bad input");
}
#[test]
fn display_other() {
let err = StepError::Other("something".into());
assert_eq!(err.to_string(), "something");
}
#[test]
fn display_transient() {
let err = StepError::Transient("timeout".into());
assert_eq!(err.to_string(), "transient: timeout");
}
#[test]
fn display_failed() {
let err = StepError::Failed("nope".into());
assert_eq!(err.to_string(), "failed: nope");
}
#[test]
fn from_io_error() {
let io_err = std::io::Error::new(std::io::ErrorKind::NotFound, "file missing");
let step_err: StepError = io_err.into();
assert!(matches!(step_err, StepError::Other(msg) if msg.contains("file missing")));
}
#[test]
fn retry_hint_new() {
let hint = RetryHint::new("reason");
assert_eq!(hint.reason, "reason");
}
}