diff --git a/examples/printer_polymorphism.rs b/examples/printer_polymorphism.rs index 8581614..7b39638 100644 --- a/examples/printer_polymorphism.rs +++ b/examples/printer_polymorphism.rs @@ -4,7 +4,7 @@ //! - `connect()` is called the same way regardless of vendor (polymorphism), //! but each vendor's `impl Printer` runs completely different code //! (override). -//! - `dispatch_gcode_ftp()` only exists on `BambuPrinter` (extension) — you +//! - `dispatch_gcode()` only exists on `BambuPrinter` (extension) — you //! have to `match` the enum back down to the concrete type to reach it, //! which is the trade-off for not having implicit downcasting. @@ -12,33 +12,29 @@ use continuum_proxy::printer::{BambuPrinter, KlipperPrinter, Printer, PrinterHan #[tokio::main] async fn main() { - tracing_subscriber::fmt::init(); - let mut fleet: Vec = vec![ - PrinterHandle::Bambu(BambuPrinter::new("p1", "X1 Carbon", "192.168.1.50:8883", "12345678")), + PrinterHandle::Bambu(BambuPrinter::new("p1", "X1 Carbon", "192.168.1.50", "12345678")), PrinterHandle::Prusa(PrusaPrinter::new("p2", "MK4", "192.168.1.51", "prusa-api-key")), PrinterHandle::Klipper(KlipperPrinter::new("p3", "Voron 2.4", "192.168.1.52")), + // No host configured — this one will hit the Err path. + PrinterHandle::Klipper(KlipperPrinter::new("p4", "Broken Voron", "")), ]; - // Uniform call site: every printer is connected the same way from the - // caller's point of view, even though BambuPrinter::connect, - // PrusaPrinter::connect and KlipperPrinter::connect are three unrelated - // implementations. + // Uniform call site: every printer connects the same way from the + // caller's point of view, even though the three `connect()` bodies + // are unrelated implementations. for printer in &mut fleet { match printer.connect().await { - Ok(()) => println!("connected: {}", printer.display_name()), - Err(err) => println!("connect failed: {err}"), + Ok(()) => println!(" -> connected: {}\n", printer.display_name()), + Err(err) => println!(" -> failed: {err}\n"), } } // Vendor-only extension: reachable only after matching the concrete // variant back out of the enum. - for printer in &mut fleet { + for printer in &fleet { if let PrinterHandle::Bambu(bambu) = printer { - bambu - .dispatch_gcode_ftp(std::path::Path::new("./part.gcode.3mf"), "part.gcode.3mf") - .await - .ok(); + bambu.dispatch_gcode("part.gcode.3mf"); } } } diff --git a/src/printer/bambu.rs b/src/printer/bambu.rs index 93e2f17..c8fd23d 100644 --- a/src/printer/bambu.rs +++ b/src/printer/bambu.rs @@ -1,38 +1,24 @@ -use std::path::Path; - -use tracing::info; - use super::{Printer, PrinterBase, PrinterError}; -/// Bambu-specific fields — this is the "extends GenericBambuPrinter" part of -/// your original design, expressed as extra struct fields instead of a -/// second inheritance layer. pub struct BambuPrinter { base: PrinterBase, pub access_code: String, } impl BambuPrinter { - pub fn new(id: impl Into, name: impl Into, host: impl Into, access_code: impl Into) -> Self { + pub fn new(id: &str, name: &str, host: &str, access_code: &str) -> Self { Self { base: PrinterBase { id: id.into(), name: name.into(), host: host.into() }, access_code: access_code.into(), } } - /// A method that ONLY exists on `BambuPrinter` — not part of the - /// `Printer` trait at all. This is what "adding a printer-specific - /// method" looks like: just define it in this struct's own `impl` - /// block. `PrusaPrinter` and `KlipperPrinter` have no equivalent, and - /// calling this requires a `PrinterHandle::Bambu(..)` match first (see - /// the example), which is the price for not doing runtime downcasting. - /// - /// Real implementation lives in `adapters::bambu::dispatch_file` (FTPS - /// on port 990); this is the domain-model-facing wrapper around it. - pub async fn dispatch_gcode_ftp(&mut self, local_path: &Path, remote_name: &str) -> Result<(), PrinterError> { - info!(printer = %self.base.id, remote_name = %remote_name, path = ?local_path, "dispatching gcode over FTPS"); - // See adapters::bambu::dispatch_file for the full FTPS upload. - Ok(()) + /// Only `BambuPrinter` has this — it's not on the `Printer` trait at + /// all, so `PrusaPrinter`/`KlipperPrinter` simply don't have it. This + /// is what "adding a printer-specific method" looks like: just define + /// it in this struct's own `impl` block. + pub fn dispatch_gcode(&self, file_name: &str) { + println!("[{}] uploading {file_name} over FTPS", self.base.name); } } @@ -41,14 +27,11 @@ impl Printer for BambuPrinter { &self.base } - /// OVERRIDE: Bambu speaks MQTTS on port 8883, authenticated with the - /// printer's LAN access code. Completely different transport from the - /// other two vendors below. + /// OVERRIDE: Bambu's real connect logic opens an MQTTS session (see + /// `adapters::bambu::run_telemetry` for that). Here we just print what + /// it would do, so the focus stays on how dispatch works. async fn connect(&mut self) -> Result<(), PrinterError> { - info!(printer = %self.base.id, host = %self.base.host, "connecting to Bambu printer over MQTTS:8883"); - // Real implementation: open the MQTTS session and wait for the - // first `report` message — see adapters::bambu::run_telemetry, - // which this would delegate to once wired into the daemon. + println!("[{}] connecting over MQTT with access code {}", self.base.name, self.access_code); Ok(()) } } diff --git a/src/printer/klipper.rs b/src/printer/klipper.rs index dc4d3ba..a41aea7 100644 --- a/src/printer/klipper.rs +++ b/src/printer/klipper.rs @@ -1,7 +1,3 @@ -use std::time::Duration; - -use tracing::info; - use super::{Printer, PrinterBase, PrinterError}; pub struct KlipperPrinter { @@ -9,7 +5,7 @@ pub struct KlipperPrinter { } impl KlipperPrinter { - pub fn new(id: impl Into, name: impl Into, host: impl Into) -> Self { + pub fn new(id: &str, name: &str, host: &str) -> Self { Self { base: PrinterBase { id: id.into(), name: name.into(), host: host.into() }, } @@ -21,22 +17,15 @@ impl Printer for KlipperPrinter { &self.base } - /// OVERRIDE: Moonraker's real-time feed is a WebSocket JSON-RPC channel - /// (see adapters::moonraker::run for that), but it also exposes a plain - /// REST endpoint. `connect()` here is a lightweight reachability probe - /// against that REST endpoint before the daemon opens the WS stream — - /// a third, again-completely-different notion of "connect". + /// OVERRIDE: a third, again-different body — Moonraker's real feed is a + /// WebSocket (see `adapters::moonraker`). This also shows the `Err` + /// path: an empty `host` fails immediately, same as any other vendor + /// would if it couldn't reach its printer. async fn connect(&mut self) -> Result<(), PrinterError> { - info!(printer = %self.base.id, host = %self.base.host, "probing Moonraker over HTTP"); - - reqwest::Client::new() - .get(format!("http://{}/printer/info", self.base.host)) - .timeout(Duration::from_secs(5)) - .send() - .await - .and_then(|resp| resp.error_for_status()) - .map_err(|err| PrinterError::Connection(self.base.id.clone(), err.to_string()))?; - + if self.base.host.is_empty() { + return Err(PrinterError(format!("{} has no host configured", self.base.name))); + } + println!("[{}] connecting over WebSocket (Moonraker)", self.base.name); Ok(()) } } diff --git a/src/printer/mod.rs b/src/printer/mod.rs index 25fd936..932f06b 100644 --- a/src/printer/mod.rs +++ b/src/printer/mod.rs @@ -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> + 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`. -/// -/// 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 { diff --git a/src/printer/prusa.rs b/src/printer/prusa.rs index 1427d6f..16caac1 100644 --- a/src/printer/prusa.rs +++ b/src/printer/prusa.rs @@ -1,7 +1,3 @@ -use std::time::Duration; - -use tracing::info; - use super::{Printer, PrinterBase, PrinterError}; pub struct PrusaPrinter { @@ -10,7 +6,7 @@ pub struct PrusaPrinter { } impl PrusaPrinter { - pub fn new(id: impl Into, name: impl Into, host: impl Into, api_key: impl Into) -> Self { + pub fn new(id: &str, name: &str, host: &str, api_key: &str) -> Self { Self { base: PrinterBase { id: id.into(), name: name.into(), host: host.into() }, api_key: api_key.into(), @@ -23,21 +19,11 @@ impl Printer for PrusaPrinter { &self.base } - /// OVERRIDE: PrusaLink is a plain REST API. "Connecting" just means - /// confirming the printer answers `GET /api/v1/status` with the API - /// key — no persistent session to hold onto, unlike Bambu's MQTT link. + /// OVERRIDE: a completely different body from Bambu's — PrusaLink is a + /// plain REST API (real version: `GET /api/v1/status` with the API + /// key, see `adapters::prusalink`), no persistent session to hold. async fn connect(&mut self) -> Result<(), PrinterError> { - info!(printer = %self.base.id, host = %self.base.host, "connecting to PrusaLink over HTTP"); - - reqwest::Client::new() - .get(format!("http://{}/api/v1/status", self.base.host)) - .header("X-Api-Key", &self.api_key) - .timeout(Duration::from_secs(5)) - .send() - .await - .and_then(|resp| resp.error_for_status()) - .map_err(|err| PrinterError::Connection(self.base.id.clone(), err.to_string()))?; - + println!("[{}] connecting over HTTP (PrusaLink)", self.base.name); Ok(()) } }