# 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
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