A good way to impl FromStr for enum?

I have an enum, which contains values and stores in data in custom format.
I can convert it from the string manually, even without regex, but I'm interested, if it is possible to do the same with the match expression and, hopefully, with full enum variants completion check?

#[derive(Debug)]
pub enum NoteNotations {
    Voice(u8),
}
impl ToString for NoteNotations {
    fn to_string(&self) -> String {
        match self {
            Self::Voice(idx) => format!("voice:{}", idx),
        }
    }
}
impl FromStr for NoteNotations {
    type Err = Box<dyn Error>;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        if s.starts_with("voice") {
            let idx: u8 = s.split(":").collect::<Vec<&str>>()[1].parse()?;
            Ok(Self::Voice(idx))
        } else {
            Err(format!("Can not parse {}", s).into())
        }
    }
}

P.S. Also, I'm not sure in Err type for FromStr implementation

Replace if with match. But not likely to do so. match is for once you have constructed the enum.

The error type is of your own choosing and so many choices. One could be returning an error enum for the three errors. 1. Err at end. 2. ? of parse. 3. the panic currently caused by [1] should ideally be replaced by e.g. get(1).ok_or(...)?

1 Like

Hi! Using the serde crate can simplify the process of serializing and deserializing your enum in Rust. It can automatically generate the code for serializing and deserializing the enum and handle custom serialization and deserialization formats. serde is a commonly used crate and is generally considered a good practice for serializing and deserializing enums in Rust.

1 Like

You don't really need serde to parse this. I think you're off to a good start. The main things I note are

  • There's no need to collect the whole iterator
  • Your indexing could panic if there's no :
  • It's nice if errors give you an idea on how to fix things
  • If you're concerned about the error type, you can add some future-proofing with a thin newtype around String (or Box<dyn Error /* + Send + Sync */> or whatever)

Here's something I threw together.


There is no way to check that a function has some set of return branches that covers your entire enum. Crates like strum can take remove some of the tedium, and indirectly provide ways for you to test your implementation against every variant.

3 Likes

Thank you all a lot!

Considering indexing — maybe you're right, but this string will always be constructed by enum, so, if it's wrong — something has gone really bad, that's why I used indexing.

Considering collect: "voice:\d" is really string it will receive. These are already parsed tokens, which have predictable form.
In the earlier stages I used constructions of kind:

let mut i = token_string.split();
i.next();
let token = i.next();

Note that of course since FromStr::from_str can accept any string, it needs to handle any potential string somehow, not just the well-formed strings you're intending to give it. Nothing's stopping you from doing e.g. "voice".parse::<NoteNotations>() or feeding in user input, so it's important that you accept malformed input and return Err in that case.

If from the place using this functionality you've already validated that the string is in the proper format (e.g. because it came from formatting the same enum), you can .unwrap() or .expect("should be valid note notation") the result.

Generally, though, Rust style generally strongly favors it when you Parse, Don't Validate. That article is written against Haskell, but the concept applies to any language.

That's a major thing Result exists to support. Instead of validating something like

if !is_note_notation(s) {
    eprintln!("invalid note notation");
    return;
}
let note = Note::from_valid_str(s);

you instead have from_str returning Result to parse a potentially ill-formed string like

let Ok(note) = Note::from_str(s)
else {
    eprintln!("invalid note notation");
    return;
}

where the process of checking the string is in the expected format and extracting the relevant information in a usable structure are one operation.

Here I discarded the error information provided in the Err case, but you can just as well utilize it, e.g.

let note = match Note::from_str(s) {
    Ok(note) => note,
    Err(e) => panic!("should be valid note notation: {e}"),
}

and most of the functionality on Result is to make common patterns; this is essentially the same as

let note = Note::from_str(s).expect("should be valid note notation");
5 Likes