//! Printer hierarchy — the Rust answer to //! `GenericPrinter -> BambuV1/BambuV2/PrusaLink/PrusaSerial/Klipper`-style //! inheritance, since Rust structs can't `extend` each other. //! //! Three pieces, each doing one job an OOP base class would normally do: //! //! 1. `PrinterBase` — a plain struct holding the shared *fields*. Every //! printer struct below *has* one of these (composition) instead of //! *extending* one. //! 2. `GenericPrinter` — 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". //! //! 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; mod prusa; pub use bambu::{BambuPrinter, BambuTls}; pub use klipper::KlipperPrinter; pub use prusa::{PrusaLinkPrinter, PrusaSerialPrinter}; /// 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); /// Fields every printer has, regardless of vendor. #[derive(Debug, Clone)] pub struct PrinterBase { pub id: String, pub name: String, pub host: String, } /// The shared interface — think `interface GenericPrinter` (Java/TS) or an /// ABC (Python). Every vendor's struct implements this. pub trait GenericPrinter { /// Read-only access to the shared fields. fn base(&self) -> &PrinterBase; /// 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. /// /// `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 /// 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) } } pub enum PrinterFlavour { BambuV1Printer, BambuV2Printer, PrusaLinkPrinter, PrusaSerialPrinter, KlipperPrinter, } /// The closed set of printer kinds this fleet can talk to. pub enum PrinterHandle { BambuV1(BambuPrinter), BambuV2(BambuPrinter), PrusaLink(PrusaLinkPrinter), PrusaSerial(PrusaSerialPrinter), 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 GenericPrinter for PrinterHandle { fn base(&self) -> &PrinterBase { match self { PrinterHandle::BambuV1(p) => p.base(), PrinterHandle::BambuV2(p) => p.base(), PrinterHandle::PrusaLink(p) => p.base(), PrinterHandle::PrusaSerial(p) => p.base(), PrinterHandle::Klipper(p) => p.base(), } } async fn connect(&mut self) -> Result<(), PrinterError> { match self { PrinterHandle::BambuV1(p) => p.connect().await, PrinterHandle::BambuV2(p) => p.connect().await, PrinterHandle::PrusaLink(p) => p.connect().await, PrinterHandle::PrusaSerial(p) => p.connect().await, PrinterHandle::Klipper(p) => p.connect().await, } } }