5 ms·
This is fantastic advice, and provides a very nice UX when coding against it. Just wondering, are you trying to protect some "inside" aspect (or allow graceful
by anw 7y ago
This is fantastic advice, and provides a very nice UX when coding against it.
Just wondering, are you trying to protect some "inside" aspect (or allow graceful degradation) by having 500 as "Unavailable" rather than "Internal Server Error"? Also, I take it this code returns a body of { status: ..., message: ... } with a status of HTTP 500? So the enum variant "Unavailable" is just for programming syntax and unwraps around the Json<T> inner body when it's called by Rocket?
Finally, I see you are making your diesel calls inside the controller. Is there a reason to prefer this over creating methods that impl your structs that can do the same thing? Such as:
impl User {
pub fn read_all(con: &PgConnection) -> Result<Self> {
User::read_all().get_results::<Self>(con)?
}
}
rather than (in the controller)
User::read_all().get_results::<User>(&*connection).map_err(|_| Status::Unavailable)?
I'm just getting in to Rust, and have been dabbling with Rocket and Diesel a bit lately. So this is a pretty interesting thread and like seeing how and why people are using it :)
- status_quo69 7y agoSo the answer to both of those things is that I basically hammered out a bit of code that looks sort of like what I have but it's exactly. It should have been a 503 error instead. In reality, the main thought is that the map_err in the controller should take some error and match against it for a known set of Error enums to translate to an enum that we want to get back out to the user. > Also, I take it this code returns a body of { status: ..., message: ... } with a status of HTTP 500? So the enum variant "Unavailable" is just for programming syntax and unwraps around the Json<T> inner body when it's called by Rocket? Yep! Exactly. It's a bit wonky but I thought it would be helpful to show off, especially in a world where JSON bodies with a shape of { status: "error" ... } are so common. In reality you can shove whatever you want in there and use serde to flatten the result. Off the top of my head, you could in theory do something like pub struct UserApiErrorMessage { status: String, message: String, #[serde(flatten)] _meta: HashMap<...> } Where _meta holds some additional information you might want to dump to the user. In practice, I'm using this error enum approach to construct JSONAPI responses with appropriate status codes and the appropriate structure. > Finally, I see you are making your diesel calls inside the controller. So what I've found is that generally you actually don't want to embed your diesel get_result(s) calls inside your impls for your struct, since you're loading the entire set up into memory at once and you lose the ability to cut down. I've leveraged the `into_boxed` method of the query builder pretty heavily to allow for building up queries on the fly, which allows me to abstract the common bits into the impl. Code shows better than words so here we go: struct User { .. } type WithUsername<'a> = Eq<users::username, String>; impl User { pub fn read_all<'a>() -> BoxedQuery<'a, Pg> { users::table.order(users::created_at).into_boxed() } fn with_username(username: &String) -> WithUsername { users::username.eq(username) } } // In some method somewhere. Note: I don't know that this actually works out of the box because I didn't compile it User::read_all().filter(User::with_username(&username)).paginate(1).per_page(25).get_results::<User>()? So I've cooked up a sort-of ORM for this, but I've abstracted away that underlying schema.rs file that's a huge part of diesel, since I think (but haven't proven) that leaking that file out into the rest of the codebase makes things tougher in the long run. As to your point about the controller, I'd advise against it but hacked something together for the comment. In my project, I actually call out to simple services (think shitty service oriented architecture) that query the database and return a Result. What's interesting (or not) here is that my services actually call the `Into::into` of the database model, so my controller never even sees that they exist. Again, some code: // Actually in some other file mod service { mod users { pub fn get_user_page(conn: &PgConnection) -> Result<Vec<UserApiResponse>> { let users = User:read_all().paginate(1).per_page(25).get_results::<User>()?; users.iter().map(Into::into).collect::<Vec<_>>() } } } #[get("/users")] fn index(connection: &PgConnection) -> Result<Json<Vec<UserApiResponse>>> { service::users::get_user_page(&*connection).map(Json).map_err(...) } But now we come to what I think is the most pertinent question: why does this exist? Testing. Spinning up a DB in CI is quite easy but since rust runs all testing in parallel (yay fearless concurrency!), we can get into trouble when we delete some records from the database but we haven't set up our tests to handle that. I ran into some really stupid problems I caused for myself in assuming that somehow it would be like rspec. If you can basically abstract away the database access part into a dumb hashmap, you get a store per test that won't experience much contention (at least that's the hope). I'm still working on that last bit so my thoughts on it are far and away from complete. EDIT: Final note on diesel: there is no into_boxed for inserts and into_boxed for updates is dark magic that I can never get working correctly, so I'd avoid being too clever with these and focus on Select/Delete instead
- steveklabnik 7y agoNot offtopic, but not quite the meat of your post: I would love to see a good solution for a drop-in JSON-API server library. Dunno how close what you're building is to that, but it would be very very very cool!
- status_quo69 7y agoSomething in the vein of juniper but for JSON:API? That would be incredibly useful! I'm not at that stage yet, mainly hacking on https://github.com/zacharygolba/json-api-rs https://github.com/zacharygolba/json-api-rs to bring it up to rust 2018 and add some more niceties as well as bringing the rocket integration up to the async branch. What I'm working with right now is more of a pattern. It's a bit rough still and allows for too much recursion and N + 1 queries galore, but since my usual queries from my front-end are for single items it's been pretty nice. Maybe one day I'll be more up to snuff on macros to expand upon that DSL. I'm already starting to see some rough patches in my very tiny pattern with relationships