The main function

In the main function implementation, we create a Runtime instance, which we will use for database queries and to process HTTP requests. Look at the following code:

pub fn main() -> Result<(), Error> {
let mut runtime = Runtime::new()?;

let handshake = tokio_postgres::connect("postgres://postgres@localhost:5432", NoTls);
let (mut client, connection) = runtime.block_on(handshake)?;
runtime.spawn(connection.map_err(drop));

// ...
}

We create a Runtime instance. After that, we can create a new database connection by calling the connect function of the tokio-postgres crate. It returns a Future that we have to execute immediately. To run a Future, we will use the same Runtime we have already created. Runtime has the block_on method, which we have already discussed in Chapter 5, Understanding Asynchronous Operations with Futures Crate. We call it with a Connect future and take a pair of results: Client and Connection instances.

Client is a type that provides a method to create statements. We will store this instance in ConnState, which we will declare later in this section.

The Connection type is a task that performs actual interaction with a database. We have to spawn this task within Runtime. If you forget to do this, your database queries will be blocked and will never be sent to a database server.

Now we can use the Client instance to execute SQL statements. The first statement we need creates a table to log User-Agent header values. The Client struct has the batch_execute method, which executes multiple statements from a string. We've used only one statement, but this call is useful if you want to create more than one table:

let execute = client.batch_execute(
"CREATE TABLE IF NOT EXISTS agents (
agent TEXT NOT NULL,
timestamp TIMESTAMPTZ NOT NULL DEFAULT NOW()
);");
runtime.block_on(execute)?;

batch_execute returns a Future instance and we have to execute it immediately to initialize the database before inserting a record into it. We use the block_on method of a Runtime instance to execute the statement.

Before we finish implementing the main function, let's look at the ConnState struct implementation:

#[derive(Clone, StateData)]
struct ConnState {
client: Arc<Mutex<Client>>,
}

The struct is very simple and contains the atomic reference counter, Arc, to a database Client wrapped with a Mutex. We need only one method to simplify instance creation:

impl ConnState {
fn new(client: Client) -> Self {
Self {
client: Arc::new(Mutex::new(client)),
}
}
}

But you can also add a method to get the inner value of this state. It's useful if you want to declare a state type in a separate module. We will use the client field directly.

Also, you might notice that ConnState derives Clone and StateData traits. The struct has to be cloneable, because a state is cloned by Gotham for every request. StateData allows us to attach an instance of this struct to StateMiddleware.

Now we can finish the main function implementation:

let state = ConnState::new(client);
let router = router(state);

let addr = "127.0.0.1:7878";
println!("Listening for requests at http://{}", addr);
gotham::start_on_executor(addr, router, runtime.executor());
runtime
.shutdown_on_idle()
.wait()
.map_err(|()| format_err!("can't wait for the runtime"))

We created the ConnState state with a Client value. We stored the result to the state variable and used it for the router function call, which we will declare later.

After that, we can start a Gotham server by calling the start_on_executor function. It expects three arguments: the address that we set to the "127.0.0.1:7878" value, the router value that we created with the router function call, and the TaskExecutor instance that we extracted from our Runtime.

Actually, the start_on_executor function call the spawns a task to the asynchronous reactor and we have to start our Runtime instance. We can do this with the shutdown_on_idle method call. It returns the Shutdown future that we run in the current thread using the wait method call. The main function ends when all tasks are complete.

Let's look at the router function implementation that creates the Router instance for our application:

fn router(state: ConnState) -> Router {
let middleware = StateMiddleware::new(state);
let pipeline = single_middleware(middleware);
let (chain, pipelines) = single_pipeline(pipeline);
build_router(chain, pipelines, |route| {
route.get("/").to(register_user_agent);
})
}

In the function implementation, we create a StateMiddleware instance and provide ConnState to it. We add a middleware to a pipeline with the single_middleware call and create a chain by calling the single_pipeline function call. It returns a pair of a chain and a set of pipelines.

We pass these values to the build_router function, which returns the Router instance, but we can tune the resulting Router by calling methods of RouterBuilder in a closure that we pass as a third argument to the build_router function.

We called the get method of RouterBuilder to set a handler implemented in the register_user_agent function to the root path, /. The Gotham framework supports scopes of routes that help you group handlers by a path prefix, like this:

route.scope("/checkout", |route| {
route.get("/view").to(checkout::view);
route.post("/item").to(checkout::item::create);
route.get("/item").to(checkout::item::read);
route.put("/item").to(checkout::item::update);
route.patch("/item").to(checkout::item::update);
route.delete("/item").to(checkout::item::delete);
}

We now only have to implement a handler.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
18.191.144.65