use crate::{
error::Result,
exec_ctx::ExecCtx,
payload::{BoxFut, Payload, PayloadOutput},
PipelineError,
};
use serde_json::Value;
pub struct Chain {
name: String,
payloads: Vec<Box<dyn Payload>>,
}
impl Chain {
pub fn new(name: impl Into<String>) -> Self {
Self {
name: name.into(),
payloads: Vec::new(),
}
}
pub fn push(mut self, payload: Box<dyn Payload>) -> Self {
self.payloads.push(payload);
self
}
pub fn add(&mut self, payload: Box<dyn Payload>) {
self.payloads.push(payload);
}
pub fn len(&self) -> usize {
self.payloads.len()
}
pub fn is_empty(&self) -> bool {
self.payloads.is_empty()
}
pub async fn execute_all(&self, ctx: &ExecCtx, input: Value) -> Result<Vec<PayloadOutput>> {
if self.payloads.is_empty() {
return Err(PipelineError::InvalidConfig(
"Chain has no payloads".to_string(),
));
}
let mut outputs = Vec::with_capacity(self.payloads.len());
let mut current = input;
for payload in &self.payloads {
ctx.check_cancelled()?;
let output = payload.invoke(ctx, current).await?;
current = output.value.clone();
outputs.push(output);
}
Ok(outputs)
}
pub async fn execute(&self, ctx: &ExecCtx, input: Value) -> Result<PayloadOutput> {
let outputs = self.execute_all(ctx, input).await?;
outputs
.into_iter()
.last()
.ok_or_else(|| PipelineError::Other("Chain produced no outputs".to_string()))
}
}
impl Payload for Chain {
fn kind(&self) -> &'static str {
"chain"
}
fn name(&self) -> &str {
&self.name
}
fn invoke<'a>(&'a self, ctx: &'a ExecCtx, input: Value) -> BoxFut<'a, Result<PayloadOutput>> {
Box::pin(self.execute(ctx, input))
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
struct EchoPayload {
tag: String,
}
impl Payload for EchoPayload {
fn kind(&self) -> &'static str {
"echo"
}
fn name(&self) -> &str {
&self.tag
}
fn invoke<'a>(
&'a self,
_ctx: &'a ExecCtx,
input: Value,
) -> BoxFut<'a, Result<PayloadOutput>> {
let tag = self.tag.clone();
Box::pin(async move {
let value = json!({ "from": tag, "input": input });
Ok(PayloadOutput::from_value(value))
})
}
}
fn test_ctx() -> ExecCtx {
ExecCtx::builder("http://test").build()
}
#[tokio::test]
async fn test_chain_single_payload() {
let chain = Chain::new("test").push(Box::new(EchoPayload { tag: "a".into() }));
let out = chain.execute(&test_ctx(), json!("hello")).await.unwrap();
assert_eq!(out.value["from"], "a");
assert_eq!(out.value["input"], "hello");
}
#[tokio::test]
async fn test_chain_pipes_output() {
let chain = Chain::new("test")
.push(Box::new(EchoPayload {
tag: "first".into(),
}))
.push(Box::new(EchoPayload {
tag: "second".into(),
}));
let out = chain.execute(&test_ctx(), json!("start")).await.unwrap();
assert_eq!(out.value["from"], "second");
assert_eq!(out.value["input"]["from"], "first");
}
#[tokio::test]
async fn test_chain_execute_all() {
let chain = Chain::new("test")
.push(Box::new(EchoPayload { tag: "a".into() }))
.push(Box::new(EchoPayload { tag: "b".into() }));
let outputs = chain.execute_all(&test_ctx(), json!("x")).await.unwrap();
assert_eq!(outputs.len(), 2);
assert_eq!(outputs[0].value["from"], "a");
assert_eq!(outputs[1].value["from"], "b");
}
#[tokio::test]
async fn test_chain_empty_fails() {
let chain = Chain::new("empty");
let result = chain.execute(&test_ctx(), json!(null)).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_chain_cancellation() {
let cancel = Arc::new(AtomicBool::new(true));
let ctx = ExecCtx::builder("http://test")
.cancellation(Some(cancel))
.build();
let chain = Chain::new("test").push(Box::new(EchoPayload { tag: "a".into() }));
let result = chain.execute(&ctx, json!("x")).await;
assert!(result.is_err());
}
#[tokio::test]
async fn test_chain_as_payload() {
let inner = Chain::new("inner").push(Box::new(EchoPayload {
tag: "inner-step".into(),
}));
let outer = Chain::new("outer").push(Box::new(inner));
let out = outer.execute(&test_ctx(), json!("input")).await.unwrap();
assert_eq!(out.value["from"], "inner-step");
}
}