Creating our first endpoint
Now that our application is set up, its time to actually implement some behavior. We're going to write the single endpoint we described earlier:
GET /ping/$slug
Committing
Before we get to that though, its a good idea to commit your repository so
that you can revert to it if things go bad. You'll also be able to use
git diff
to view the changes you've made at each step of the documentation.
git add --all
git commit -m "An empty cargonauts app, ripe with potential."
Creating our resource
The first thing we need to create an endpoint is a resource. Because creating
resources is a very common thing to do, the cargonauts command has a subcommand
which will create a new resource for you. This command is aptly named
generate resource
. Let's create a resource called ping:
cargonauts generate resource ping
Feel free to use git to view the changes, the generator makes these three changes:
- We add a new module to the resource module. This contains the beginnings of the definition of our new resource.
- We re-export that module from the top-level resource module.
- We use that resource in the
routes!
macro.
There's one important problem though: the definition of a resource generated by the scaffolding is incomplete. For this reason, if you try to build your application right now, you will get some errors.
We're going to fix that, but before we go any further, let's look at the change to the routes! macro.
Changes to routing.rs
Our routes!
DSL now has its first declaration:
# #![allow(unused_variables)] #fn main() { routes! { resource Ping { } } #}
The routes!
DSL consists mostly of "resource objects" like this - there will
be one for each resource in your application, and it will contain various
additional items, mainly method
statements, which we'll see in a bit.
Finishing the Resource impl
If you open src/resources/ping.rs
, you'll see that it contains this impl:
# #![allow(unused_variables)] #fn main() { impl Resource for Ping { type Identifier = (); // TODO: Select the identifier type for this resource } #}
This impl contains an error: ()
is not a valid Identifier type. This is
because identifiers have to implement both ToString
and FromStr
, and ()
does not.
The identifier type is parsed from the URL of the resource. When users request
/ping/$slug
, it is $slug
that is parsed into the identifier. If the slug
is not a valid string representation of that identifier type, users receive an
error response.
For our purposes, in which the slug can be anything, a String
is a fine
representation of the slug, so let's make that change:
# #![allow(unused_variables)] #fn main() { impl Resource for Ping { type Identifier = String; } #}
Adding data to Ping
We said earlier that the response from a ping request would have two things:
- The slug requested
- A timestamp
The Ping
type will be used to represent this data, so it needs to have a
field for each of these.
We've already decided that the slug is represented as a String. To represent
the timestamp, we can use the chrono
crate.
Add a dependency on chrono (I used chrono version 0.3.1
while writing these
docs), and define your struct:
# #![allow(unused_variables)] #fn main() { use chrono::{DateTime, UTC}; pub struct Ping { slug: String, timestamp: DateTime<UTC>, } #}
Adding a method to Ping
Even though your app has a resource now, it still doesn't do anything! This is because we haven't implemented any methods for that resource. A resource without methods is like an object without methods, it doesn't actually do anything.
We're going to make the Ping
type implement the Get
method. This is the
method that corresponds to GET /$resource/$identiifer
, so its exactly what
we need to implement what we said.
We don't have a generator for this, so we'll do it all by hand.
Adding the impl
The Get
trait can be found in cargonauts::methods
; we need to import it
and implement it for Ping
. We also need to import from the futures crate
(which cargonauts re-exports), so that we can create a future. We'll start by
making our endpoint panic:
# #![allow(unused_variables)] #fn main() { use cargonauts::methods::Get; use cargonauts::futures::*; impl Get for Ping { fn get(slug: String, _: Environment) -> Box<Future<Item = Ping, Error = Error>> { panic!() } } #}
Creating our resource doesn't involve any IO or complex method calls; all we
need to do is construct our resource and wrap future::ok().boxed()
around it,
like so:
# #![allow(unused_variables)] #fn main() { future::ok(Ping { slug: slug, timestamp: UTC::now(), }).boxed() #}
That's it! Ping implements Get.
Deciding what format to use
The other thing we need to do before our endpoint is complete is to decide which format we will use to display this method. cargonauts comes with three formats out of the box:
- Debug: This format prints your type into the response using the
Debug
trait from the standard library. As its name implies, its intended for debugging, not as much for production code. - JsonApi: An implementation of the JSON API spec, this provides a JSON exchange format for machine consumption.
- Handlebars: This renders your response from a template using the handlebars templating language; this is intended for server side rendering of HTML.
For our purposes, we're going to use Debug
, because its the easiest to use
for examples like this. Each format will have its own additional requirements
for resources to be formatted with it. In the case of Debug
, the resource
type needs to implement the Debug
trait from the standard library.
We can achieve that by deriving Debug
for Ping
:
# #![allow(unused_variables)] #fn main() { #[derive(Debug)] struct Ping { slug: String, timestamp: DateTime<UTC>, } #}
Adding the method to the routes!
macro
Though Ping implements Get, the endpoint still doesn't exist. The last thing you need to do is declare your method in the routes! DSL. This check keeps all of your routes in one place, so you don't have to trace impls all over your code to figure out what routes your application has.
The syntax for creating a new route is like this:
# #![allow(unused_variables)] #fn main() { resource Ping { method Get in Debug; } #}
Every method you add to a resource will look like the same, a method name and a format the method is to be displayed in. Like the resource, these are just types that need to be in scope.