mqtt-typed-client

Crates.iomqtt-typed-client
lib.rsmqtt-typed-client
version0.1.0
created_at2025-07-27 14:31:22.400703+00
updated_at2025-07-27 14:31:22.400703+00
descriptionType-safe async MQTT client with automatic topic routing
homepage
repositoryhttps://github.com/holovskyi/mqtt-typed-client
max_upload_size
id1770130
size316,977
Artem Holovskyi (holovskyi)

documentation

README

🦀 MQTT Typed Client

A type-safe async MQTT client built on top of rumqttc

Automatic topic routing and subscription management with compile-time guarantees

CI Crates.io Documentation License: MIT OR Apache-2.0 MSRV

✨ Key Features

  • Type-safe topic patterns with named parameters and automatic parsing
  • Zero-cost abstractions via procedural macros with compile-time validation
  • IDE-friendly experience - full autocomplete for topics, parameters, and generated client methods
  • Automatic subscription management with intelligent routing and lifecycle handling
  • Built-in serialization support for 8+ formats (Bincode, JSON, MessagePack, etc.)
  • Efficient message routing with tree-based topic matching and internal caching
  • Smart defaults with full configurability when needed
  • Memory efficient design with proper resource management
  • Automatic reconnection and graceful shutdown

⚠️ MSRV: Rust 1.85.1 (driven by default bincode serializer; can be lowered with alternative serializers)

🚀 Quick Start

Add to your Cargo.toml:

[dependencies]
mqtt-typed-client = "0.1.0"
use mqtt_typed_client::prelude::*;
use mqtt_typed_client_macros::mqtt_topic;
use serde::{Deserialize, Serialize};
use bincode::{Encode, Decode};

#[derive(Serialize, Deserialize, Encode, Decode, Debug)]
enum SensorStatus {
    Active,
    Inactive,
    Maintenance,
}

#[derive(Serialize, Deserialize, Encode, Decode, Debug)]
struct SensorReading {
    temperature: f64,
    status: SensorStatus,    // enum field
    location_note: String,   // string field for variety
}

// Define typed topic with automatic parameter extraction
#[mqtt_topic("sensors/{location}/{device_id}/data")]
struct SensorTopic {
    location: String,    // String parameter
    device_id: u32,      // Numeric parameter - automatic conversion!
    payload: SensorReading,
}

#[tokio::main]
async fn main() -> Result<()> {
    // Connect to MQTT broker
    let (client, connection) = MqttClient::<BincodeSerializer>::connect(
        "mqtt://broker.hivemq.com:1883"
    ).await?;

    // Get typed client for this specific topic - method generated by macro
    // Returns a typed client for publishing and subscribing to messages 
    // with automatic parameter handling for this topic pattern
    let topic_client = client.sensor_topic();
    
    // Subscribe to all matching topics: "sensors/+/+/data"
    // Returns typed subscriber that automatically extracts and converts
    // topic parameters into struct fields
    let mut subscriber = topic_client.subscribe().await?;
    
    let reading = SensorReading { 
        temperature: 22.5,
        status: SensorStatus::Active,
        location_note: "Kitchen sensor near window".to_string(),
    };
    
    // Publish with automatic type conversion to specific topic: "sensors/kitchen/42/data"
    // Parameters are automatically converted to strings and inserted into topic pattern
    topic_client.publish("kitchen", 42u32, &reading).await?;
    //                    ^^^^^^^^  ^^^^^ 
    //                    String    u32 -> automatically converts to "42" in topic
    
    // Receive with automatic parameter extraction and conversion
    if let Some(Ok(msg)) = subscriber.receive().await {
        println!("Device {} in location '{}' reported: temp={}°C, status={:?}", 
            msg.device_id,  // u32 (converted from "42" in topic)
            msg.location,   // String (extracted from topic)
            msg.payload.temperature, msg.payload.status);
    }
    
    connection.shutdown().await?;
    Ok(())
}

📚 Examples

See examples/ - Complete usage examples with source code

  • 000_hello_world.rs - Basic publish/subscribe with macros
  • 001_ping_pong.rs - Multi-client communication
  • 002_configuration.rs - Advanced client configuration
  • 003_hello_world_lwt.rs - Last Will & Testament
  • 004_hello_world_tls.rs - TLS/SSL connections
  • 005_hello_world_serializers.rs - Custom serializers
  • 006_retain_and_clear.rs - Retained messages
  • 007_custom_patterns.rs - Custom topic patterns
  • 008_modular_example.rs - Modular application structure

Run examples:

cargo run --example 000_hello_world

📦 Serialization Support

Multiple serialization formats are supported via feature flags:

  • bincode - Binary serialization (default, most efficient)
  • json - JSON serialization (default, human-readable)
  • messagepack - MessagePack binary format
  • cbor - CBOR binary format
  • postcard - Embedded-friendly binary format
  • ron - Rusty Object Notation
  • flexbuffers - FlatBuffers FlexBuffers
  • protobuf - Protocol Buffers (requires generated types)

Enable additional serializers:

[dependencies]
mqtt-typed-client = { version = "0.1.0", features = ["messagepack", "cbor"] }

Custom serializers can be implemented by implementing the MessageSerializer trait.

🎯 Topic Pattern Matching

Supports MQTT wildcard patterns with named parameters:

  • {param} - Named parameter (equivalent to + wildcard)
  • {param:#} - Multi-level named parameter (equivalent to # wildcard)
use mqtt_typed_client_macros::mqtt_topic;

// Traditional MQTT wildcards
#[mqtt_topic("home/+/temperature")]     // matches: home/kitchen/temperature
struct SimplePattern { payload: f64 }

// Named parameters (recommended)
#[mqtt_topic("home/{room}/temperature")] // matches: home/kitchen/temperature  
struct NamedPattern { 
    room: String,        // Automatically extracted: "kitchen"
    payload: f64 
}

// Multi-level parameters
#[mqtt_topic("logs/{service}/{path:#}")]  // matches: logs/api/v1/users/create
struct LogPattern {
    service: String,     // "api"
    path: String,        // "v1/users/create"  
    payload: String     // Changed from Data to String
}

🔧 Advanced Usage: Low-Level API

For cases where you need direct control without macros:

use mqtt_typed_client::prelude::*;
use serde::{Deserialize, Serialize};
use bincode::{Encode, Decode};

#[derive(Serialize, Deserialize, Encode, Decode, Debug)]
struct SensorData {
    temperature: f64,
    humidity: f64,
}

#[tokio::main]
async fn main() -> Result<()> {
    let (client, connection) = MqttClient::<BincodeSerializer>::connect(
        "mqtt://broker.hivemq.com:1883"
    ).await?;

    // Direct topic operations
    let publisher = client.get_publisher::<SensorData>("sensors/temperature")?;
    let mut subscriber = client.subscribe::<SensorData>("sensors/+").await?;

    let data = SensorData { temperature: 23.5, humidity: 45.0 };
    publisher.publish(&data).await?;

    if let Some((topic, result)) = subscriber.receive().await {
        match result {
            Ok(sensor_data) => println!("Received from {}: {:?}", topic.topic_path(), sensor_data),
            Err(e) => eprintln!("Deserialization error: {:?}", e),
        }
    }

    connection.shutdown().await?;
    Ok(())
}

🆚 What mqtt-typed-client adds over rumqttc

Publishing:

// rumqttc - manual topic construction and serialization
let sensor_id = "sensor001";
let data = SensorData { temperature: 23.5 };
let topic = format!("sensors/{}/temperature", sensor_id);
let payload = serde_json::to_vec(&data)?;
client.publish(topic, QoS::AtLeastOnce, false, payload).await?;

// mqtt-typed-client - type-safe, automatic  
topic_client.publish(&sensor_id, &data).await?;

Subscribing with routing:

// rumqttc - manual pattern matching and dispatching
// while let Ok(event) = eventloop.poll().await {
//     if let Event::Incoming(Packet::Publish(publish)) = event {
//         if publish.topic.starts_with("sensors/") {
//             // Manual topic parsing, manual deserialization...
//         } else if publish.topic.starts_with("alerts/") {
//             // More manual parsing...
//         }
//     }
// }

// mqtt-typed-client - automatic routing to typed handlers
let mut sensor_sub = client.sensor_topic().subscribe().await?;
let mut alert_sub = client.alert_topic().subscribe().await?;

tokio::select! {
    msg = sensor_sub.receive() => { /* typed sensor data ready */ }
    msg = alert_sub.receive() => { /* typed alert data ready */ }
}

📋 For detailed comparison see: docs/COMPARISON_WITH_RUMQTTC.md

📄 License

This project is licensed under either of

at your option.

🤝 Contributing

Contributions are welcome! Please feel free to submit a Pull Request.

  1. Fork the repository
  2. Create your feature branch (git checkout -b feature/amazing-feature)
  3. Commit your changes (git commit -m 'Add some amazing feature')
  4. Push to the branch (git push origin feature/amazing-feature)
  5. Open a Pull Request

See CONTRIBUTING.md for detailed guidelines.

📖 API Reference

For detailed API documentation, visit docs.rs/mqtt-typed-client.

🔗 See Also

Commit count: 0

cargo fmt