Are huge enums a good way of handling requests in a server?

I am writing a server, that offers dozens of API funtions. The number is likely to get bigger in the future. At the moment I am porting the request handling to Rust and I am struggling what the most idiomatic, safe, flexible or future-proof solution is.

My server is basically doing this for each request:

  1. Decode request
  2. Check authorization
  3. Execute request
  4. Encode answer
  5. Create log string

One easy and powerful solution I can see is just building huge enums, that contain all necessary information for one step. I like that a lot, except the huge match blocks that will be necessary. Let's assume this architecture:

enum Request {
    DoSomething { param1: u32 },
    DeleteEntry { id: [u8; 16] },
}

enum Answer {
    BufferAnswer { buffer: Vec<u8> },
    ValueAnswer { value: u32 },
}

enum UserRole {
    Admin,
    Elevated { val1: u32 },
    User { val1: u32, val2: u32 },
}

I could now have functions like:

fn decode_request() -> Request
fn encode_answer(answer: &Answer)

that encapsulate the wire protocol (which is not relevant for the server logic).

Checking the authorization and executing the request would just give a enormous match. I probably would need to access some of the role values during this. So I probably would end up with something like this:

    match (request, role) {
        (
            Request::DoSomething { param1 },
            UserRole::Elevated { val1 } | UserRole::User { val1, val2: _ },
        ) => {
            execute_do_something(param1, val1);
        }
    }

Is this good idiomatic Rust? Is it efficient? Are there other ways to do it?

Yes, this is idiomatic Rust, and yes it is efficient. One way to structure it that you may find more readable is the following:

enum Request {
    DoSomething(DoSomethingRequest),
    DeleteEntry(DeleteEntryRequest),
}

impl Request {
    fn evaluate(self) {
        match self {
            Request::DoSomething(inner) => inner.evaluate(),
            Request::DeleteEntry(inner) => inner.evaluate(),
        }
    }
}
struct DoSomethingRequest {
    param1: u32,
}

impl DoSomethingRequest {
    fn evaluate(self) {
        ...
    }
}
struct DeleteEntryRequest {
    id: [u8; 16],
}

impl DeleteEntryRequest {
    fn evaluate(self) {
        ...
    }
}

It is possible to eliminate the above match statement with the enum_dispatch crate, but I'm not sure I would bother with an extra crate if it was me.

1 Like

Thanks a lot :slight_smile:

I have a system with a gazillion message types. I started with an enum, but the match blocks got pretty big, so I switched to a trait. Type erasure made handling messages awkward until I learned that serde could deserialize directly to a message of the correct type.

Thanks, I'll keep that in mind.

I am probably thinking in all the wrong directions, but I don't understand the second part of your message. How do you deserialize in all the different implementations of the trait? Do you know the message type before you start deserializing?

There is a crate typetag that inserts the type into the serialization and uses it when deserializing. Then I can just

let msg: Box<dyn Message> = serde_json::from_str(&serialized)?;
msg.process();

where msg.process() is a trait method that gets called on the specific implementation.

1 Like

Since different requests and endpoints typically don't have much to do with each other, I rarely find the "many of one kind" approach of enums fitting. Instead, I like to use distinct types and a unified Request and Response trait, which makes it possible to handle requests and generate responses in a type-safe manner, and it also makes it possible to decouple the logic of each endpoint from the others.

Here's an earlier post of mine that explains the approach in more detail and links to some real-world example code.

1 Like

OK that is really valuable input. Thank you all :slight_smile:

I thought a good bit about it now and while I totally agree that this sum-of-all-requests type feels clumsy, it still is really flexible. Especially when writing a custom decoder, as I am forced to.

I am implementing a proprietary binary protocol, that is quite complex at times. Sometimes the request type can only be determined after a lot of decoding. So at the moment the enum version seems like the most straight forward one to me, but I will keep all the other ideas in mind.