diff --git a/AGENTS.md b/AGENTS.md index 2d87bf1b7..80e752859 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -31,6 +31,7 @@ cargo test -p e2e-tests # requires mock server running - **Protocol**: Cross-reference **whatsmeow**, **Baileys**, and captured WhatsApp Web JS (`docs/captured-js/`) to verify implementations. - **IQ Requests**: Use `client.execute(Spec::new(&jid)).await?` pattern. IqSpec constructors take `&Jid` not `Jid`. - **New features**: Expose via `src/features/mod.rs`, re-export in `src/lib.rs`. +- **Event payloads**: Model a maybe-absent field as `Option`, never an empty-string/zero sentinel. Pre-1.0 the payload structs stay constructible (not `#[non_exhaustive]`), so consumers read the fields they need instead of destructuring exhaustively; see the `Event` doc in `wacore/src/types/events.rs` for the full stability policy. - **Wire-tagged enums**: Every protocol enum uses `#[derive(WireEnum)]`. The `#[wire = "..."]` (or `#[wire = NUM]` for int mode) attribute is the SINGLE source of truth for each variant's wire value. Do NOT also derive `serde::Serialize`/`Deserialize` or add `#[serde(rename_all)]` — the derive owns both. Three modes: unit-string (default), tagged-with-payload (`#[wire(tag = "type")]` on the enum, optional `#[wire_alias = "..."]` and `#[wire(skip)]` on fields, `#[wire_fallback]` for catch-all), and int (`#[wire(kind = "int")]`). In tagged mode the derive auto-generates a sibling `Tag` enum; parsers must dispatch via `Tag::try_from(node.tag.as_ref())` instead of matching string literals, so renaming a wire tag stays a single-attribute change. ## Detailed Docs diff --git a/src/client/node_io.rs b/src/client/node_io.rs index fdaa1990f..f893b669d 100644 --- a/src/client/node_io.rs +++ b/src/client/node_io.rs @@ -1149,10 +1149,7 @@ impl Client { { let ack = wacore::types::events::ServerAck { id: id.as_str().to_string(), - class: node - .get_attr("class") - .map(|v| v.as_str().to_string()) - .unwrap_or_default(), + class: node.get_attr("class").map(|v| v.as_str().to_string()), from: node.get_attr("from").and_then(|v| v.as_str().parse().ok()), timestamp: node .get_attr("t") diff --git a/src/client/tests.rs b/src/client/tests.rs index 0c16f1d70..c7c304940 100644 --- a/src/client/tests.rs +++ b/src/client/tests.rs @@ -235,7 +235,7 @@ async fn test_ack_dispatches_server_ack_event() { e.as_ref(), Event::ServerAck(ack) if ack.id == "ack-evt-1" - && ack.class == "message" + && ack.class.as_deref() == Some("message") && ack.from.as_ref().is_some_and(|j| j.to_string() == "123456789@s.whatsapp.net") && ack.timestamp.is_some_and(|t| t.timestamp() == 1_720_000_000) && ack.error.is_none() @@ -243,7 +243,7 @@ async fn test_ack_dispatches_server_ack_event() { "server should dispatch Event::ServerAck with class/from/t" ); - // Nack: the error code rides along; absent class/t stay empty/None. + // Nack: the error code rides along; absent class/t stay None. let nack_node = NodeBuilder::new("ack") .attr("id", "ack-evt-2") .attr("error", "479") @@ -255,7 +255,7 @@ async fn test_ack_dispatches_server_ack_event() { e.as_ref(), Event::ServerAck(ack) if ack.id == "ack-evt-2" - && ack.class.is_empty() + && ack.class.is_none() && ack.timestamp.is_none() && ack.error.as_deref() == Some("479") )), diff --git a/wacore/src/types/events.rs b/wacore/src/types/events.rs index eb727abcc..bf9f7c161 100755 --- a/wacore/src/types/events.rs +++ b/wacore/src/types/events.rs @@ -603,6 +603,17 @@ pub struct DisappearingModeChanged { pub setting_timestamp: DateTime, } +/// An event dispatched by the client to registered handlers. +/// +/// # Stability (pre-1.0) +/// +/// The enum is `#[non_exhaustive]`, so match arms must keep a `_` catch-all. +/// The payload structs are *not* sealed: while the crate is `0.x`, an existing +/// payload may gain new fields in a minor release, so read the fields you need +/// (`ack.class`) or keep a `..` rest when destructuring, rather than binding +/// every field. A maybe-absent field is always modeled as `Option`, never +/// an empty-string / zero sentinel. Sealing the payloads behind +/// `#[non_exhaustive]` + constructors is deferred to the 1.0 API freeze. #[derive(Debug, Clone, Serialize)] #[non_exhaustive] pub enum Event { @@ -1195,8 +1206,8 @@ pub struct ServerAck { /// Id of the acked stanza (for a sent message, its message id). pub id: String, /// Stanza class the ack refers to (`"message"`, `"receipt"`, - /// `"notification"`, `"call"`, …). Empty when the server omits it. - pub class: String, + /// `"notification"`, `"call"`, …). `None` when the server omits it. + pub class: Option, /// Chat/entity the ack refers to, when present and parseable. pub from: Option, /// Server timestamp from the ack's `t` attribute, when present. For a