Search code examples
rusttypesenumsidiomsprimitive

In Rust, what is the idiomatic way to associate members of one enum with other enum types and map between their integer and string representations?


Goal:

  • I'd like to turn a pair of (usagePage, usageID) integers to Rust enumeration variants and get the names of both the page and specific usages as strings for debugging purposes.
  • I'd like to define this in the most idiomatic way.
  • I'd like to reuse the enums provided by the usbd_human_interface_device::page module.
  • I'd like this mapping to fail gracefully - that is, given integers out of range of both enums, I'd like to detect that and return strings indicating that the mapping was not found.
    • This failure case could be a failure to look up the usagePage or a failure to look up the usageID within the correct enumeration.

Concrete:

  • I'm using the enum types from usbd_human_interface_device::page, namely Consumer, Desktop, Game and the like.
  • That library does not provide an enum for each page, so I've written HIDPage below.
  • I'd like to turn a pair of integers into a HIDPage variant and the associated page-specific varient, such as Desktop::SystemPowerDown
  • Each of those enum types are used when a HIDPage numeric enum variant specifies to do so. That is, given the tuple of integers (1, 129) I'd like to return (HIDPage::Desktop,Desktop::SystemPowerDown).

Challenges:

  • The type of enumeration must be determined at runtime, as the enum to look up depends on an integer that is not known until then.
    • This implies that the second return value is generic over the set of enums I'd like to return, but I'm not sure how to write that. I tried defining a new trait that each enum implements but I couldn't get that working.
  • I'd like to return the enum type directly, but I think that's not possible as enums are not types. https://stackoverflow.com/a/72438660/84041 seemed to have a similar question, but I'm not sure how to apply it here.
  • I'd like to not be overly verbose in this mapping - I have a solution below, but it seems overly verbose. Is there a better way to write this?

Abstract:

  • Is this the most idiomatic way to do this in Rust?
// My HIDPage enum implementation
use num_enum::{TryFromPrimitive};

#[repr(u16)]
#[derive(
    Debug,
    Copy,
    Clone,
    Eq,
    PartialEq,
    Ord,
    PartialOrd,
    Hash,
    TryFromPrimitive,
)]
pub enum HIDPage {
    Desktop = 1,
    Simulation = 2,
    Game = 5,
    Keyboard = 7,
    Telephony = 0x0b,
    Consumer = 0x0c,
}
use usbd_human_interface_device::page::*;

// This enumeration, plus HIDPage, seem overly verbose
#[derive(Debug)]
pub enum HIDEnum {
    Desktop(Desktop),
    Simulation(Simulation),
    Game(Game),
    Keyboard(Keyboard),
    Telephony(Telephony),
    Consumer(Consumer),
}


fn new_usage_from_ints(page:u16 , id: u16) -> Result<String, Box<dyn Error>> {
    let knownPage: Option<HIDPage> = HIDPage::try_from(page as u16).ok();

    if let Some(knownPage) = knownPage.as_ref() {
        let knownId: Option<HIDEnum> = match knownPage {
            // Each line requires repeating the type (in different contexts) three times
            HIDPage::Desktop => Desktop::try_from(id as u8).ok().map(|v| HIDEnum::Desktop(v)),
            HIDPage::Simulation => Simulation::try_from(id as u8).ok().map(|v| HIDEnum::Simulation(v)),
            HIDPage::Game => Game::try_from(id as u8).ok().map(|v| HIDEnum::Game(v)),
            HIDPage::Keyboard => Keyboard::try_from(id as u8).ok().map(|v| HIDEnum::Keyboard(v)),
            HIDPage::Telephony => Telephony::try_from(id as u8).ok().map(|v| HIDEnum::Telephony(v)),
            HIDPage::Consumer => Consumer::try_from(id as u16).ok().map(|v| HIDEnum::Consumer(v)),
        };
        return Ok(format!("nui {:?}/{:?} (0x{:x}/0x{:x})", knownPage, knownId, page, id));
    }
    return Ok(format!("nui {:?} (0x{:x}/0x{:x})", knownPage, page, id));
}


Solution

  • A better solution may be to have just the HIDEnum (called just HID below) and manually implement TryFrom or just have an inherent function for it.

    pub enum HID {
        Desktop(Desktop),
        Simulation(Simulation),
        Game(Game),
        Keyboard(Keyboard),
        Telephony(Telephony),
        Consumer(Consumer),
    }
    
    impl HID {
        pub fn parse(page: u16, id: u16) -> Result<HID, Box<dyn Error>> {
            let out = match page {
                1 => HID::Desktop(Desktop::try_from(u8::try_from(id)?)?),
                2 => HID::Simulation(Simulation::try_from(u8::try_from(id)?)?),
                5 => HID::Game(Game::try_from(u8::try_from(id)?)?),
                7 => HID::Keyboard(Keyboard::try_from(u8::try_from(id)?)?),
                0x0b => HID::Telephony(Telephony::try_from(u8::try_from(id)?)?),
                0x0c => HID::Consumer(Consumer::try_from(id)?),
                _ => Err(UnknownPageError)?,
            };
            Ok(out)
        }
    }
    

    But if you want to keep things more declarative, you can use a separate enum for the page ID and map from that to HID:

    #[repr(u16)]
    #[derive(
        Debug,
        Copy,
        Clone,
        Eq,
        PartialEq,
        Ord,
        PartialOrd,
        Hash,
        TryFromPrimitive,
    )]
    pub enum PageId {
        Desktop = 1,
        Simulation = 2,
        Game = 5,
        Keyboard = 7,
        Telephony = 0x0b,
        Consumer = 0x0c,
    }
    
    impl HID {
        pub fn parse2(page: u16, id: u16) -> Result<HID, Box<dyn Error>> {
            let page = PageId::try_from(page)?;
    
            let out = match page {
                PageId::Desktop => HID::Desktop(Desktop::try_from(u8::try_from(id)?)?),
                PageId::Simulation => HID::Simulation(Simulation::try_from(u8::try_from(id)?)?),
                PageId::Game => HID::Game(Game::try_from(u8::try_from(id)?)?),
                PageId::Keyboard => HID::Keyboard(Keyboard::try_from(u8::try_from(id)?)?),
                PageId::Telephony => HID::Telephony(Telephony::try_from(u8::try_from(id)?)?),
                PageId::Consumer => HID::Consumer(Consumer::try_from(id)?),
            };
            Ok(out)
        }
    }