You can find the complete code for this chapter on github
Wrapping the client
Just like for the server, there is some boilerplate code that can be spared to users. This kind of code is un-necessarily verbose for example:
# #![allow(unused_variables)] #fn main() { let req = Message::Request(Request { method: "add".into(), id: 0, params: vec![1.into(), 2.into()], }); client.call(req).and_then(|response| { // ... }); #}
We should not have to build a full Message
in the first place. To send a
request, users should just have to call a method that takes two arguments: the
method, and the parameters. Let's build a Client
type with such a method:
# #![allow(unused_variables)] #fn main() { // src/client.rs use std::io; use futures::{Future, BoxFuture}; use rmpv::Value; pub struct Client; pub type Response = Box<Future<Item = Result<Value, Value>, Error = io::Error>>; impl Client { pub fn request(&self, method: &str, params: Vec<Value>) -> Response { // TODO } } #}
This is a bit naive because as it stands, Client
is an empty type. Instead,
it should wrap a type that implements Service
and can send requests. It turns
out that tokio provides such a type:
tokio_proto::multiplex::ClientService
.
All we have to do is wrap it and use it to send the requests:
# #![allow(unused_variables)] #fn main() { // src/client.rs // ... use tokio_proto::multiplex::ClientService; use message::{Message, Request}; pub struct Client(ClientService<TcpStream, Protocol>); pub type Response = Box<Future<Item = Result<Value, Value>, Error = io::Error>>; impl Client { pub fn request(&self, method: &str, params: Vec<Value>) -> Response { let req = Message::Request(Request { // we can set this to 0 because under the hood it's handle by tokio at the // protocol/codec level id: 0, method: method.to_string(), params: params, }); let resp = self.0.call(req).and_then(|resp| { match resp { Message::Response(response) => Ok(response.result), _ => panic!("Response is not a Message::Response"); } }); Box::new(resp) as Response } } #}
It is easy to adapt the example code to add a connect()
method as well:
# #![allow(unused_variables)] #fn main() { // src/client.rs // ... use tokio_proto::TcpClient; // ... impl Client { pub fn connect(addr: &SocketAddr, handle: &Handle) -> Box<Future<Item = Client, Error = io::Error>> { let ret = TcpClient::new(Protocol) .connect(addr, handle) .map(Client); Box::new(ret) } // ... } #}
Complete code
client.rs
# #![allow(unused_variables)] #fn main() { use std::io; use std::net::SocketAddr; use futures::Future; use tokio_core::net::TcpStream; use tokio_core::reactor::Handle; use tokio_proto::multiplex::ClientService; use tokio_proto::TcpClient; use tokio_service::Service; use rmpv::Value; use message::{Message, Request}; use protocol::Protocol; pub struct Client(ClientService<TcpStream, Protocol>); pub type Response = Box<Future<Item = Result<Value, Value>, Error = io::Error>>; impl Client { pub fn connect(addr: &SocketAddr, handle: &Handle) -> Box<Future<Item = Client, Error = io::Error>> { let ret = TcpClient::new(Protocol) .connect(addr, handle) .map(Client); Box::new(ret) } pub fn request(&self, method: &str, params: Vec<Value>) -> Response { let req = Message::Request(Request { // we can set this to 0 because under the hood it's handle by tokio at the // protocol/codec level id: 0, method: method.to_string(), params: params, }); let resp = self.0.call(req).and_then(|resp| { match resp { Message::Response(response) => Ok(response.result), _ => panic!("Response is not a Message::Response"), } }); Box::new(resp) as Response } } #}