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:
+43
-47
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user