mqtt_typed_client_macros/lib.rs
1//! # MQTT Typed Client Macros
2//!
3//! This crate provides procedural macros for generating typed MQTT subscribers
4//! and publishers with automatic topic parameter extraction and payload serialization.
5//!
6//! ## Overview
7//!
8//! The main macro `mqtt_topic` allows you to annotate a struct with
9//! a topic pattern, automatically generating the necessary trait implementations
10//! and helper methods for MQTT subscription and publishing.
11//!
12//! ## Features
13//!
14//! - **Topic Parameter Extraction**: Automatically extracts named parameters from MQTT topics
15//! - **Type Safety**: Compile-time validation of struct fields against topic patterns
16//! - **Flexible Payload Handling**: Support for custom payload types with automatic serialization
17//! - **Optional Topic Access**: Include the full topic match information if needed
18//! - **Dual Mode Generation**: Generate subscriber, publisher, or both methods
19//! - **Generated Helper Methods**: Convenient subscription and publishing methods with pattern constants
20//!
21//! ## Quick Start
22//!
23//! ```rust,ignore
24//! use mqtt_typed_client_macros::mqtt_topic;
25//! use std::sync::Arc;
26//! use mqtt_typed_client_core::topic::topic_match::TopicMatch;
27//!
28//! #[derive(Debug)]
29//! #[mqtt_topic("sensors/{sensor_id}/temperature/{room}")]
30//! struct TemperatureReading {
31//! sensor_id: u32, // Extracted from {sensor_id} in topic
32//! room: String, // Extracted from {room} in topic
33//! payload: f64, // Message payload (temperature value)
34//! topic: Arc<TopicMatch>, // Optional: full topic match info
35//! }
36//!
37//! // Generated constants:
38//! // TemperatureReading::TOPIC_PATTERN = "sensors/{sensor_id}/temperature/{room}"
39//! // TemperatureReading::MQTT_PATTERN = "sensors/+/temperature/+"
40//!
41//! // Generated methods:
42//! // let subscriber = TemperatureReading::subscribe(&client).await?;
43//! // TemperatureReading::publish(&client, sensor_id, room, &data).await?;
44//! ```
45//!
46//! ## Supported Field Types
47//!
48//! - **Topic Parameters**: Any field name matching a `{parameter}` in the topic pattern
49//! - **`payload`**: The message payload, can be any deserializable type
50//! - **`topic`**: Must be `Arc<TopicMatch>`, provides access to full topic information
51//!
52//! ## Topic Pattern Syntax
53//!
54//! - `{param_name}` - Named parameter that becomes a struct field
55//! - `+` - Anonymous single-level wildcard (not extracted)
56//! - `#` - Anonymous multi-level wildcard (not extracted, must be last)
57//! - `{param_name:#}` - Named multi-level wildcard (extracted as string)
58//!
59//! ### ⚠️ Publisher Limitations
60//!
61//! **Multi-level wildcards (`#` or `{param:#}`) are not supported for publishers**
62//! because they represent variable-length topic segments that cannot be constructed
63//! from fixed parameters.
64//!
65//! ```rust,ignore
66//! use mqtt_typed_client_macros::mqtt_topic;
67//!
68//! // ✅ This works for both subscriber and publisher
69//! #[mqtt_topic("sensors/{sensor_id}/data")]
70//! struct SensorData { sensor_id: u32, payload: f64 }
71//!
72//! // ✅ This works for subscriber only
73//! #[mqtt_topic("alerts/{category}/#", subscriber)]
74//! struct Alert { category: String, payload: String }
75//!
76//! // ❌ This will cause a compile error
77//! // #[mqtt_topic("events/{event_type}/{details:#}")]
78//! // struct Event { /* ... */ }
79//!
80//! // 🔧 Solution: Use separate structs
81//! #[mqtt_topic("events/{event_type}/{details:#}", subscriber)]
82//! struct EventReceived { event_type: String, details: String, payload: Vec<u8> }
83//!
84//! #[mqtt_topic("events/{event_type}", publisher)]
85//! struct EventToSend { event_type: String, payload: Vec<u8> }
86//! ```
87
88mod analysis;
89#[cfg(test)]
90mod analysis_test;
91mod codegen;
92#[cfg(test)]
93mod codegen_test;
94mod codegen_typed_client;
95#[cfg(test)]
96mod lib_test;
97mod naming;
98
99use mqtt_typed_client_core::routing::subscription_manager::CacheStrategy;
100use mqtt_typed_client_core::topic::topic_pattern_path::TopicPatternPath;
101use proc_macro::TokenStream;
102use syn::{LitStr, parse::Parser, parse_macro_input};
103
104// Re-export key types for testing and advanced usage
105// pub use analysis::{StructAnalysisContext, TopicParam};
106// pub use codegen::{CodeGenerator, GenerationInfo};
107
108/// Generate a typed MQTT subscriber and/or publisher from a struct and topic pattern
109///
110/// This macro analyzes the annotated struct and generates:
111/// 1. `FromMqttMessage` trait implementation for message conversion (if subscriber enabled)
112/// 2. Helper constants (`TOPIC_PATTERN`, `MQTT_PATTERN`)
113/// 3. Async `subscribe()` method for easy subscription (if subscriber enabled)
114/// 4. `publish()` and `get_publisher()` methods for publishing (if publisher enabled)
115///
116/// ## Arguments
117///
118/// The macro takes a topic pattern string and optional mode flags:
119/// - `#[mqtt_topic("pattern")]` - Generate both subscriber and publisher (default)
120/// - `#[mqtt_topic("pattern", subscriber)]` - Generate only subscriber
121/// - `#[mqtt_topic("pattern", publisher)]` - Generate only publisher
122/// - `#[mqtt_topic("pattern", subscriber, publisher)]` - Generate both (explicit)
123///
124/// The pattern can include:
125/// - Literal segments: `sensors`, `data`, `status`
126/// - Named wildcards: `{sensor_id}`, `{room}`, `{device_type}`
127/// - Anonymous wildcards: `+` (single level), `#` (multi-level, subscriber-only)
128///
129/// ## Struct Requirements
130///
131/// The annotated struct must:
132/// - Be a struct with named fields
133/// - Only contain fields that correspond to:
134/// - Topic parameters (matching `{param}` names in the pattern)
135/// - `payload` field (optional, for message data)
136/// - `topic` field (optional, must be `Arc<TopicMatch>`)
137///
138/// ## Generated Code
139///
140/// For a struct annotated with `#[mqtt_topic("sensors/{id}/data")]`:
141///
142/// ```rust,ignore,ignore
143/// // Subscriber functionality (if enabled)
144/// impl<DE> FromMqttMessage<PayloadType, DE> for YourStruct {
145/// fn from_mqtt_message(
146/// topic: Arc<TopicMatch>,
147/// payload: PayloadType,
148/// ) -> Result<Self, MessageConversionError<DE>> {
149/// // Parameter extraction and struct construction
150/// }
151/// }
152///
153/// impl YourStruct {
154/// pub const TOPIC_PATTERN: &'static str = "sensors/{id}/data";
155/// pub const MQTT_PATTERN: &'static str = "sensors/+/data";
156///
157/// // Subscriber methods (if enabled)
158/// pub async fn subscribe<F>(client: &MqttClient<F>) -> Result<...> {
159/// // Subscription logic
160/// }
161///
162/// // Publisher methods (if enabled)
163/// pub async fn publish<F>(client: &MqttClient<F>, id: ParamType, data: &PayloadType) -> Result<...> {
164/// // Publishing logic
165/// }
166///
167/// pub fn get_publisher<F>(client: &MqttClient<F>, id: ParamType) -> Result<...> {
168/// // Publisher creation
169/// }
170/// }
171/// ```
172///
173/// ## Examples
174///
175/// ### Basic Usage (Both Modes)
176/// ```rust,ignore
177/// # use mqtt_typed_client_macros::mqtt_topic;
178/// #[derive(Debug)]
179/// #[mqtt_topic("sensors/{sensor_id}/temperature")]
180/// struct TemperatureReading {
181/// sensor_id: u32,
182/// payload: f64,
183/// }
184/// ```
185///
186/// ### Subscriber Only
187/// ```rust,ignore
188/// # use mqtt_typed_client_macros::mqtt_topic;
189/// # use std::sync::Arc;
190/// # use mqtt_typed_client_core::topic::topic_match::TopicMatch;
191/// #[derive(Debug)]
192/// #[mqtt_topic("devices/{device_id}/status/#", subscriber)]
193/// struct DeviceStatus {
194/// device_id: String,
195/// payload: Vec<u8>,
196/// topic: Arc<TopicMatch>, // Access to full topic match
197/// }
198/// ```
199///
200/// ### Publisher Only
201/// ```rust,ignore
202/// # use mqtt_typed_client_macros::mqtt_topic;
203/// #[derive(Debug)]
204/// #[mqtt_topic("commands/{service}/{action}", publisher)]
205/// struct Command {
206/// service: String,
207/// action: String,
208/// payload: String,
209/// }
210/// ```
211///
212/// ### Multiple Parameters
213/// ```rust,ignore
214/// # use mqtt_typed_client_macros::mqtt_topic;
215/// #[derive(Debug)]
216/// #[mqtt_topic("buildings/{building}/floors/{floor}/rooms/{room}/sensors/{sensor_id}")]
217/// struct SensorReading {
218/// building: String,
219/// floor: u32,
220/// room: String,
221/// sensor_id: u32,
222/// payload: Vec<u8>,
223/// }
224/// ```
225///
226/// ### No Payload
227/// ```rust,ignore
228/// # use mqtt_typed_client_macros::mqtt_topic;
229/// #[derive(Debug)]
230/// #[mqtt_topic("heartbeat/{service_name}")]
231/// struct Heartbeat {
232/// service_name: String,
233/// // No payload field - will default to Vec<u8>
234/// }
235/// ```
236///
237/// ## Error Handling
238///
239/// The macro performs compile-time validation and will produce helpful error
240/// messages for common issues:
241///
242/// - Unknown fields that don't match topic parameters
243/// - Invalid topic patterns (e.g., `#` not at the end)
244/// - Incorrect type for `topic` field
245/// - Non-struct types or structs without named fields
246/// - Publisher mode with `#` wildcards (not supported)
247///
248/// ## Runtime Behavior
249///
250/// ### Subscriber
251/// When messages are received:
252/// 1. Topic is matched against the pattern
253/// 2. Named parameters are extracted and parsed to their field types
254/// 3. Payload is deserialized to the payload field type
255/// 4. Struct is constructed with all extracted values
256///
257/// ### Publisher
258/// When publishing messages:
259/// 1. Topic parameters are provided as method arguments
260/// 2. Topic string is constructed from the pattern
261/// 3. Payload is serialized and published
262///
263/// If parameter parsing fails (e.g., non-numeric string for `u32` field),
264/// a `MessageConversionError` is returned.
265#[proc_macro_attribute]
266pub fn mqtt_topic(args: TokenStream, input: TokenStream) -> TokenStream {
267 let input_struct = parse_macro_input!(input as syn::DeriveInput);
268
269 let macro_args = match parse_macro_args(args) {
270 | Ok(args) => args,
271 | Err(err) => return err.to_compile_error().into(),
272 };
273
274 match generate_mqtt_code(macro_args, &input_struct) {
275 | Ok(tokens) => tokens.into(),
276 | Err(err) => err.to_compile_error().into(),
277 }
278}
279
280/// Main orchestration function that coordinates analysis and code generation
281///
282/// Ties together the analysis and code generation phases.
283///
284/// This function ties together the analysis and code generation phases:
285/// 1. Parse and validate the topic pattern
286/// 2. Analyze the struct against the pattern
287/// 3. Generate the complete implementation
288///
289/// # Error Handling
290///
291/// All errors are converted to `syn::Error` with appropriate spans and
292/// descriptive messages for the best possible compile-time diagnostics.
293fn generate_mqtt_code(
294 macro_args: MacroArgs,
295 input_struct: &syn::DeriveInput,
296) -> Result<proc_macro2::TokenStream, syn::Error> {
297 // Analyze the struct against the pattern
298 let context = analysis::StructAnalysisContext::analyze(
299 input_struct,
300 ¯o_args.pattern,
301 )?;
302
303 // Generate the complete implementation
304 let generator = codegen::CodeGenerator::new(context, macro_args);
305 generator.generate_complete_implementation(input_struct)
306}
307
308/// Parse macro arguments: pattern string and optional generation mode flags
309///
310/// Supports: `pattern`, `(pattern, subscriber)`, `(pattern, publisher)`, `(pattern, subscriber, publisher)`
311fn parse_macro_args(args: TokenStream) -> Result<MacroArgs, syn::Error> {
312 let parser = syn::punctuated::Punctuated::<syn::Expr, syn::Token![,]>::parse_terminated;
313 let args = parser.parse(args)?;
314
315 if args.is_empty() {
316 return Err(syn::Error::new(
317 proc_macro2::Span::call_site(),
318 "mqtt_topic macro requires at least a topic pattern string",
319 ));
320 }
321
322 // First argument must be the pattern string
323 let pattern = match &args[0] {
324 | syn::Expr::Lit(syn::ExprLit {
325 lit: syn::Lit::Str(lit_str),
326 ..
327 }) => lit_str.clone(),
328 | _ => {
329 return Err(syn::Error::new_spanned(
330 &args[0],
331 "First argument must be a string literal containing the topic \
332 pattern",
333 ));
334 }
335 };
336
337 let topic_pattern = parse_topic_pattern(&pattern)?;
338
339 // Default: generate both
340 let mut generate_subscriber = true;
341 let mut generate_publisher = true;
342 let mut explicit_modes = Vec::new();
343
344 // Parse optional mode flags
345 for arg in args.iter().skip(1) {
346 match arg {
347 | syn::Expr::Path(expr_path)
348 if expr_path.path.is_ident("subscriber") =>
349 {
350 explicit_modes.push("subscriber");
351 }
352 | syn::Expr::Path(expr_path)
353 if expr_path.path.is_ident("publisher") =>
354 {
355 explicit_modes.push("publisher");
356 }
357 | _ => {
358 return Err(syn::Error::new_spanned(
359 arg,
360 "Invalid argument. Only 'subscriber' and 'publisher' \
361 flags are allowed",
362 ));
363 }
364 }
365 }
366
367 if explicit_modes.len() > 2 {
368 return Err(syn::Error::new(
369 proc_macro2::Span::call_site(),
370 "Too many arguments. Only 'subscriber' and 'publisher' flags are \
371 allowed",
372 ));
373 }
374 // Apply explicit modes if any were specified
375 if !explicit_modes.is_empty() {
376 generate_subscriber = explicit_modes.contains(&"subscriber");
377 generate_publisher = explicit_modes.contains(&"publisher");
378 }
379
380 // Validate configuration
381 if !(generate_subscriber || generate_publisher) {
382 return Err(syn::Error::new(
383 proc_macro2::Span::call_site(),
384 "At least one of 'subscriber' or 'publisher' must be enabled",
385 ));
386 }
387
388 // Check for multi-level wildcards if publisher is requested
389 if generate_publisher && topic_pattern.contains_hash() {
390 #[rustfmt::skip]
391 return Err(syn::Error::new_spanned(
392 &pattern,
393 format!(
394 "Cannot generate publisher methods for patterns with '#' wildcards.\n\n\
395 Solutions:\n\
396 • Use subscriber-only mode: #[mqtt_topic(\"{}\", subscriber)]\n\
397 • Create separate structs for different purposes:\n\n\
398 #[mqtt_topic(\"{}\", subscriber)]\n\
399 struct EventReceived {{ /* fields */ }}\n\n\
400 #[mqtt_topic(\"events/{{category}}\", publisher)]\n\
401 struct EventToSend {{ /* fields */ }}\n\n\
402 Why: Publishers need concrete topic strings, but '#' represents \n\
403 variable-length paths that cannot be determined at compile time.",
404 topic_pattern.topic_pattern(),
405 topic_pattern.topic_pattern()
406 ),
407 ));
408 }
409
410 let macro_args = MacroArgs {
411 pattern: topic_pattern,
412 generate_subscriber,
413 generate_publisher,
414 generate_typed_client: true, // Enable by default
415 generate_last_will: generate_publisher, // Enable if publisher is requested
416 };
417
418 Ok(macro_args)
419}
420
421/// Parse and validate a topic pattern string
422///
423/// Converts string literal into validated TopicPatternPath with syntax validation.
424///
425/// Converts a string literal from the macro arguments into a validated
426/// `TopicPatternPath`, checking for syntax errors and invalid wildcard usage.
427///
428/// # Arguments
429/// * `topic_pattern_str` - String literal from macro arguments
430///
431/// # Returns
432/// * `Ok(TopicPatternPath)` - Valid pattern ready for analysis
433/// * `Err(syn::Error)` - Parse error with helpful message and correct span
434///
435/// # Validation
436/// - Empty patterns are rejected
437/// - `#` wildcards must be at the end
438/// - Named parameters cannot be duplicated
439/// - Wildcard syntax must be correct
440fn parse_topic_pattern(
441 topic_pattern_str: &LitStr,
442) -> Result<TopicPatternPath, syn::Error> {
443 TopicPatternPath::new_from_string(
444 topic_pattern_str.value(),
445 CacheStrategy::NoCache,
446 )
447 .map_err(|err| {
448 syn::Error::new_spanned(
449 topic_pattern_str,
450 format!("Invalid topic pattern: {err}"),
451 )
452 })
453}
454
455/// Macro configuration arguments
456#[derive(Debug)]
457struct MacroArgs {
458 pattern: TopicPatternPath,
459 generate_subscriber: bool,
460 generate_publisher: bool,
461 generate_typed_client: bool,
462 generate_last_will: bool,
463}
464
465#[cfg(test)]
466mod test_helpers {
467 use super::*;
468
469 pub fn create_test_macro_args() -> MacroArgs {
470 let topic_pattern = TopicPatternPath::new_from_string(
471 "sensors/{sensor_id}/temp".to_string(),
472 CacheStrategy::NoCache,
473 )
474 .unwrap();
475
476 MacroArgs {
477 pattern: topic_pattern,
478 generate_subscriber: true,
479 generate_publisher: true,
480 generate_typed_client: true,
481 generate_last_will: true,
482 }
483 }
484}