13

I have an enum:

enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

I want to ensure at compile time that every enum variant is handled in the from function.

Why do I need this? For example, I might add a Product operation and forget to handle this case in the from function:

enum Operation {
    // ...
    Product,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        // No changes, I forgot to add a match arm for `Product`.
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

Is it possible to guarantee that match expression returns every variant of an enum? If not, what is the best way to mimic this behaviour?

2 Answers2

16

A solution would be to generate the whole enumeration, variants and translation arms with a macro:

macro_rules! operations {
    (
        $($name:ident: $chr:expr)*
    ) => {
        #[derive(Debug)]
        pub enum Operation {
            $($name,)*
        }
        impl Operation {
            fn from(s: &str) -> Result<Self, &str> {
                match s {
                    $($chr => Ok(Self::$name),)*
                    _ => Err("Invalid operation"),
                }
            }
        }
    }
}

operations! {
    Add: "+"
    Subtract: "-"
}

This way adding a variant is trivial and you can't forget a parsing. It's also a very DRY solution.

It's easy to extend this construct with other functions (for example the inverse translation) that you'll surely need later and you won't have to duplicate the parsing char.

playground

Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
10

While there is certainly a complicated — and fragile — way to inspect your code with procedural macros, a much better path is to use tests. Tests are more robust, much faster to write, and will verify the circumstances in which each variant is returned, not just that it appears somewhere.

If you are concerned that the tests might continue to pass after you add new variants to the enum, you can use a macro to ensure that all cases are tested:

#[derive(PartialEq, Debug)]
enum Operation {
    Add,
    Subtract,
}

impl Operation {
    fn from(s: &str) -> Result<Self, &str> {
        match s {
            "+" => Ok(Self::Add),
            "-" => Ok(Self::Subtract),
            _ => Err("Invalid operation"),
        }
    }
}

macro_rules! ensure_mapping {
    ($($str: literal => $variant: path),+ $(,)?) => {
        // assert that the given strings produce the expected variants
        $(assert_eq!(Operation::from($str), Ok($variant));)+

        // this generated fn will never be called but will produce a 
        // non-exhaustive pattern error if you've missed a variant
        fn check_all_covered(op: Operation) {
            match op {
                $($variant => {})+
            };
        }
    }
}

#[test]
fn all_variants_are_returned_by_from() {
   ensure_mapping! {
      "+" => Operation::Add,
       "-" => Operation::Subtract,
   }
}
Peter Hall
  • 53,120
  • 14
  • 139
  • 204