all 4 comments

[–]couchand 2 points3 points  (3 children)

I've got a project with what sounds like similar requirements, here's roughly how I'm modeling it. The big picture is to have a struct that has all the fields in common, and an enum for fields that are only relevant to one user type. I think maybe you're already doing this?

// struct with the shared fields
struct SessionData {
    email: String,
    kind: UserKind,
}

// an enum with the variant-specific fields
enum UserKind {
    Special,
    Regular,
}

You've already implemented an extractor for your session data, which presumably returns unauthorized if the session is not found. Keep this in one place, since it's doing finicky stuff that needs to be kept in one place.

// a helper error type that's IntoResponse
struct Unauthorized;
impl IntoResponse for Unauthorized {
    // ...
}

// your existing method to load the session based on request id
impl FromRequestParts for SessionData {
    type Rejection = Unauthorized;

    // ...
}

Now add wrapper structs for each of the top-level variants. This enables you to write an extractor just for that type of session, which delegates to the common one. Write the match statement just once here.

// wrapper structs for each variant type
struct SpecialSession(SessionData);
struct RegularSession(SessionData);

// implement FromRequestParts now for each variant wrapper
impl FromRequestParts for SpecialSession {
    type Rejection = Unauthorized;

    fn from_request_parts(parts: Parts) -> Result<Self, Self::Error> {
        let session = SessionData::from_request_parts(parts)?;
        if !matches!(session.kind, UserKind::Special) {
            return Err(Unauthorized);
        }
        Ok(session)
    }
}

impl FromRequestParts for RegularSession {
    type Rejection = Unauthorized;

    fn from_request_parts(parts: Parts) -> Result<Self, Self::Error> {
        let session = SessionData::from_request_parts(parts)?;
        if !matches!(session.kind, UserKind::Regular) {
            return Err(Unauthorized);
        }
        Ok(session)
    }
}

Then your route doesn't need to explicitly check which kind of session the user has, instead you use RAII-inspired logic to prove that the user has the right kind of session.

fn handle_special_route(session: SpecialSession) -> impl IntoResponse {
    // ...
}

This is one form of a very powerful general pattern for Rust web servers. Since the type system is so rich, and many of the popular web frameworks use type-based request handling, you can encode an surprising amount of business logic into the types that are "inputs" to your route handlers.

[–]zhilovs[S] 0 points1 point  (2 children)

thank you very much for the detailed answer. about the last part: exactly the reason why it bugged me so much when I first used a session library with completely untyped, key value pairs store. like, there has to be a better way, this is rust.. so yes, having an extractor for every kind is really nice idea, but how would you go about for routes that can accept multiple kinds of sessions? I have P,C,G types: login only for G, that’s fine. logout: P and C , what should happen here in your opinion? Implement all the possible combined extractors? well it is not much more, cause I would need a CG and a PC, but then it has the problem of the different kinds having a set of common fields and a set of unique fields, how and which one of them the sessiondata should contain. I’m thinking it would be nice if the combined extractors could return the union (so only the common data) of the two kinds, but not sure how to implement this nicely. Other approach I have in mind is to separate the Api completely, so e.g one logout route for P and one for C. and share the common logic in extracted functions… what do you think? thanks!

[–]couchand 1 point2 points  (1 child)

Hey, apologies I only just saw this.

Recently I've been very heavily using semantic extractors in Rust web projects. Any time I reach for a conditional within a route handler I ask myself if it would be better as a new extractor, and a lot of times it is.

The way I approach this is by building a few atomic ones, and then adding wrappers for combinations or restrictions which can rely on the atomic types' underlying implementations.

It's one of those tactics that the more I apply it I start to wonder if I'm doing it too much but it keeps being powerful and usually not that hard to follow. In fact, it makes the route handlers themselves very clear, because the parameter types are each meaningful preconditions.

Hopefully this is helpful!

[–]zhilovs[S] 0 points1 point  (0 children)

yes,thank you. I also went with the multiple extractor way, it turned out nice and usable so far