mecha10-macros

Crates.iomecha10-macros
lib.rsmecha10-macros
version0.1.46
created_at2025-11-03 23:24:13.450568+00
updated_at2026-01-25 23:00:36.508868+00
descriptionProcedural macros for the Mecha10 robotics framework
homepage
repositoryhttps://github.com/mecha10/mecha10
max_upload_size
id1915412
size62,809
Peter C (PeterChauYEG)

documentation

README

Mecha10 Macros

Procedural macros to reduce boilerplate in Mecha10 configuration structs.

Overview

Writing configuration structs in Mecha10 typically involves a lot of repetitive code:

  • Individual default_*() functions for each field
  • #[serde(default = "default_*")] attributes on every field
  • Manual Default trait implementation

The ConfigDefaults derive macro eliminates most of this boilerplate.

Installation

Add to your Cargo.toml:

[dependencies]
mecha10-macros = { path = "../mecha10-macros" }
serde = { version = "1.0", features = ["derive"] }

Usage

Before (Manual Approach)

use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CameraConfig {
    /// Frame rate in Hz
    #[serde(default = "default_fps")]
    pub fps: f32,

    /// Image width
    #[serde(default = "default_width")]
    pub width: u32,

    /// Image height
    #[serde(default = "default_height")]
    pub height: u32,

    /// Optional device path
    pub device: Option<String>,
}

fn default_fps() -> f32 { 30.0 }
fn default_width() -> u32 { 640 }
fn default_height() -> u32 { 480 }

impl Default for CameraConfig {
    fn default() -> Self {
        Self {
            fps: default_fps(),
            width: default_width(),
            height: default_height(),
            device: None,
        }
    }
}

Problems with this approach:

  • 20+ lines of boilerplate for a simple config
  • Easy to forget to update Default impl when adding fields
  • Repetitive function definitions
  • Error-prone: typos in function names or serde attributes

After (Using ConfigDefaults)

use mecha10_macros::ConfigDefaults;
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Serialize, Deserialize, ConfigDefaults)]
pub struct CameraConfig {
    /// Frame rate in Hz
    #[serde(default = "CameraConfig::default_fps")]
    #[config(default = 30.0)]
    pub fps: f32,

    /// Image width
    #[serde(default = "CameraConfig::default_width")]
    #[config(default = 640)]
    pub width: u32,

    /// Image height
    #[serde(default = "CameraConfig::default_height")]
    #[config(default = 480)]
    pub height: u32,

    /// Optional device path
    pub device: Option<String>,
}

Benefits:

  • ~10 lines of code instead of 30+
  • No manual Default impl needed
  • No separate default_*() function definitions
  • Single source of truth for default values
  • Option fields automatically default to None

How It Works

The ConfigDefaults macro generates:

  1. Associated functions for each field with #[config(default = value)]:

    impl CameraConfig {
        pub fn default_fps() -> f32 { 30.0 }
        pub fn default_width() -> u32 { 640 }
        pub fn default_height() -> u32 { 480 }
    }
    
  2. Default trait implementation:

    impl Default for CameraConfig {
        fn default() -> Self {
            Self {
                fps: Self::default_fps(),
                width: Self::default_width(),
                height: Self::default_height(),
                device: None,
            }
        }
    }
    

Examples

String Defaults

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct ImuConfig {
    #[serde(default = "ImuConfig::default_i2c_bus")]
    #[config(default = "/dev/i2c-1".to_string())]
    pub i2c_bus: String,

    #[serde(default = "ImuConfig::default_mode")]
    #[config(default = "NDOF".to_string())]
    pub mode: String,
}

Numeric Defaults

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct MotorConfig {
    #[serde(default = "MotorConfig::default_max_velocity")]
    #[config(default = 1.0)]
    pub max_velocity: f32,

    #[serde(default = "MotorConfig::default_max_acceleration")]
    #[config(default = 0.5)]
    pub max_acceleration: f32,

    #[serde(default = "MotorConfig::default_encoder_tpr")]
    #[config(default = 1024)]
    pub encoder_tpr: u32,
}

Optional Fields

Fields with Option<T> type automatically default to None (no #[config(default)] needed):

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct SensorConfig {
    #[serde(default = "SensorConfig::default_rate")]
    #[config(default = 100.0)]
    pub update_rate_hz: f32,

    // Automatically defaults to None
    pub calibration_file: Option<String>,
    pub serial_number: Option<String>,
}

Complex Default Values

You can use any expression that compiles:

use std::path::PathBuf;

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct AppConfig {
    #[serde(default = "AppConfig::default_data_dir")]
    #[config(default = PathBuf::from("/var/lib/mecha10"))]
    pub data_dir: PathBuf,

    #[serde(default = "AppConfig::default_workers")]
    #[config(default = std::thread::available_parallelism().map(|n| n.get()).unwrap_or(4))]
    pub worker_threads: usize,
}

Migration Guide

To migrate existing configs to use ConfigDefaults:

  1. Add ConfigDefaults to the derive list:

    #[derive(Debug, Clone, Serialize, Deserialize, ConfigDefaults)]
    //                                              ^^^^^^^^^^^^^^
    
  2. For each field with a default, add #[config(default = value)]:

    #[serde(default = "TypeName::default_field")]
    #[config(default = value)]  // Add this
    pub field: Type,
    
  3. Update serde default path from "default_field" to "TypeName::default_field":

    // Before:
    #[serde(default = "default_fps")]
    
    // After:
    #[serde(default = "CameraConfig::default_fps")]
    
  4. Remove the separate default_*() function definitions

  5. Remove the manual impl Default block

Best Practices

Document Default Values

Include default values in field documentation:

/// Frame rate in Hz (default: 30.0)
#[serde(default = "CameraConfig::default_fps")]
#[config(default = 30.0)]
pub fps: f32,

Use Constants for Magic Numbers

For commonly used values, define constants:

const DEFAULT_I2C_ADDRESS: u8 = 0x28;
const DEFAULT_UPDATE_RATE: f32 = 100.0;

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct ImuConfig {
    #[serde(default = "ImuConfig::default_i2c_address")]
    #[config(default = DEFAULT_I2C_ADDRESS)]
    pub i2c_address: u8,

    #[serde(default = "ImuConfig::default_update_rate")]
    #[config(default = DEFAULT_UPDATE_RATE)]
    pub update_rate_hz: f32,
}

Group Related Defaults

Organize config structs by functionality:

#[derive(ConfigDefaults, Serialize, Deserialize)]
pub struct CameraConfig {
    // Resolution settings
    #[config(default = 640)]
    pub width: u32,

    #[config(default = 480)]
    pub height: u32,

    // Performance settings
    #[config(default = 30.0)]
    pub fps: f32,

    // Optional features
    pub enable_depth: Option<bool>,
}

Limitations

  1. Serde attributes still required: You still need to add #[serde(default = "TypeName::default_field")] to each field. The macro can't automatically inject these attributes (Rust limitation).

  2. Simple expressions only: The #[config(default = expr)] value must be a valid Rust expression that can be evaluated at the definition site.

  3. Named fields only: The macro only works with structs that have named fields (not tuple structs or unit structs).

Future Enhancements

Planned features for future versions:

  • Validation attributes: #[config(min = 0.0, max = 10.0)]
  • Range checking: #[config(range = "0.1..=1.0")]
  • Custom validators: #[config(validate_with = "my_validator")]
  • Auto-documentation: Generate config schema files

See Also

Commit count: 0

cargo fmt