Enum (with values) with associated Enum

I'm looking for advice from experienced Rust users on how best to use the type system to express a certain set of constraints.

I have several kinds of actions my system supports, so I have an enum to represent them:

enum ApiAction {
    SetFoo,
    GetFoo,
    GetBar,
    ResetAll,
}

I pass this around in various circumstances to identify which action my system is handling.

I also have certain inputs needed (different inputs for each action). At first I created a struct to hold the input values:

struct ApiRequest {
    foo_id: Option<String>,
    new_foo_value: Option<String>,
    bar_id: Option<String>,
}

But I think I can do better! I can create an enum where each instance contains only the required. That's nifty, because now I can parse the input, then attempt to create an instance of ApiRequest and lower levels of my code which receive an ApiRequest object can be assured that all the required fields were present because otherwise we'd have reported an issue during parsing. It looks like this:

enum ApiRequest {
    SetFoo{foo_id: String, new_value: String},
    GetFoo{foo_id: String},
    GetBar{bar_id: String},
    ResetAll,
}

But I have one problem. When Rust creates an enum, it creates a discriminant to identify the values. In the case of this ApiRequest enum, the discrimant SHOULD be my ApiAction enum. As shown, there is nothing other than a naming convention that shows that ApiRequest::SetFoo is associated with ApiAction::SetFoo. I can't easily have the compiler verify that when the action is SetFoo the request is ALSO SetFoo.

Is there a straightforward and idiomatic way to do this?

1 Like

In Rust, enum variants can hold data, so you can do something like this:

enum ApiAction {
    SetFoo(ApiRequestGetFoo),
    GetFoo(ApiRequestGetFoo),
    GetBar(ApiRequestGetBar),
    ResetAll(ApiRequestResetAll),
}

struct ApiRequestSetFoo {
  foo_id: String, 
  new_value: String
}

struct ApiRequestGetFoo {
  foo_id: String
}

struct ApiRequestGetBar {
  bar_id: String
}

struct ApiRequestResetAll

Just to give you an idea. But at this point, I'm not really sure why do you want to keep these as sepparate enums.

But at this point, I'm not really sure why do you want to keep these as sepparate [sic] enums.

Good question. I was thinking I wanted to do that because sometimes I use ApiAction to parameterize things or pass it around to keep track of which action I am performing, and I am assuming I will need to be able to create instances of ApiAction without having actual request data.

Why have ApiAction and ApiRequest separated?

I am assuming that sometimes I will need to create an ApiAction instance for reasons that don't have to do with the input, and I expected to need to be able to create such instances without having to create dummy data. There is no default for ApiRequest instances.

If you really have to separate both and need to make sure the discriminants match you could do so explicitly:

Interesting! I didn't realize this was possible. Is it a good idea?

Never mind what I wrote, I tested it and it didn't work. However, you could implement PartialEq for ApiAction and ApiRequest. This allows you to compare both with the == operator:

use std::cmp::PartialEq;

#[derive(Debug)]
enum ApiAction {
    SetFoo,
    GetFoo,
    GetBar,
    ResetAll,
}

#[derive(Debug)]
enum ApiRequest {
    SetFoo{foo_id: String, new_value: String},
    GetFoo{foo_id: String},
    GetBar{bar_id: String},
    ResetAll,
}

impl PartialEq<ApiAction> for ApiRequest {
    fn eq(&self, other: &ApiAction) -> bool {
        match (self, other) {
            (ApiRequest::SetFoo{..}, ApiAction::SetFoo) => true,
            (ApiRequest::GetFoo{..}, ApiAction::GetFoo) => true,
            (ApiRequest::GetBar{..}, ApiAction::GetBar) => true,
            (ApiRequest::ResetAll, ApiAction::ResetAll) => true,
            _ => false,
        }
    }
}

impl PartialEq<ApiRequest> for ApiAction {
    fn eq(&self, other: &ApiRequest) -> bool {
        other.eq(self)
    }
}

fn main() {
    let request = ApiRequest::ResetAll;
    let action = ApiAction::ResetAll;
    
    assert_eq!(action, request);
}

Playground.

The strongest-typed way (I know of) doesn't involve any enums: it uses separate request types for which an associated type is the corresponding response. This makes it possible to make the request sending completely generic and type-safe.

Read more in my earlier post. A concrete example is ring_api::Client::send() and the related Request trait.

Got it. You could make use of of the Option type and do some additional magic with enums if you want a third way of doing things besides what @jofas and @H2CO3 have shown. Just to give you an idea of what you can do with enums, since that was your initial request and you might still learn a thing or two about them :slight_smile:

enum ApiAction {
    SetFoo {associated_request: Option<ApiRequestSetFoo>},
    GetFoo {associated_request: Option<ApiRequestGetFoo>},
    GetBar {associated_request: Option<ApiRequestGetBar>},
    ResetAll {associated_request: Option<ApiRequestResetAll>}
}

enum ApiRequest {
    SetFoo(ApiRequestGetFoo),
    GetFoo(ApiRequestGetFoo),
    GetBar(ApiRequestGetBar),
    ResetAll(ApiRequestResetAll),
}


struct ApiRequestSetFoo {
  foo_id: String, 
  new_value: String
}

struct ApiRequestGetFoo {
  foo_id: String
}

struct ApiRequestGetBar {
  bar_id: String
}

struct ApiRequestResetAll

Basically, what I just did was to decouple each of the ApiRequest enum variants into its own struct, which then can be associated with the ApiAction enum variants optionally.

1 Like

Thank you! I think this is similar to what I want to accomplish, except that I want the association to run the other way -- from an ApiRequest I want to find the corresponding ApiAction. That ends up being even easier, because I don't need to use the Option<> -- the values of ApiAction can always be constructed.

Thank you - the example was particularly helpful.

I see - I've learned something new, in that I never considered trying to do that.

It's a bit misleading though -- I don't think I did a good job of explaining what I was seeking. You provided a way of testing whether an ApiAction and an ApiRequest "matched", which might be very useful for some purposes. (It might even come up later in what I'm working on!)

But after seeing this I realize that my REAL goal was to find the ApiAction given an ApiRequest. Which, honestly, is a far simpler need than I had realized. The answers I got here helped me to work that out. Thanks very much for the help.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.