Patterns for robustness principle

⚓ Rust    📅 2025-07-30    👤 surdeus    👁️ 10      

surdeus

Warning

This post was published 126 days ago. The information described in this article may have changed.

I 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:

  1. Make it impossible for me to implement traits for it due to the orphan rule.
  2. 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

Read full topic

🏷️ Rust_feed