How to design a library with optionally borrowed dependencies

Let's say I want to write a client for an HTTP API where the actual HTTP implementation can be swapped out. So the usage would be like this:

fn main() {
    let http_client = reqwest::blocking::Client::new();
    let api_client = ApiClient::new(http_client);

    api_client.get_something();
}

The http_client is consumed.

The library would look like this:

struct ApiClient {
    http_client: reqwest::blocking::Client
}

impl ApiClient {
    fn new(http_client: reqwest::blocking::Client) -> ApiClient {
        ApiClient {
            http_client
        }
    }

    fn get_something(&self) {
        let a = &self.http_client;
        println!("{}", a.get("https://httpbin.org/get").send().unwrap().text().unwrap());
    }
}

Now the user of the library might want to do other things with the HTTP connection and would therefore prefer if the http_client wasn't consumed. I can change my library to this:

struct ApiClient<'a> {
    http_client: &'a reqwest::blocking::Client
}

impl ApiClient<'_> {
    fn new(http_client: &reqwest::blocking::Client) -> ApiClient {
        ApiClient {
            http_client
        }
    }

    fn get_something(&self) {
        let a = self.http_client;
        println!("{}", a.get("https://httpbin.org/get").send().unwrap().text().unwrap());
    }
}

But then the user decides that she wants to extract the initialization code to a function:

fn main() {
    let api_client = make_client();
    api_client.get_something();
}

fn make_client() -> ApiClient {
    let http_client = reqwest::blocking::Client::new();
    let api_client = ApiClient::new(&http_client);
    api_client
}

This isn't possible of course because the ApiClient would live longer than the reqwest::blocking::Client.

Is there an idiomatic way to let my library optionally consume the HTTP client?

I think you can write ApiClient<T: AsRef<Client>>. This lets T easily be a reference, a Box, Arc, or even a Cow if you want the choice to be dynamic.

I've run into this problem a fair amount, often in the context of something like font data, where sometimes you want to do some processing quickly, other times you want to retain a resource so it can be queried at leisure later.

2 Likes

I don't understand how to use that.

Here's a Playground link, how would I change ApiClient to allow both usages?

I was able to make it work with Borrow - I think AsRef works well with strings but Borrow might be the right choice for general T because of its blanket impl.

2 Likes