pipe-io 1.0.0

Typed source-transform-sink pipelines with backpressure, batching, windowing, and per-stage error isolation. A lightweight runtime-agnostic stream processor for in-process workloads. The missing middle ground between raw iterators and full distributed stream processing.
Documentation
# pipe-io v0.7.0 - Driver trait and custom executors

**Date:** 2026-05-19
**Compare:** `v0.6.0...v0.7.0`

## Headline

Lands the `Driver` trait that was deferred at `0.3.0`. Closes the
**last** of the three design-lock deferrals: every surface listed
in the locked `REPS.md` section 4 is now shipped except for
`PipelineBuilder::buffer(capacity)`, which is roadmap material
rather than a deferral.

The trait is generic, `Send`-bounded, and *not* sealed. External
executors (tokio runtime, rayon pool, custom thread farm) can
implement it. The built-in drivers also implement it, so
`Pipeline::run_with(SyncDriver::new())` and
`Pipeline::run_with(ThreadedDriver::new())` work alongside any
user impl.

## What changed since `v0.6.0`

### New - `pub trait Driver`

```rust
pub trait Driver {
    fn run<S>(self, pipeline: Pipeline<S>) -> Result<RunStats>
    where
        S: Source + Send + 'static,
        S::Item: Send + 'static,
        S::Error: Send + 'static;
}
```

The trait carries the stricter `Send` bound (matching the natural
requirements of `ThreadedDriver` and any threaded executor). Any
type that implements `Driver` can be handed to a pipeline via
`Pipeline::run_with`. The trait is open: third-party crates can
implement it without coordinating with this crate.

Re-exported at the crate root as `pipe_io::Driver`.

### New - `Pipeline::run_with`

```rust
impl<S: Source> Pipeline<S> {
    pub fn run_with<D>(self, driver: D) -> Result<RunStats>
    where
        D: Driver,
        S: Send,
        S::Item: Send,
        S::Error: Send;
}
```

Builder-terminal method. Use it to be explicit about driver
choice (`pipeline.run_with(ThreadedDriver::new())`) or to plug
in any custom executor. The `Send` bounds on `S`, `S::Item`, and
`S::Error` match the trait's signature.

### Changed - `SyncDriver::run` keeps its looser bound

The inherent `SyncDriver::run` method does *not* require `Send`
on the source. Use it directly when driving a non-`Send` source
on the calling thread:

```rust
SyncDriver::new().run(pipeline)?;       // looser bound, inherent method
pipeline.run_with(SyncDriver::new())?;  // tighter bound, trait dispatch
```

Both compile to the same call when the `Send` bounds hold; the
inherent method is the escape hatch when they don't.

### Changed - `REPS.md` section 4.8 un-defers the trait

The 4.8 section that said "a trait-based driver abstraction is
deferred past `0.3.0`" now documents the shipped trait. Section
4.9 records `.run_with(driver)` on the builder surface.

### New - Tests

`tests/driver_trait.rs` adds 6 integration tests:

- `sync_driver_through_trait` - `SyncDriver` invoked through the
  `Driver` trait.
- `threaded_driver_through_trait` - `ThreadedDriver` through the
  trait.
- `pipeline_run_with_sync` - `Pipeline::run_with(SyncDriver)`.
- `pipeline_run_with_threaded` - `Pipeline::run_with(ThreadedDriver)`.
- `custom_driver_impl_works` - a `CountingDriver` external impl;
  asserts both invocation count and pipeline output.
- `driver_trait_is_object_safe_for_owned_drivers` - static check
  that the trait accepts generic dispatch over the built-ins and
  custom impls.

## Public surface

Crate root adds:

```text
pipe_io::Driver
```

`pipe_io::driver` exposes:

```text
pipe_io::driver::Driver         (new)
pipe_io::driver::SyncDriver     (now also impls Driver)
pipe_io::driver::ThreadedDriver (std; now also impls Driver)
pipe_io::driver::RunStats
```

`Pipeline` gains:

```text
pipeline.run_with<D: Driver>(driver: D) -> Result<RunStats>
```

### MSRV

`1.75`, unchanged.

### Runtime dependencies

Zero, unchanged.

## Why these bounds

A unified `Driver` trait has to commit to one set of `Send` bounds
on the source. Two options were considered:

1. **Looser bound (no `Send`).** Then `ThreadedDriver` cannot
   implement `Driver` because its impl can't add the `Send`
   bound that `std::thread::spawn` requires.
2. **Tighter bound (`Send` on `S`, `S::Item`, `S::Error`).**
   Both built-in drivers implement it. The cost is that
   non-`Send` sources cannot use the trait.

`0.7.0` takes option (2) and pairs it with the `SyncDriver::run`
inherent method as an escape hatch for non-`Send` sources. The
trade-off matches the way the crate is actually used: every
built-in source and sink is `Send`, and most user code follows
suit. The rare non-`Send` source still has a clean path through
`SyncDriver::run`.

## Verification

Local matrix (Windows x86_64-pc-windows-msvc, stable 1.95):

- `cargo build` (default features) [OK]
- `cargo build --no-default-features` [OK]
- `cargo build --all-features` [OK]
- `cargo +1.75 build --all-features` (MSRV) [OK]
- `cargo fmt --all -- --check` [OK]
- `cargo clippy --all-targets --no-default-features -- -D warnings` [OK]
- `cargo clippy --all-targets --all-features -- -D warnings` [OK]
- `cargo test --all-features` - **73 tests pass, 0 ignored**
  (32 unit + 31 integration + 10 doctest)
- `RUSTDOCFLAGS="-D warnings -D rustdoc::broken-intra-doc-links" cargo doc --all-features --no-deps` [OK]
- Banned-word and em-dash scan across `src/`, `tests/`, `benches/`,
  `docs/`, `README.md`, `CHANGELOG.md`, `REPS.md`: 0 hits

CI matrix continues to run on ubuntu-latest, macos-latest,
windows-latest.

## Migration

### From `v0.6.0`

No breaking changes. The trait and `run_with` method are pure
additions. Existing code using `.run()`, `.run_threaded()`,
`SyncDriver::new().run(p)`, or `ThreadedDriver::new().run(p)`
continues to compile and behave identically.

To use a custom driver:

```rust
use pipe_io::driver::Driver;
use pipe_io::{sink::NullSink, Pipeline, Result};

struct MyDriver;

impl Driver for MyDriver {
    fn run<S>(self, pipeline: Pipeline<S>) -> Result<pipe_io::RunStats>
    where
        S: pipe_io::Source + Send + 'static,
        S::Item: Send + 'static,
        S::Error: Send + 'static,
    {
        // pre-flight setup ...
        let result = pipe_io::driver::SyncDriver::new().run(pipeline);
        // post-run cleanup ...
        result
    }
}

Pipeline::from_iter(0..10).sink(NullSink::<i32>::new()).run_with(MyDriver)?;
```

A future companion crate (e.g. `pipe-io-tokio`) can ship a
tokio-backed `Driver` without coordinating with this crate beyond
the trait dependency.

## Stability commitment

Unchanged. `0.x.y` releases are not API-stable; API freeze begins
at `1.0.0`. After `0.7.0`, the locked `REPS.md` surface is
**fully implemented** except for `PipelineBuilder::buffer(capacity)`,
which was listed in section 4.9 but is roadmap material rather
than a deferral. Next stops: the bridge release `0.8.0`
(examples and a guide; populates `examples/` and adds
`docs/GUIDE.md`), then `0.9.0` pre-1.0 stabilization (property
tests, fuzz targets, `cargo-semver-checks` in CI, doc audit,
migration guide).

## Release ceremony

```bash
git tag -a v0.7.0 -m "Release v0.7.0 - Driver trait and custom executors"
git push origin main
git push origin v0.7.0

cargo publish

cargo search pipe-io | head -3
```

GitHub release title: `v0.7.0 - Driver trait and custom executors`.
Not tagged as pre-release.

## Acknowledgements

`pipe-io 0.7.0` closes the last design-lock deferral. The trait
shape that I documented as "deferred" at `0.3.0` landed exactly
as described - generic, `Send`-bounded, not sealed, alongside an
inherent `SyncDriver::run` for the non-`Send` escape hatch. The
companion-crate door is now open: a `pipe-io-tokio` adapter
needs only the `Driver` trait, not any internal collaboration
with this crate.

Next: `v0.8.0` - Examples and guide. After that, `v0.9.0`
pre-1.0 stabilization.

---

**Full Changelog:** https://github.com/jamesgober/pipe-io/compare/v0.6.0...v0.7.0