diff --git a/src/features/contacts.rs b/src/features/contacts.rs index ef39b6633..6a7314e9e 100644 --- a/src/features/contacts.rs +++ b/src/features/contacts.rs @@ -24,6 +24,31 @@ fn ensure_is_on_whatsapp_jids_supported(jids: &[Jid]) -> Result<()> { Ok(()) } +/// Mapping extractors as fn items, NOT closures. A closure returning +/// references tied to its argument is inferred at a concrete lifetime, and +/// because its type is embedded in the public methods' future types, callers +/// that box those futures (`#[async_trait]`, `Box`) hit +/// "implementation of `FnOnce` is not general enough" (issue #825). Fn items +/// implement `Fn` for every lifetime by construction. +/// PN-primary result -> (PN, LID mapping). +fn forward_lid_pair(r: &IsOnWhatsAppResult) -> (&Jid, Option<&Jid>) { + (&r.jid, r.lid.as_ref()) +} + +/// LID-primary result inverted to (PN, LID); None when not LID-primary. +fn reverse_lid_pair(r: &IsOnWhatsAppResult) -> Option<(&Jid, Option<&Jid>)> { + if r.jid.is_lid() { + r.pn_jid.as_ref().map(|pn| (pn, Some(&r.jid))) + } else { + None + } +} + +/// UserInfo entry -> (queried JID, LID mapping). +fn user_info_lid_pair(entry: &UserInfo) -> (&Jid, Option<&Jid>) { + (&entry.jid, entry.lid.as_ref()) +} + pub struct Contacts<'a> { client: &'a Client, } @@ -33,6 +58,10 @@ impl<'a> Contacts<'a> { Self { client } } + /// Callers must pass `fn` items (e.g. [`forward_lid_pair`]), NOT + /// closures: a closure returning borrowed pairs embeds a non-HRTB type in + /// the public caller's future and breaks `#[async_trait]` consumers + /// (issue #825, guarded by tests/async_trait_boxed_future_compat.rs). async fn persist_lid_mappings<'b, I>(&self, entries: I) where I: IntoIterator)>, @@ -112,16 +141,10 @@ impl<'a> Contacts<'a> { results.extend(self.client.execute(spec).await?); } - self.persist_lid_mappings(results.iter().map(|r| (&r.jid, r.lid.as_ref()))) + self.persist_lid_mappings(results.iter().map(forward_lid_pair)) + .await; + self.persist_lid_mappings(results.iter().filter_map(reverse_lid_pair)) .await; - self.persist_lid_mappings(results.iter().filter_map(|r| { - if r.jid.is_lid() { - r.pn_jid.as_ref().map(|pn| (pn, Some(&r.jid))) - } else { - None - } - })) - .await; Ok(results) } @@ -189,7 +212,7 @@ impl<'a> Contacts<'a> { let spec = UserInfoSpec::new(jids.to_vec(), request_id); let info = self.client.execute(spec).await?; - self.persist_lid_mappings(info.values().map(|entry| (&entry.jid, entry.lid.as_ref()))) + self.persist_lid_mappings(info.values().map(user_info_lid_pair)) .await; Ok(info) } diff --git a/tests/async_trait_boxed_future_compat.rs b/tests/async_trait_boxed_future_compat.rs new file mode 100644 index 000000000..a8b67b91d --- /dev/null +++ b/tests/async_trait_boxed_future_compat.rs @@ -0,0 +1,66 @@ +//! Compile-time regression guard for issue #825. +//! +//! Public async methods whose future type embeds a closure returning +//! references (e.g. an iterator adapter passed to a generic helper) fail with +//! "implementation of `FnOnce` is not general enough" ONLY when a downstream +//! caller boxes the future, which is exactly what `#[async_trait]` does. The +//! library's own tests never box that way, so this file reproduces the +//! consumer shape: if it compiles, the guard passes. + +use async_trait::async_trait; +use std::sync::Arc; +use tokio::sync::RwLock; +use wacore_binary::Jid; +use whatsapp_rust::client::Client; + +#[async_trait] +pub trait WhatsAppGateway: Send + Sync { + async fn check_phones(&self, phones: Vec) -> Result, String>; + async fn statuses(&self, phones: Vec) -> Result>, String>; +} + +pub struct GatewayImpl { + client: RwLock>>, +} + +#[async_trait] +impl WhatsAppGateway for GatewayImpl { + async fn check_phones(&self, phones: Vec) -> Result, String> { + let guard = self.client.read().await; + let client = guard.as_ref().expect("client").clone(); + drop(guard); + + let jids: Vec = phones.iter().map(|p| Jid::pn(p.as_str())).collect(); + let results = client + .contacts() + .is_on_whatsapp(&jids) + .await + .map_err(|e| e.to_string())?; + Ok(results.into_iter().map(|r| r.is_registered).collect()) + } + + async fn statuses(&self, phones: Vec) -> Result>, String> { + let guard = self.client.read().await; + let client = guard.as_ref().expect("client").clone(); + drop(guard); + + let jids: Vec = phones.iter().map(|p| Jid::pn(p.as_str())).collect(); + let info = client + .contacts() + .get_user_info(&jids) + .await + .map_err(|e| e.to_string())?; + Ok(jids + .iter() + .map(|jid| info.get(jid).and_then(|i| i.status.clone())) + .collect()) + } +} + +/// The guard is the compilation itself; this just keeps the test binary +/// non-empty and the trait impl reachable. +#[test] +fn async_trait_consumers_compile() { + fn assert_impl() {} + assert_impl::(); +}