Simplify printer/ module for someone learning Rust

Drop reqwest, RPITIT+Send signature, and multi-variant error enum from the
teaching example so the only new idea per file is the trait override itself.
Real networking stays in adapters/, unaffected.
This commit is contained in:
2026-08-28 18:04:13 +00:00
parent 9b3815fd25
commit 5705f36c01
5 changed files with 79 additions and 129 deletions
+43 -47
View File
@@ -1,22 +1,25 @@
//! Printer hierarchy — the Rust answer to
//! `GenericPrinter -> GenericBambuPrinter -> BambuPrinterV1/V2`-style
//! inheritance.
//! `GenericPrinter -> BambuPrinter/PrusaPrinter/KlipperPrinter`-style
//! inheritance, since Rust structs can't `extend` each other.
//!
//! Rust structs can't `extend` each other, so instead of an inheritance
//! chain we use three separate, composable mechanisms:
//! Three pieces, each doing one job an OOP base class would normally do:
//!
//! 1. **Composition** (`PrinterBase` as a field) for shared *data* — stands
//! in for a base class's fields.
//! 2. **A trait** (`Printer`) for shared *behavior* — stands in for a base
//! class's methods. A method with no default body (like `connect`) must
//! be supplied by every implementor: that's what "overriding" looks like
//! here, since there's no inherited body to override in the first place.
//! 3. **An enum** (`PrinterHandle`) for the closed set of concrete printer
//! kinds — stands in for "any subclass of GenericPrinter". Matching on it
//! is exhaustive, so the compiler forces every call site to handle a new
//! vendor when one is added.
//! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every
//! printer struct below *has* one of these (composition) instead of
//! *extending* one.
//! 2. `Printer` — a trait holding the shared *behavior*. `connect()` has no
//! default body, so every printer type is forced to write its own —
//! that's what "overriding" looks like when there's no inherited body to
//! override in the first place.
//! 3. `PrinterHandle` — an enum listing the closed set of printer kinds.
//! Stands in for "any subclass of GenericPrinter".
//!
//! See `examples/printer_polymorphism.rs` for this in action.
//! Run `cargo run --example printer_polymorphism` to see it in action.
//!
//! Deliberately left out of this module for now, to keep it focused on one
//! idea at a time: real MQTT/HTTP/FTPS calls (that's `src/adapters/`) and
//! `Send`-bound futures (matters once this is wired into a multi-threaded
//! `tokio::spawn`, not yet).
mod bambu;
mod klipper;
@@ -26,18 +29,13 @@ pub use bambu::BambuPrinter;
pub use klipper::KlipperPrinter;
pub use prusa::PrusaPrinter;
use thiserror::Error;
/// A tiny error type: just a message. `String` would work too — this exists
/// mainly so `?` has something concrete to convert into everywhere.
#[derive(Debug, thiserror::Error)]
#[error("{0}")]
pub struct PrinterError(pub String);
#[derive(Debug, Error)]
pub enum PrinterError {
#[error("connection to {0} failed: {1}")]
Connection(String, String),
#[error("{0} does not support this operation")]
Unsupported(&'static str),
}
/// Fields every printer has, regardless of vendor. Every concrete printer
/// struct holds one of these as a field rather than "inheriting" it.
/// Fields every printer has, regardless of vendor.
#[derive(Debug, Clone)]
pub struct PrinterBase {
pub id: String,
@@ -48,42 +46,40 @@ pub struct PrinterBase {
/// The shared interface — think `interface Printer` (Java/TS) or an ABC
/// (Python). Every vendor's struct implements this.
pub trait Printer {
/// Read-only access to the shared fields (the "inherited" data).
/// Read-only access to the shared fields.
fn base(&self) -> &PrinterBase;
/// REQUIRED, no default body: every printer vendor speaks a different
/// protocol, so there's nothing sensible to share here. Each `impl`
/// below provides a completely different body — that's the "override".
/// REQUIRED, no default body: every vendor speaks a different protocol,
/// so there's nothing sensible to share here. Each `impl` in
/// `bambu.rs`/`prusa.rs`/`klipper.rs` provides a totally different
/// body — that's the override.
///
/// Spelled as `-> impl Future<..> + Send` rather than plain `async fn`
/// so the resulting future is `Send` and can be `.await`ed inside a
/// `tokio::spawn`ed task; each `impl` below still just writes a normal
/// `async fn` body, which satisfies this signature automatically.
fn connect(&mut self) -> impl std::future::Future<Output = Result<(), PrinterError>> + Send;
/// `cargo check` will warn about `async fn` in a public trait here.
/// That's about a more advanced concern (whether the resulting work can
/// safely move across OS threads) that only matters once this is wired
/// into the multi-threaded daemon — safe to ignore for now.
async fn connect(&mut self) -> Result<(), PrinterError>;
/// OPTIONAL: a default body every printer gets for free unless it
/// chooses to provide its own. None of ours do, so all three vendors
/// use this exact implementation — this is the trait-method equivalent
/// of inheriting a base-class method unchanged.
/// writes its own. None of ours do below, so all three vendors share
/// this exact implementation — the trait-method equivalent of
/// inheriting a base-class method unchanged.
fn display_name(&self) -> String {
format!("{} ({}) @ {}", self.base().name, self.base().id, self.base().host)
format!("{} ({})", self.base().name, self.base().id)
}
}
/// The closed set of printer kinds this fleet can talk to — stands in for
/// "any subclass of GenericPrinter" without needing a `Box<dyn Printer>`.
///
/// The `impl Printer for PrinterHandle` below is hand-written delegation:
/// each trait method just matches on the variant and forwards to that
/// variant's own implementation. This is exactly the boilerplate a crate
/// like `enum_dispatch` would generate for you — written out by hand here
/// so the mechanism is visible rather than hidden behind a macro.
/// The closed set of printer kinds this fleet can talk to.
pub enum PrinterHandle {
Bambu(BambuPrinter),
Prusa(PrusaPrinter),
Klipper(KlipperPrinter),
}
// Hand-written delegation: each trait method just matches on the variant
// and forwards to that variant's own implementation. This one block is the
// only "boilerplate tax" for not having inheritance — everything else reads
// like normal code.
impl Printer for PrinterHandle {
fn base(&self) -> &PrinterBase {
match self {