Skip to main content

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		&macro_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}