use std::{any::TypeId, pin::Pin};
use thiserror::Error;
use crate::{FromTypeMap, TypeMap, prelude::*};
#[derive(Error, Debug)]
pub enum Error {
#[error("failed to resolve at least one dependency in step '{0}'")]
DepResolution(String),
#[error("failed to add a dependency of type '{0:?}' as it was already present")]
AddDep(TypeId),
#[error("step '{0}' failed to execute: {1}")]
Step(String, Box<dyn std::error::Error + Send + Sync>),
#[error("step '{0}' returned a fatal outcome without error")]
UnknownStep(String),
}
type Result<T> = std::result::Result<T, Error>;
#[must_use]
pub fn new<O>() -> ImperativeStepBuilder<O> {
ImperativeStepBuilder::<O>::default()
}
struct Step<O> {
#[allow(dead_code)]
name: String,
fut: Pin<Box<dyn Future<Output = O>>>,
}
pub struct ImperativeStepBuilder<O> {
tm: TypeMap,
steps: Vec<Step<O>>,
errors: Vec<Error>,
}
#[allow(clippy::missing_fields_in_debug)]
impl<O> std::fmt::Debug for ImperativeStepBuilder<O> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("ImperativeStepBuilder")
.field("tm", &self.tm)
.field("errors", &self.errors)
.finish()
}
}
impl<O> Default for ImperativeStepBuilder<O> {
fn default() -> Self {
ImperativeStepBuilder::<O> {
tm: TypeMap::default(),
steps: Vec::default(),
errors: Vec::default(),
}
}
}
impl<O: IntoStepOutcome + 'static> ImperativeStepBuilder<O> {
#[must_use]
pub fn add_step<C: Callable<A, Out = O> + 'static, A: FromTypeMap>(
mut self,
name: &str,
func: C,
) -> Self {
let Some(args) = A::retrieve_from_map(&self.tm) else {
eprintln!("will not run step '{name}' as at least one dependency was absent");
self.errors.push(Error::DepResolution(name.to_string()));
return self;
};
self.steps.push(Step {
name: name.to_string(),
fut: Box::pin(func.call(args)),
});
self
}
#[must_use]
pub fn add_dep<T: 'static>(mut self, dep: T) -> Self {
if self.tm.get::<T>().is_some() {
self.errors.push(Error::AddDep(TypeId::of::<T>()));
return self;
}
self.tm.bind(dep);
self
}
pub async fn execute(mut self) -> Result<Vec<O>> {
if let Some(e) = self.errors.pop() {
return Err(e);
}
let mut outputs = Vec::with_capacity(self.steps.len());
for step in self.steps {
let r = step.fut.await;
if r.success() {
outputs.push(r);
} else if let Some(e) = r.error() {
return Err(Error::Step(step.name, e));
} else {
return Err(Error::UnknownStep(step.name));
}
}
Ok(outputs)
}
}
pub trait IntoStepOutcome {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>>;
fn success(&self) -> bool;
}
impl IntoStepOutcome for std::io::Error {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
Some(Box::new(self))
}
fn success(&self) -> bool {
false
}
}
impl IntoStepOutcome for Box<dyn std::error::Error + Send + Sync> {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
Some(self)
}
fn success(&self) -> bool {
false
}
}
impl IntoStepOutcome for bool {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
None
}
fn success(&self) -> bool {
*self
}
}
#[cfg(feature = "anyhow")]
impl IntoStepOutcome for anyhow::Error {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
Some(self.into())
}
fn success(&self) -> bool {
false
}
}
impl<T, E: IntoStepOutcome + Into<Box<dyn std::error::Error + Send + Sync>>> IntoStepOutcome
for std::result::Result<T, E>
{
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
if self.is_err() {
self.err().map(Into::into)
} else {
None
}
}
fn success(&self) -> bool {
self.is_ok()
}
}
macro_rules! impl_into_step_outcome {
($($typ:ty)*) => {
$(
impl IntoStepOutcome for $typ {
fn error(self) -> Option<Box<dyn std::error::Error + Send + Sync>> {
None
}
fn success(&self) -> bool {
true
}
}
)*
};
}
impl_into_step_outcome!(
() usize isize char &str String u8 i8 i16 u16 i32 u32
i64 u64 i128 u128 f32 f64
);