How to call trait functions that are implemented by enum variant structs without using a 'match'

Hello Rust Community!

I am working on this code that has an enum

Enum TestCaseStatus{
    Failure(FailureStruct)
    Error(ErrorStruct)
    Skip(SkipStruct)
}

Now all of the structs in the enum (FailureStruct, ErrorStruct, SkipStruct) implement a trait called QueryT.

trait QueryT {
     fn get_status(self) -> String; 
}

Now whenever I want to call get_status I am having to use a match statement

let status = TestCaseStatus::Failure::new(); 
let status_string = match status {
    TestCaseStatus::Failure(struct) => struct:.get_status(), 
    TestCaseStatus::Error(struct) => struct.get_status(), 
    TestCaseStatus::Skip(struct) => struct.get_status()
}

Now I know this is the correct way to do it, and there is no problem with that. However, I am finding my match statements are getting pretty big and repetitive. Is there a way I can call the trait functions without having to deconstruct the enum every time?

I know enum variants in cannot be inferred as types in rust, could there be a way around this?

Note: I cant implement the trait on the entire enum because QueryT doesnt look like how its above (i have simplified it). I am an external crate, and QueryT cannot be implemented on an enum for certain reasons.

Not sure if this is useful to your use-case, but if you can't implement QueryT on TestCaseStatus (probably because both QueryT and TestCaseStatus are foreign types to your crate), you could create your own trait AsQueryT and implement it on TestCaseStatus like this:

struct Failure;
struct Error;
struct Skip;

trait QueryT {
    fn get_status(&self) -> String {
        format!("default implementation")
    }
}

trait AsQueryT {
    type Output;
    
    fn as_query_t(self) -> Self::Output;
}

impl QueryT for Failure {}
impl QueryT for Error {}
impl QueryT for Skip {}

enum TestCaseStatus {
    Failure(Failure),
    Error(Error),
    Skip(Skip),
}

impl<'a> AsQueryT for &'a TestCaseStatus {
    type Output = &'a dyn QueryT;
    
    fn as_query_t(self) -> Self::Output {
        match self {
            TestCaseStatus::Failure(f) => f,
            TestCaseStatus::Error(e) => e,
            TestCaseStatus::Skip(s) => s,
        }
    }
}

fn main() {
    println!("{}", TestCaseStatus::Failure(Failure).as_query_t().get_status());
    println!("{}", TestCaseStatus::Error(Error).as_query_t().get_status());
    println!("{}", TestCaseStatus::Skip(Skip).as_query_t().get_status());
}

Playground.

This way you can call the as_query_t() method on TestCaseStatus, which returns a trait object you can call every method of QueryT on

3 Likes

The flip side of your own trait is your own type.

// Sketch, untested
#[derive(Copy, Clone)] // ?
struct LocalTest(TestCaseStatus);
impl QueryT for LocalTest {
    fn get_status(self) -> String {
        match self.0 {
            // the boilerplate you're used to
        }
    }
}
// ...
let status_string = LocalTest(status).get_status();

There's no way around the match being somewhere more generally though, at least once. If you're calling multiple methods without changing the variant, you could perhaps convert to or temporarily borrow a dyn QueryT. (However this may not work depending on the trait and whether or not Box<dyn QueryT> implements the trait, say.)

4 Likes

Thank you for the response! The solution sounds pretty good, but I am not able to implement it because my trait QueryT has an associated const, so rust is panicking with error QueryT cannot be made into an object because it contains this associated const``

Thank you for your help! The problem is that I am using TestCaseStatus as a parameter in another struct. So, I will have to end up using LocalTest and I will end up with alot of nesting.

Your example had a method taking self by value and that in part shaped my response. Is that accurate? If so, is the enum Copy?

If it is Copy, no big deal to just create a copy within the wrapper every time you need to call a method too (even if you only have a & or &mut yourself). Make a helper trait or macro if you like.

If it's not Copy, you're giving away ownership when you make the (self-taking) method call so it doesn't matter if you wrapped it right before that.

If the methods actually take &self or &mut self and you have ownership, you can wrap and then unwrap with wrapped.0 (i.e. with an ergonomic hit).

If the methods actually take &self or &mut self and you have a & or &mut, you can make newtypes for those instead.


In short you have options, but which are viable or best depends on the details.