Skip to content

Crates IO API Helper

In this section, we will make a helper module to simplify handling of the request and response for the purposes of the tutorial. We are going to use the crates_io_api crate’s AsyncClient to retrieve results from a search query to crates.io.

Async test

Before you proceed, create a file src/crates_io_api_helper.rs with a async test block so you can experiment with the API.

src/crates_io_api_helper.rs
use color_eyre::Result;
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_crates_io() -> Result<()> {
println!("TODO: test crates_io_api here")
// ...
}
}

You’ll also need to add the module to main.rs:

src/main.rs
mod crates_io_api_helper;
#[tokio::main]
async fn main() -> color_eyre::Result<()> {

You can test this async function by running the following in the command line:

Terminal window
$ cargo test -- crates_io_api_helper::tests::test_crates_io --nocapture

Client

To initialize the crates_io_api::AsyncClient, you have to provide an email to use as the user agent.

src/crates_io_api_helper.rs ::tests
#[tokio::test]
async fn test_crates_io() -> Result<()> {
let email = "your-email-address@foo.com";
let user_agent = format!("crates-tui ({email})");
let rate_limit = std::time::Duration::from_millis(1000);
let client = crates_io_api::AsyncClient::new(user_agent, rate_limit)?;
// ...
}

Crates Query

Once you have created a client, you can make a query using the AsyncClient::crates function. This crates method takes a CratesQuery object that you will need to construct.

We can build this CratesQuery object using the following parameters:

  • Search query: String
  • Page number: u64
  • Page size: u64
  • Sort order: crates_io_api::Sort

Search Parameters

To make the code easier to manage, let’s store everything we need to construct a CratesQuery in a SearchParameters struct:

src/crates_io_api_helper.rs
use std::sync::{Arc, Mutex};
pub struct SearchParameters {
// Request
pub search: String,
pub page: u64,
pub page_size: u64,
pub sort: crates_io_api::Sort,
// Response
pub crates: Arc<Mutex<Vec<crates_io_api::Crate>>>,
}

You’ll notice that we also added a crates field to the SearchParameters.

This crates field will hold a clone of Arc<Mutex<Vec<crates_io_api::Crate>>> that will be passed into the async task. Inside the async task, it will be populated with the results from the response of the query once the query is completed.

Constructor

Create a new constructor to make it easier to create a SearchParameter instance:

src/crates_io_api_helper.rs
impl SearchParameters {
pub fn new(
search: String,
crates: Arc<Mutex<Vec<crates_io_api::Crate>>>,
) -> SearchParameters {
SearchParameters {
search,
page: 1,
page_size: 100,
sort: crates_io_api::Sort::Relevance,
crates,
}
}
}

Now, in the test function, you can initialize the search parameters with a search term "ratatui" like so:

src/crates_io_api_helper.rs (tests::test_crates_io)
// ...
let crates: Arc<Mutex<Vec<crates_io_api::Crate>>> = Default::default();
let search_params = SearchParameters::new("ratatui".into(), crates.clone());
// ...

Crates Query Builder

Construct the query using crates_io_api’s CratesQueryBuilder:

src/crates_io_api_helper.rs (tests::test_crates_io)
// ...
let query = crates_io_api::CratesQueryBuilder::default()
.search(&search_params.search)
.page(search_params.page)
.page_size(search_params.page_size)
.sort(search_params.sort.clone())
.build();
// ...

Request Crates

Once you have created the client and query, you can call the .crates() method on the client and await the response.

src/crates_io_api_helper.rs (tests::test_crates_io)
let page_result = client
.crates(query)
.await
let crates = page_result.crates;

Once the request is completed, you get a response in page_result that has a field called .crates which is a Vec<crates_io_api::Crate>.

Results

Clear the existing results in the search_params.crates field and update the Arc<Mutex<Vec<crates_io_api::Crate>>> with the response:

src/crates_io_api_helper.rs (tests::test_crates_io)
let mut app_crates = search_params.crates.lock().unwrap();
app_crates.clear();
app_crates.extend(crates);

Print

Finally, add a println! for every element in the response to test that it worked:

src/crates_io_api_helper.rs (tests::test_crates_io)
for krate in crates.lock().unwrap().iter() {
println!(
"name: {}\ndescription: {}\ndownloads: {}\n",
krate.name,
krate.description.clone().unwrap_or_default(),
krate.downloads
);
}

Run the test again now:

Terminal window
$ cargo test -- crates_io_api_helper::tests::test_crates_io --nocapture

You should get results like below (only the first three results are shown here for brevity):

running 1 test
name: ratatui
description: A library that's all about cooking up terminal user interfaces
downloads: 1026661
name: ratatui-textarea
description: [deprecated] ratatui is a simple yet powerful text editor widget for ratatui. Multi-line
text editor can be easily put as part of your ratatui application. Forked from tui-textarea.
downloads: 1794
name: ratatui-macros
description: Macros for Ratatui
downloads: 525
...
...
...
test crates_io_api_helper::tests::test_crates_io ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.31s

Refactor

You may want to refactor the above code into separate functions for simplicity. If you do so, it’ll look like this:

src/crates_io_api_helper.rs
/// Performs the actual search, and sends the result back through the
/// sender.
pub async fn request_search_results(
search_params: &SearchParameters,
) -> Result<(), String> {
let client = create_client()?;
let query = create_query(search_params);
let crates = fetch_crates(client, query).await?;
update_search_params_with_fetched_crates(crates, search_params);
Ok(())
}

You can now use this helper module to make async requests from the app.

Here’s the full code in src/crates_io_api_helper.rs for your reference:

src/crates_io_api_helper.rs (click to expand)
use color_eyre::{eyre::Context, Result};
use std::sync::{Arc, Mutex};
pub struct SearchParameters {
// Request
pub search: String,
pub page: u64,
pub page_size: u64,
pub sort: crates_io_api::Sort,
// Response
pub crates: Arc<Mutex<Vec<crates_io_api::Crate>>>,
}
impl SearchParameters {
pub fn new(
search: String,
crates: Arc<Mutex<Vec<crates_io_api::Crate>>>,
) -> SearchParameters {
SearchParameters {
search,
page: 1,
page_size: 100,
sort: crates_io_api::Sort::Relevance,
crates,
}
}
}
/// Performs the actual search, and sends the result back through the
/// sender.
pub async fn request_search_results(
search_params: &SearchParameters,
) -> Result<(), String> {
let client = create_client()?;
let query = create_query(search_params);
let crates = fetch_crates(client, query).await?;
update_search_params_with_fetched_crates(crates, search_params);
Ok(())
}
fn create_client() -> Result<crates_io_api::AsyncClient, String> {
let email = std::env::var("CRATES_TUI_TUTORIAL_APP_MYEMAIL").context("Need to set CRATES_TUI_TUTORIAL_APP_MYEMAIL environment variable to proceed").unwrap();
let user_agent = format!("crates-tui ({email})");
let rate_limit = std::time::Duration::from_millis(1000);
crates_io_api::AsyncClient::new(&user_agent, rate_limit)
.map_err(|err| format!("API Client Error: {err:#?}"))
}
fn create_query(
search_params: &SearchParameters,
) -> crates_io_api::CratesQuery {
#[allow(clippy::let_and_return)]
let query = crates_io_api::CratesQueryBuilder::default()
.search(&search_params.search)
.page(search_params.page)
.page_size(search_params.page_size)
.sort(search_params.sort.clone())
.build();
query
}
async fn fetch_crates(
client: crates_io_api::AsyncClient,
query: crates_io_api::CratesQuery,
) -> Result<Vec<crates_io_api::Crate>, String> {
let page_result = client
.crates(query)
.await
.map_err(|err| format!("API Client Error: {err:#?}"))?;
let crates = page_result.crates;
Ok(crates)
}
fn update_search_params_with_fetched_crates(
crates: Vec<crates_io_api::Crate>,
search_params: &SearchParameters,
) {
let mut app_crates = search_params.crates.lock().unwrap();
app_crates.clear();
app_crates.extend(crates);
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_crates_io() -> Result<()> {
let crates: Arc<Mutex<Vec<crates_io_api::Crate>>> = Default::default();
let search_params =
SearchParameters::new("ratatui".into(), crates.clone());
tokio::spawn(async move {
let _ = request_search_results(&search_params).await;
})
.await?;
for krate in crates.lock().unwrap().iter() {
println!(
"name: {}\ndescription: {}\ndownloads: {}\n",
krate.name,
krate.description.clone().unwrap_or_default(),
krate.downloads
);
}
Ok(())
}
}

With the refactor, your test code should look like this:

src/crates_io_api_helper.rs
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_crates_io() -> Result<()> {
let crates: Arc<Mutex<Vec<crates_io_api::Crate>>> = Default::default();
let search_params =
SearchParameters::new("ratatui".into(), crates.clone());
tokio::spawn(async move {
let _ = request_search_results(&search_params).await;
})
.await?;
for krate in crates.lock().unwrap().iter() {
println!(
"name: {}\ndescription: {}\ndownloads: {}\n",
krate.name,
krate.description.clone().unwrap_or_default(),
krate.downloads
);
}
Ok(())
}
}

Conclusion

With this crates_io_api_helper module set up, you can spawn a task using tokio to fill the results of the query into the Arc<Mutex<Vec<Crate>>> like so:

let crates: Arc<Mutex<Vec<crates_io_api::Crate>>> = Default::default();
let search_params = SearchParameters::new("ratatui".into(), crates.clone());
tokio::spawn(async move {
let _ = crates_io_api_helper::request_search_results(&search_params).await;
});

We will use this helper module once we set up our TUI application. To do that, let’s look at the contents of the tui module next.

Your file structure should now look like this:

.
├── Cargo.lock
├── Cargo.toml
└── src
├── crates_io_api_helper.rs
└── main.rs