An idea I've been toying with is the addition of "resources" to Rocket. This would be similar to ActiveResource
in Rails. The motivation is that many apps are CRUD-like, and this would remove some of boilerplate of doing this as well as provide more structure when necessary.
As an example, consider a Todo
resource that be created, read, updated, and deleted. Here's what writing this might look like:
#[resource("todo")]
struct Todo {
description: String,
completed: bool
}
impl RocketResource for Todo {
type ReadResponse = DBResult<Either<Template, JSON<String>>>;
fn read(id: ID, format: ContentType) -> Self::ReadResponse {
let todo = try!(Database::find_todo(id));
if format.is_json() {
Either::B(JSON(todo.to_string()))
} else {
Either::A(Template::render("todo", &todo))
}
}
}
fn main() {
rocket::ignite().register("/", resources![Todo]).launch()
}
Note the associated type ReadResponse
. This can go away entirely once impl Trait
is allowed as a return type of trait methods. Then the return type can simply be impl Response
.
This example above would create four routes: POST todo/
, GET todo/<id>
, PATCH todo/<id>
, and DELETE todo/<id>
mapping to the four RocketResource
trait methods: create, read, update, delete
. PUT
should likely map to update
as well, though PATCH
is more appropriate. All of the RocketResource
methods should have default implementation that return a "not-implemented" error of some sort. In the example above, a GET todo/<id>
request would be routed to the read
method, running the logic in the example. Note also the ContentType
: resources will respond to any content-type, and the first content-type in the Accept
header will be passed in.
Here's a sketch of what the trait might look like:
trait RocketResource {
type CreateResponse = DefaultResponse;
type ReadResponse = DefaultResponse;
type UpdateResponse = DefaultResponse;
type DeleteResponse = DefaultResponse;
fn create(&self, format: ContentType) -> Self::CreateResponse {
DefaultResponse::new()
}
fn read(id: ID, format: ContentType) -> Self::ReadResponse {
DefaultResponse::new()
}
fn update(id: ID, format: ContentType) -> Self::UpdateResponse {
DefaultResponse::new()
}
fn delete(id: ID, format: ContentType) -> Self::DeleteResponse {
DefaultResponse::new()
}
}
Of note is the create
method which takes in an &self
. This is because Rocket will automatically parse the incoming request from forms and JSON. This means that the resource must implement FromForm
and Deserialize
. An alternative is to simply pass in Data
instead and let the user decide what to do. Another alternative is to pass in the accepted content-type to the resource
attribute: #[resource("todo", format = "application/json, text/html")]
. In this case, Rocket will return an error on any other kind of request format, and the user is guaranteed that ContentType
will always be one of those in the format
argument. And, the user will only have to implement FromForm
if they specify text/html
, and Deserialize
if they specify application/json
, which sounds nice.
Anyway, this is a developing thought. impl Trait
in method return types would make this really nice.