pub mod stages;
pub mod standard;
use std::fmt;
#[derive(Debug, Clone, PartialEq)]
pub enum TransformError {
Error(String),
StageFailed { stage: String, message: String },
}
impl fmt::Display for TransformError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
TransformError::Error(msg) => write!(f, "{msg}"),
TransformError::StageFailed { stage, message } => {
write!(f, "Stage '{stage}' failed: {message}")
}
}
}
}
impl std::error::Error for TransformError {}
impl From<String> for TransformError {
fn from(s: String) -> Self {
TransformError::Error(s)
}
}
impl From<&str> for TransformError {
fn from(s: &str) -> Self {
TransformError::Error(s.to_string())
}
}
pub trait Runnable<I, O> {
fn run(&self, input: I) -> Result<O, TransformError>;
}
pub struct Transform<I, O> {
run_fn: Box<dyn Fn(I) -> Result<O, TransformError> + Send + Sync>,
}
impl<I, O> Transform<I, O> {
pub fn identity() -> Self
where
I: Clone + 'static,
O: From<I> + 'static,
{
Transform {
run_fn: Box::new(|input| Ok(O::from(input))),
}
}
pub fn from_fn<F>(f: F) -> Self
where
F: Fn(I) -> Result<O, TransformError> + Send + Sync + 'static,
{
Transform {
run_fn: Box::new(f),
}
}
pub fn then<O2, S>(self, stage: S) -> Transform<I, O2>
where
S: Runnable<O, O2> + Send + Sync + 'static,
I: 'static,
O: 'static,
O2: 'static,
{
let prev_run = self.run_fn;
Transform {
run_fn: Box::new(move |input| {
let intermediate = prev_run(input)?;
stage.run(intermediate)
}),
}
}
pub fn then_transform<O2>(self, next: &'static Transform<O, O2>) -> Transform<I, O2>
where
I: 'static,
O: 'static,
O2: 'static,
{
let prev_run = self.run_fn;
Transform {
run_fn: Box::new(move |input| {
let intermediate = prev_run(input)?;
next.run(intermediate)
}),
}
}
pub fn run(&self, input: I) -> Result<O, TransformError> {
(self.run_fn)(input)
}
}
impl<I, O> Runnable<I, O> for Transform<I, O>
where
I: 'static,
O: 'static,
{
fn run(&self, input: I) -> Result<O, TransformError> {
Transform::run(self, input)
}
}
impl<I, O> Transform<I, O> {
pub fn new<F>(f: F) -> Self
where
F: Fn(I) -> Result<O, TransformError> + Send + Sync + 'static,
{
Transform::from_fn(f)
}
}
#[cfg(test)]
mod tests {
use super::*;
struct DoubleNumber;
impl Runnable<i32, i32> for DoubleNumber {
fn run(&self, input: i32) -> Result<i32, TransformError> {
Ok(input * 2)
}
}
struct AddTen;
impl Runnable<i32, i32> for AddTen {
fn run(&self, input: i32) -> Result<i32, TransformError> {
Ok(input + 10)
}
}
struct IntToString;
impl Runnable<i32, String> for IntToString {
fn run(&self, input: i32) -> Result<String, TransformError> {
Ok(input.to_string())
}
}
struct FailingStage;
impl Runnable<i32, i32> for FailingStage {
fn run(&self, _input: i32) -> Result<i32, TransformError> {
Err(TransformError::Error("intentional failure".to_string()))
}
}
#[test]
fn test_transform_from_fn() {
let transform = Transform::from_fn(|x: i32| Ok(x * 2));
assert_eq!(transform.run(5).unwrap(), 10);
}
#[test]
fn test_single_stage() {
let transform = Transform::from_fn(|x: i32| Ok(x)).then(DoubleNumber);
assert_eq!(transform.run(5).unwrap(), 10);
}
#[test]
fn test_multiple_same_type_stages() {
let transform = Transform::from_fn(|x: i32| Ok(x))
.then(DoubleNumber)
.then(AddTen)
.then(DoubleNumber);
assert_eq!(transform.run(5).unwrap(), 40);
}
#[test]
fn test_type_changing_stage() {
let transform = Transform::from_fn(|x: i32| Ok(x))
.then(DoubleNumber)
.then(IntToString);
assert_eq!(transform.run(5).unwrap(), "10");
}
#[test]
fn test_error_propagation() {
let transform = Transform::from_fn(|x: i32| Ok(x))
.then(DoubleNumber)
.then(FailingStage)
.then(AddTen);
let result = transform.run(5);
assert!(result.is_err());
assert_eq!(
result.unwrap_err(),
TransformError::Error("intentional failure".to_string())
);
}
#[test]
fn test_transform_composition() {
let double_and_add = Transform::from_fn(|x: i32| Ok(x))
.then(DoubleNumber)
.then(AddTen);
let to_string = Transform::from_fn(|x: i32| Ok(x)).then(IntToString);
assert_eq!(double_and_add.run(5).unwrap(), 20);
assert_eq!(to_string.run(5).unwrap(), "5");
}
#[test]
fn test_error_display() {
let err = TransformError::Error("test error".to_string());
assert_eq!(format!("{err}"), "test error");
let stage_err = TransformError::StageFailed {
stage: "tokenization".to_string(),
message: "invalid token".to_string(),
};
assert_eq!(
format!("{stage_err}"),
"Stage 'tokenization' failed: invalid token"
);
}
#[test]
fn test_error_conversion() {
let err1: TransformError = "string error".into();
assert_eq!(err1, TransformError::Error("string error".to_string()));
let err2: TransformError = "owned string".to_string().into();
assert_eq!(err2, TransformError::Error("owned string".to_string()));
}
}