Patterns for robustness principle
⚓ Rust 📅 2025-07-30 👤 surdeus 👁️ 10I am currently implementing the Zigbee Cluster Library in Rust.
In the protocol, there are various numeric values, which are either limited in range or represent enum values in a subset of the range of the respective integer type. E.g.:
use crate::units::Mireds;
pub const PREVIOUS: u16 = 0xffff;
/// The startup color temperature to use.
pub enum StartupColorTemperature {
/// Set the color temperature to the specified value in mireds.
Value(Mireds),
/// Use the previous color temperature value.
Previous,
}
impl From<StartupColorTemperature> for u16 {
fn from(value: StartupColorTemperature) -> Self {
match value {
StartupColorTemperature::Value(mireds) => mireds.into(),
StartupColorTemperature::Previous => PREVIOUS,
}
}
}
impl TryFrom<u16> for StartupColorTemperature {
type Error = u16;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value == PREVIOUS {
Ok(Self::Previous)
} else {
Mireds::try_from(value).map(Self::Value)
}
}
}
Given the robustness principle, I want the user of my library only to be able to provide valid values.
On the other hand, I want to handle invalid values received from third-party devices gracefully.
Now, the above enum is used in a variant of another enum which identifies certain attributes that can be read and set:
/// Color information attribute for the Color Control cluster.
///
/// TODO: Add respective associated data.
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
#[repr(u16)]
#[derive(FromLeStreamTagged)]
pub enum ColorInformationAttribute {
<SNIP>
/// The desired startup color temperature in mireds.
StartUpColorTemperature(MaybeStartupColorTemperature) = 0x4010,
}
As you can see, I did not use the type directly, since the underlying u16 value might not be a valid StartupColorTemperature which can be deserialized from it.
I considered using an Option here, but this would:
- Make it impossible for me to implement traits for it due to the orphan rule.
- Allow the user to provide
None, which is not what I want.
Hence, I implemented the following wrapper struct as you can see in above variant:
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd, FromLeStream, ToLeStream)]
#[repr(transparent)]
pub struct MaybeStartupColorTemperature(u16);
impl TryFrom<MaybeStartupColorTemperature> for StartupColorTemperature {
type Error = u16;
fn try_from(value: MaybeStartupColorTemperature) -> Result<Self, Self::Error> {
Self::try_from(value.0)
}
}
impl From<StartupColorTemperature> for MaybeStartupColorTemperature {
fn from(value: StartupColorTemperature) -> Self {
Self(value.into())
}
}
Since the Maybe* type is library-internal, this allows graceful parsing of potentially invalid data received, but forces the user of the library to specify a valid StartupColorTemperature (by e.g. calling .into() when instantiating the variant of ColorInformationAttribute.
My issue is, that I encounter this issue a lot, so I have a lot of these internal wrapper types.
Do you know of a better way to handle this kind of pattern?
Edit: Mireds is:
/// Represents a color temperature in mireds (micro reciprocal degrees).
#[derive(
Clone, Copy, Debug, Default, Eq, Hash, Ord, PartialEq, PartialOrd, FromLeStream, ToLeStream,
)]
#[repr(transparent)]
pub struct Mireds(u16);
impl Mireds {
/// Minimum value for mireds.
pub const MIN: u16 = 0x0000;
/// Maximum value for mireds.
pub const MAX: u16 = 0xffef;
}
impl From<Mireds> for u16 {
fn from(value: Mireds) -> Self {
value.0
}
}
impl TryFrom<u16> for Mireds {
type Error = u16;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if value <= Self::MAX {
Ok(Self(value))
} else {
Err(value)
}
}
}
5 posts - 3 participants
🏷️ Rust_feed