Cloning boxed traits in Rust
I enjoy coding with Rust’s composition system. It’s nice to also have generics. It makes adding and incorporating changes rather nice. I recently ran into a problem when I wanted to copy a group of boxed traits.
Lets start from the beginning. Imagine we have an http client that makes requests give a URL. Like so…
use reqwest::{Client, Method, RequestBuilder};
struct Cl {
client: Client,
}
impl Cl {
fn new() -> Self {
Cl {
client: Client::new(),
}
}
async fn make_request(&self, url: &str) -> Result<String, Error> {
let req = self.client.request(Method::GET, url);
let res = req.send().await?;
let res_str = res.text().await?;
Ok(res_str)
}
}
type Error = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let c = Cl::new();
let resp = c.make_request("http://google.com").await?;
println!("{}", resp);
Ok(())
}
Now, this just works. What if we used an endpoint that needed authentication instead of google.com? Assuming this service used basic authentication, we’d do something like this.
use reqwest::{Client, Method, RequestBuilder};
struct Cl {
client: Client,
}
impl Cl {
fn new() -> Self {
Cl {
client: Client::new(),
}
}
async fn make_request(
&self,
url: &str,
user_name: &str,
password: &str,
) -> Result<String, Error> {
let req = self.client.request(Method::GET, url);
let res = req
.basic_auth(user_name.to_string(), Some(password.to_string()))
.send()
.await?;
let res_str = res.text().await?;
Ok(res_str)
}
}
type Error = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let c = Cl::new();
let resp = c
.make_request("http://site-with-basic-auth.com", "username", "password")
.await?;
println!("{}", resp);
Ok(())
}
But what if we wanted to call an endpoint that authenticates with a bearer token?
use reqwest::{Client, Method, RequestBuilder};
struct Cl {
client: Client,
}
impl Cl {
fn new() -> Self {
Cl {
client: Client::new(),
}
}
async fn make_request(&self, url: &str, token_value: &str) -> Result<String, Error> {
let req = self.client.request(Method::GET, url);
let res = req
.header(reqwest::header::AUTHORIZATION, token_value.clone())
.send()
.await?;
let res_str = res.text().await?;
Ok(res_str)
}
}
type Error = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let c = Cl::new();
let resp = c
.make_request("http://site-that-needs-bearer-token", "Bearer token")
.await?;
println!("{}", resp);
Ok(())
}
Now, we don’t want make_request
to keep changing or override it. Enter traits.
We could simply replace the myriad input variables with a trait that looks like
this.
trait Authorizer {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder;
}
and now we re-write make_request
to look like this
use reqwest::{Client, Method, RequestBuilder};
trait Authorizer {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder;
}
struct Cl {
client: Client,
auth: Box<dyn Authorizer>,
}
impl Cl {
fn new(auth: Box<dyn Authorizer>) -> Self {
Cl {
client: Client::new(),
auth: auth,
}
}
async fn make_request(&self, url: &str) -> Result<String, Error> {
let req = self.client.request(Method::GET, url);
let req = self.auth.authorize_request(req);
let res = req.send().await?;
let res_str = res.text().await?;
Ok(res_str)
}
}
Note: I could have simply have included the trait as a field to make_request like so
async fn make_request(&self, url: &str, &dyn Authorizer) -> Result<String, Error> {
This makes authorizer be dynamic for different urls. I changed this out a bit to suit my demonstration.
However, imagine I had a use case that required clients made with base endpoints for different APIs. This lets the caller implement Authorizer. Nothing new so far. Now the caller can implement variations of Basic and Bearer Auth.
use reqwest::{Client, Method, RequestBuilder};
trait Authorizer {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder;
}
struct Cl {
client: Client,
auth: Box<dyn Authorizer>,
}
impl Cl {
fn new(auth: Box<dyn Authorizer>) -> Self {
Cl {
client: Client::new(),
auth: auth,
}
}
async fn make_request(&self, url: &str) -> Result<String, Error> {
let req = self.client.request(Method::GET, url);
let req = self.auth.authorize_request(req);
let res = req.send().await?;
let res_str = res.text().await?;
Ok(res_str)
}
}
type Error = Box<dyn std::error::Error + Send + Sync>;
#[tokio::main]
async fn main() -> Result<(), Error> {
let bearer_client = Cl::new(Box::new(BearerTokenAuthorizer::new("key", "value")));
let resp = bearer_client
.make_request("http://site-that-needs-bearer-token")
.await?;
println!("{}", resp);
let basic_auth_client = Cl::new(Box::new(BasicAuth::new("user", "password")));
let resp = basic_auth_client
.make_request("http://site-that-needs-basic-auth")
.await?;
println!("{}", resp);
Ok(())
}
pub struct BasicAuth {
user: String,
api_token: String,
}
impl Authorizer for BasicAuth {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder {
req.basic_auth(self.user.to_owned(), Some(self.api_token.to_owned()))
}
}
impl BasicAuth {
fn new(user: &str, value: &str) -> Self {
BasicAuth {
user: user.to_string(),
api_token: value.to_string(),
}
}
}
pub struct BearerTokenAuthorizer {
token_key: String,
token_value: String,
}
impl Authorizer for BearerTokenAuthorizer {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder {
req.header(reqwest::header::AUTHORIZATION, self.token_value.clone())
}
}
impl BearerTokenAuthorizer {
fn new(key: &str, value: &str) -> Self {
let value = format!("Bearer {}", value.to_string());
BearerTokenAuthorizer {
token_key: key.to_string(),
token_value: value,
}
}
}
So far, so good right?
So for my usecase… I wanted to clone the Client around. This meant deriving a trait. As simple as
#[derive(Clone)]
struct Cl {
client: Client,
auth: Box<dyn Authorizer>,
}
Lets try to build it.
copy-box-traits % cargo build
Compiling copy-box-traits v0.1.0 (/Users/sudarsan/dev/rust/copy-box-traits)
error[E0277]: the trait bound `dyn Authorizer: std::clone::Clone` is not satisfied
--> src/main.rs:10:5
|
10 | auth: Box<dyn Authorizer>,
| ^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `std::clone::Clone` is not implemented for `dyn Authorizer`
|
OOPS.
So now I had to implement a custom clone for my Boxed trait.
This didn’t seem as straightforward as I thought it would be. I started with…
impl Clone for Box<dyn Authorizer> {
fn clone(&self) -> Self {
Box::new(*self.clone())
}
}
Now… Rust would complain saying…
the size for values of type `dyn Authorizer` cannot be known at compilation time
I clearly can’t dereference the box and put it back in after cloning it.
So the right way to do it, is to implement a generic authorizer cloner trait CloneAuthorizer
for all types .
Now all we have to do is add this as a built in trait to the trait. This
is because only built in traits can be used as closure or object bounds.
trait Authorizer: CloneAuthorizer {
fn authorize_request(&self, req: RequestBuilder) -> RequestBuilder;
}
impl Clone for Box<dyn Authorizer> {
fn clone(&self) -> Self {
self.clone_authorizer()
}
}
trait CloneAuthorizer {
fn clone_authorizer(&self) -> Box<dyn Authorizer>;
}
impl<T> CloneAuthorizer for T
where
T: Authorizer + Clone + 'static,
{
fn clone_authorizer(&self) -> Box<dyn Authorizer> {
Box::new(self.clone())
}
}
This makes the Box<dyn Authorizer>
or any struct with this trait clone-able.
The full code can be found at https://github.com/sudarshan-reddy/copy-box-traits