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.
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
:
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:
$ 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.
#[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:
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:
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:
// ... 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
:
// ... 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.
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:
let mut app_crates = search_params.crates.lock().unwrap(); app_crates.clear(); app_crates.extend(crates);
Finally, add a println!
for every element in the response to test that it worked:
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:
$ 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: ratatuidescription: A library that's all about cooking up terminal user interfacesdownloads: 1026661
name: ratatui-textareadescription: [deprecated] ratatui is a simple yet powerful text editor widget for ratatui. Multi-linetext editor can be easily put as part of your ratatui application. Forked from tui-textarea.downloads: 1794
name: ratatui-macrosdescription: Macros for Ratatuidownloads: 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:
/// 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:
#[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