xmtp_macro/lib.rs
1extern crate proc_macro;
2
3mod async_trait;
4mod error_code;
5mod log_macros;
6mod logging;
7mod sdk_export;
8mod sdk_member;
9mod span_macro;
10mod test_macro;
11mod timeout_macro;
12
13#[cfg(test)]
14mod facade_markers_test;
15#[cfg(test)]
16mod sdk_export_test;
17#[cfg(test)]
18mod sdk_member_test;
19#[cfg(test)]
20mod timeout_macro_test;
21
22/// Export an impl block, trait, function, record, or enum through UniFFI on
23/// native and wasm32 targets, and describe it to the SDK generator.
24///
25/// If the item has an async function, native targets use the Tokio async
26/// runtime. Sync-only items use plain `uniffi::export` on every target.
27/// Every method in an impl block and every free function gets a tracing span.
28/// Trait methods with a default body also get a span. Functions that return
29/// `Result` record errors. An existing `#[tracing::instrument]` is kept.
30/// On a record or enum, make this the first attribute, above every derive: a
31/// derive written above it expands first, and the macro never sees it. The
32/// `#[derive(uniffi::Record)]` or `#[derive(uniffi::Enum)]` below it still
33/// exports the type.
34///
35/// Arguments:
36///
37/// - `native_only` / `wasm_only`: limit the whole item to one target, as
38/// `#[cfg(not(target_arch = "wasm32"))]` or `#[cfg(target_arch = "wasm32")]`
39/// above it would.
40/// - `pure`: a synchronous free function with value-only arguments, for the
41/// browser's main-thread module. It can forward stock argument defaults with
42/// `pure, default(name = None)`. The SDK generator rejects object, client,
43/// and foreign-trait arguments on a pure export.
44/// - `client_static`: an asynchronous free function that every SDK also
45/// exposes as a static member of its Client. The static's name is the
46/// function's without a trailing `_with_backend`, in the SDK's casing, and
47/// it takes the function's arguments in order with the `BackendSource`
48/// argument moved last: `can_message_with_backend(backend, identities)`
49/// becomes `Client.canMessage(identities, backend)`. The function stays
50/// exported as well. It cannot be `native_only` or `wasm_only`. The
51/// `BackendSource` cannot have a default, and no defaulted parameter can
52/// end the function after it: the static takes the backend last, so a
53/// TypeScript caller could not leave that parameter out.
54///
55/// Members take `#[sdk(...)]`:
56///
57/// - `#[sdk(immutable)]` on a synchronous `&self` method without arguments
58/// that returns a value: the value never changes for the object's lifetime.
59/// The SDKs expose it as a readonly property, and the browser bridge reads
60/// it once. Such a method needs the option unless the bridge never forwards
61/// it: its item is `native_only`, or its doc comment says `@xmtp-worker`.
62/// Make a live read async instead.
63/// - `#[sdk(kind = "namespace.name")]` on an enum variant: the public string
64/// of the variant. `EventKind` takes one per variant. Mark every variant of
65/// the enum or none.
66/// - `#[sdk(redact)]` on a named record or variant field: generated Kotlin
67/// `toString` and Swift `description` print `<redacted>` for it.
68/// `#[sdk(redact = "key")]` hides one key of a string map field; the key
69/// holds ASCII letters, digits, `_`, `.`, and `-`. Redaction fails closed:
70/// every other field of that record, or of any variant of that enum, takes
71/// `#[sdk(redact)]` or `#[sdk(shown)]`, and the macro implements `Debug`
72/// for the type by calling its `fn redacted_debug(&self, f: &mut
73/// Formatter<'_>) -> fmt::Result`, which the type writes. A derived `Debug` then conflicts
74/// with it, wherever the derive sits. A `uniffi::Error` type cannot redact
75/// a field: the Kotlin binding renames it.
76///
77/// `pure`, `client_static`, and each member option but `shown` become a
78/// `#[doc = "@xmtp-..."]` line that UniFFI carries into the library metadata,
79/// and a type with a redacted field gets `@xmtp-redacted`. The generator reads them and strips
80/// them from generated documentation; see `apps/xmtp_sdk_bindgen/README.md`.
81/// The macro rejects these markers in a doc comment, where they would skip
82/// its checks. When rustc reports "cannot find
83/// attribute `sdk` in this scope", the item lacks `#[xmtp_macro::sdk_export]`
84/// as its first attribute.
85/// The caller must depend on `uniffi` and `tracing`.
86///
87/// ```ignore
88/// #[xmtp_macro::sdk_export]
89/// impl Client {
90/// #[sdk(immutable)]
91/// pub fn inbox_id(&self) -> InboxId { /* ... */ }
92/// pub async fn sync(&self) -> Result<(), SyncError> { /* ... */ }
93/// }
94///
95/// #[xmtp_macro::sdk_export]
96/// #[derive(Clone, Copy, uniffi::Enum)]
97/// pub enum EventKind {
98/// #[sdk(kind = "conversation.joined")]
99/// ConversationJoined,
100/// }
101///
102/// #[xmtp_macro::sdk_export]
103/// #[derive(Clone, uniffi::Record)]
104/// pub struct Credential {
105/// #[sdk(shown)]
106/// pub name: Option<String>,
107/// #[sdk(redact)]
108/// pub value: String,
109/// }
110///
111/// impl Credential {
112/// fn redacted_debug(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
113/// f.debug_struct("Credential").field("name", &self.name).finish_non_exhaustive()
114/// }
115/// }
116/// ```
117#[proc_macro_attribute]
118pub fn sdk_export(
119 attr: proc_macro::TokenStream,
120 input: proc_macro::TokenStream,
121) -> proc_macro::TokenStream {
122 sdk_export::sdk_export(attr.into(), input.into())
123 .unwrap_or_else(syn::Error::into_compile_error)
124 .into()
125}
126
127/// Check that a foreign-trait error type accepts unexpected UniFFI callback errors.
128///
129/// Put this attribute on a concrete error enum or struct. The caller must
130/// depend on `uniffi`.
131///
132/// ```
133/// #[xmtp_macro::callback_error]
134/// #[derive(Debug, thiserror::Error)]
135/// #[error("callback failed")]
136/// struct CallbackError;
137///
138/// impl From<uniffi::UnexpectedUniFFICallbackError> for CallbackError {
139/// fn from(_: uniffi::UnexpectedUniFFICallbackError) -> Self {
140/// Self
141/// }
142/// }
143/// ```
144///
145/// ```compile_fail
146/// #[xmtp_macro::callback_error]
147/// #[derive(Debug, thiserror::Error)]
148/// #[error("callback failed")]
149/// struct CallbackError;
150/// ```
151#[proc_macro_attribute]
152pub fn callback_error(
153 attr: proc_macro::TokenStream,
154 input: proc_macro::TokenStream,
155) -> proc_macro::TokenStream {
156 sdk_export::callback_error(attr.into(), input.into())
157 .unwrap_or_else(syn::Error::into_compile_error)
158 .into()
159}
160
161/// A proc macro attribute that wraps the input in an `async_trait` implementation,
162/// delegating to the appropriate `async_trait` implementation based on the target architecture.
163///
164/// On wasm32 architecture, it delegates to `async_trait::async_trait(?Send)`.
165/// On all other architectures, it delegates to `async_trait::async_trait`.
166#[proc_macro_attribute]
167pub fn async_trait(
168 attr: proc_macro::TokenStream,
169 input: proc_macro::TokenStream,
170) -> proc_macro::TokenStream {
171 async_trait::async_trait(attr, input)
172}
173
174/// A test macro that delegates to the appropriate test framework based on the target architecture.
175///
176/// On wasm32 architecture, it delegates to `wasm_bindgen_test::wasm_bindgen_test`.
177/// On all other architectures, it delegates to `tokio::test`.
178///
179/// When using with 'rstest', ensure any other test invocations come after rstest invocation.
180/// # Example
181///
182/// ```ignore
183/// #[test]
184/// async fn test_something() {
185/// assert_eq!(2 + 2, 4);
186/// }
187/// ```
188#[proc_macro_attribute]
189pub fn test(
190 attr: proc_macro::TokenStream,
191 body: proc_macro::TokenStream,
192) -> proc_macro::TokenStream {
193 test_macro::test(attr, body)
194}
195
196#[proc_macro_attribute]
197pub fn build_logging_metadata(
198 attr: proc_macro::TokenStream,
199 item: proc_macro::TokenStream,
200) -> proc_macro::TokenStream {
201 log_macros::build_logging_metadata(attr, item)
202}
203
204#[proc_macro]
205pub fn log_event(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
206 log_macros::log_event(input)
207}
208
209/// Derive macro for the `ErrorCode` trait.
210///
211/// Automatically generates an `error_code()` implementation that returns
212/// `"TypeName::VariantName"` for each enum variant, or `"TypeName"` for structs.
213///
214/// # Example
215///
216/// ```ignore
217/// use xmtp_common::ErrorCode;
218///
219/// #[derive(Debug, thiserror::Error, ErrorCode)]
220/// pub enum GroupError {
221/// #[error("Group not found")]
222/// NotFound, // Returns "GroupError::NotFound"
223///
224/// #[error("Storage error: {0}")]
225/// #[error_code(inherit)] // Delegates to StorageError::error_code()
226/// Storage(#[from] StorageError),
227/// }
228/// ```
229///
230/// # Attributes
231///
232/// - `#[error_code(inherit)]` - Delegate to the inner error's `error_code()` method.
233/// Use this for single-field variants that wrap another error implementing `ErrorCode`.
234///
235/// - `#[error_code(remote = "path::Type")]` - Implement `ErrorCode` for a remote type.
236/// The derived item should mirror the remote type's shape. Default codes use the derived
237/// item's type name, so keep it aligned with the remote type's name unless overridden.
238///
239/// - `#[error_code("CustomCode")]` - Override the generated code with a custom value.
240/// Use this to maintain backwards compatibility when renaming variants.
241///
242/// # Example: Custom Code for Backwards Compatibility
243///
244/// ```ignore
245/// #[derive(Debug, thiserror::Error, ErrorCode)]
246/// pub enum MyError {
247/// // Renamed from "OldName" but keeps the old error code
248/// #[error("new name")]
249/// #[error_code("MyError::OldName")]
250/// NewName,
251/// }
252/// ```
253#[proc_macro_derive(ErrorCode, attributes(error_code))]
254pub fn derive_error_code(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
255 error_code::derive_error_code(input)
256}
257
258/// Attribute macro that wraps an async test body with a WASM-compatible timeout.
259///
260/// This is a drop-in replacement for rstest's `#[timeout]` that works on
261/// `wasm32-unknown-unknown` by using `xmtp_common::time::timeout` internally.
262///
263/// # Example
264///
265/// ```ignore
266/// #[xmtp_common::test]
267/// #[xmtp_common::timeout(std::time::Duration::from_secs(60))]
268/// async fn test_something() { ... }
269/// ```
270#[proc_macro_attribute]
271pub fn timeout(
272 attr: proc_macro::TokenStream,
273 body: proc_macro::TokenStream,
274) -> proc_macro::TokenStream {
275 timeout_macro::timeout(attr, body)
276}
277
278/// Instrument an `ApiClientWrapper` RPC method as `operation = "rpc.<fn_name>"`
279/// in libxmtp's canonical, OTEL-safe span form (`err, skip_all`). Surfaces as
280/// `xmtp.api.*` Collector metrics. See [`span`] for the shared rationale.
281///
282/// ```ignore
283/// #[xmtp_macro::rpc_span]
284/// pub async fn upload_key_package(&self, ..) -> Result<()> { .. }
285/// // → #[tracing::instrument(err, skip_all, fields(operation = "rpc.upload_key_package",
286/// // sentry.op = "rpc", sentry.name = "rpc.upload_key_package"))]
287/// ```
288#[proc_macro_attribute]
289pub fn rpc_span(
290 attr: proc_macro::TokenStream,
291 body: proc_macro::TokenStream,
292) -> proc_macro::TokenStream {
293 span_macro::rpc_span(attr, body)
294}
295
296/// Instrument an `xmtp_db` query method as `operation = "db.<fn_name>"` in
297/// libxmtp's canonical, OTEL-safe span form (`err, skip_all`). Use
298/// `#[db_span(redact_error)]` to log a fixed error field and return the original
299/// error unchanged. Surfaces as `xmtp.db.*` Collector metrics. See [`span`] for the shared rationale.
300#[proc_macro_attribute]
301pub fn db_span(
302 attr: proc_macro::TokenStream,
303 body: proc_macro::TokenStream,
304) -> proc_macro::TokenStream {
305 span_macro::db_span(attr, body)
306}
307
308/// Instrument a high-level MLS operation as `operation = "mls.<fn_name>"` in
309/// libxmtp's canonical, OTEL-safe span form (`err, skip_all`). Use
310/// `#[mls_span(redact_error)]` to log a fixed error field and return the original
311/// error unchanged. Surfaces as `xmtp.mls.*` Collector metrics. See [`span`] for the shared rationale.
312#[proc_macro_attribute]
313pub fn mls_span(
314 attr: proc_macro::TokenStream,
315 body: proc_macro::TokenStream,
316) -> proc_macro::TokenStream {
317 span_macro::mls_span(attr, body)
318}
319
320/// Instrument a method as a telemetry operation span in libxmtp's single
321/// canonical, OTEL-safe form: `#[tracing::instrument(err, skip_all,
322/// fields(operation = "<prefix>.<fn_name>", sentry.op = "<prefix>",
323/// sentry.name = "<prefix>.<fn_name>"))]`.
324///
325/// `err` records span status=error on an `Err` return; `skip_all` keeps every
326/// argument off the span so a per-call id can never leak in and explode
327/// trace-attribute cardinality. `operation` is the single dimension the
328/// Collector's `span_metrics` connector buckets on. Making this the only
329/// writable form guarantees those invariants at compile time — no runtime test.
330///
331/// `sentry.op`/`sentry.name` are static vendor hints (same pattern as `otel.*`)
332/// consumed by sentry-tracing; without them every Sentry span arrives as
333/// op = "default".
334///
335/// This is the escape hatch for a namespace without a dedicated attribute;
336/// prefer [`rpc_span`] / [`db_span`] / [`mls_span`] where they apply.
337///
338/// ```ignore
339/// #[xmtp_macro::span(prefix = "stream")]
340/// pub async fn subscribe(&self, ..) -> Result<..> { .. }
341/// // → operation = "stream.subscribe", sentry.op = "stream",
342/// // sentry.name = "stream.subscribe"
343/// ```
344#[proc_macro_attribute]
345pub fn span(
346 attr: proc_macro::TokenStream,
347 body: proc_macro::TokenStream,
348) -> proc_macro::TokenStream {
349 span_macro::span(attr, body)
350}
351
352/// Error-case-only tracing for an FFI-exported fn: a `trace`-level `skip_all`
353/// span carrying `sentry.op = "ffi"` / `sentry.name = "<fn_name>"`, plus an
354/// ERROR event on an `Err` return.
355///
356/// The span is `trace`-level, so under normal filters the success path emits
357/// nothing, and `skip_all` keeps arguments (keys, pins, paths) off the span.
358/// `sentry.op`/`sentry.name` are static vendor hints, as in [`span`].
359///
360/// An async fn gets more than the attribute: its body is rewritten into a
361/// nested `async move` bound to a per-call Sentry Hub (`bind_task_hub`), so
362/// concurrent FFI calls keep separate breadcrumb trails, and the ERROR event is
363/// emitted by hand *inside* that hub instead of by `instrument(err)` — `err`
364/// fires only after the awaited body returns, by which point the task hub is
365/// gone and the event Sentry promotes to an issue carries none of its
366/// breadcrumbs. A sync fn keeps plain `instrument(.., err)` (rare at FFI).
367///
368/// Unlike [`span`], this is napi-safe: napi-rs clones every method attribute
369/// onto the `extern "C"` wrapper it generates (which returns a raw
370/// `napi_value`, not `Result`), so a bare `#[tracing::instrument(err)]` on an
371/// exported method fails to compile. This macro detects the wrapper by its
372/// `extern` ABI and passes it through untouched, instrumenting only the real
373/// method.
374///
375/// ```ignore
376/// #[napi]
377/// #[xmtp_common::err_span]
378/// pub async fn sync(&self) -> Result<()> { .. }
379/// ```
380#[proc_macro_attribute]
381pub fn err_span(
382 attr: proc_macro::TokenStream,
383 body: proc_macro::TokenStream,
384) -> proc_macro::TokenStream {
385 span_macro::err_span(attr, body)
386}