| Crates.io | mecha10-macros |
| lib.rs | mecha10-macros |
| version | 0.1.46 |
| created_at | 2025-11-03 23:24:13.450568+00 |
| updated_at | 2026-01-25 23:00:36.508868+00 |
| description | Procedural macros for the Mecha10 robotics framework |
| homepage | |
| repository | https://github.com/mecha10/mecha10 |
| max_upload_size | |
| id | 1915412 |
| size | 62,809 |
Procedural macros to reduce boilerplate in Mecha10 configuration structs.
Writing configuration structs in Mecha10 typically involves a lot of repetitive code:
default_*() functions for each field#[serde(default = "default_*")] attributes on every fieldDefault trait implementationThe ConfigDefaults derive macro eliminates most of this boilerplate.
Add to your Cargo.toml:
[dependencies]
mecha10-macros = { path = "../mecha10-macros" }
serde = { version = "1.0", features = ["derive"] }
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:
Default impl when adding fieldsuse 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:
Default impl neededdefault_*() function definitionsOption fields automatically default to NoneThe ConfigDefaults macro generates:
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 }
}
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,
}
}
}
#[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,
}
#[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,
}
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>,
}
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,
}
To migrate existing configs to use ConfigDefaults:
Add ConfigDefaults to the derive list:
#[derive(Debug, Clone, Serialize, Deserialize, ConfigDefaults)]
// ^^^^^^^^^^^^^^
For each field with a default, add #[config(default = value)]:
#[serde(default = "TypeName::default_field")]
#[config(default = value)] // Add this
pub field: Type,
Update serde default path from "default_field" to "TypeName::default_field":
// Before:
#[serde(default = "default_fps")]
// After:
#[serde(default = "CameraConfig::default_fps")]
Remove the separate default_*() function definitions
Remove the manual impl Default block
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,
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,
}
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>,
}
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).
Simple expressions only: The #[config(default = expr)] value must be a valid Rust expression that can be evaluated at the definition site.
Named fields only: The macro only works with structs that have named fields (not tuple structs or unit structs).
Planned features for future versions:
#[config(min = 0.0, max = 10.0)]#[config(range = "0.1..=1.0")]#[config(validate_with = "my_validator")]