use crate::{
error::{Error, Result},
jolpica::{
resource::{Page, Resource},
response::Response,
},
rate_limiter::RateLimiter,
};
#[cfg(doc)]
use crate::jolpica::{agent::Agent, response::Pagination};
pub fn get_response_page(base_url: &str, resource: &Resource, page: Option<Page>) -> Result<Response> {
let url = resource.to_url_with_base_and_opt_page(base_url, page);
let json_str = ureq::get(url.as_str()).call()?.into_body().read_to_string()?;
serde_json::from_str::<Response>(json_str.as_str()).map_err(Into::into)
}
pub fn get_response_multi_pages(
base_url: &str,
resource: &Resource,
initial_page: Option<Page>,
max_page_count: Option<usize>,
rate_limiter: Option<&RateLimiter>,
http_retries: Option<usize>,
) -> Result<Vec<Response>> {
let mut responses = vec![retry_on_http_error(
|| get_response_page(base_url, resource, initial_page),
rate_limiter,
http_retries,
)?];
let mut pages = vec![responses.last().unwrap_or_else(|| unreachable!()).pagination];
while let Some(next_page) = pages.last().unwrap_or_else(|| unreachable!()).next_page() {
pages.push(next_page);
}
if let Some(max_page_count) = max_page_count
&& pages.len() > max_page_count
{
return Err(Error::ExceededMaxPageCount((pages.len(), max_page_count)));
}
for page in &pages[1..] {
responses.push(retry_on_http_error(
|| get_response_page(base_url, resource, Some((*page).into())),
rate_limiter,
http_retries,
)?);
}
Ok(responses)
}
pub fn retry_on_http_error<T>(
f: impl Fn() -> Result<T>,
rate_limiter: Option<&RateLimiter>,
max_retries: Option<usize>,
) -> Result<T> {
let max_retries = max_retries.unwrap_or(0);
let rate_limited_call = || {
if let Some(limiter) = rate_limiter {
limiter.wait_until_ready();
}
f()
};
let mut result = rate_limited_call();
if max_retries == 0 || !matches!(result, Err(Error::Http(_))) {
return result;
}
for _ in 0..max_retries {
result = rate_limited_call();
if !matches!(result, Err(Error::Http(_))) {
return result;
}
}
let Err(Error::Http(ureq_err)) = result else {
unreachable!()
};
Err(Error::HttpRetries((max_retries, ureq_err)))
}
#[cfg(test)]
mod tests {
use std::cell::RefCell;
use std::time::Duration;
use crate::{
error::Error,
jolpica::{
api::{JOLPICA_API_PAGINATION, JOLPICA_API_RATE_LIMIT_QUOTA},
resource::Filters,
tests::util::{get_jolpica_test_base_url, get_jolpica_test_rate_limiter, get_request_avg_duration_ms},
},
rate_limiter::{Quota, RateLimiter, nonzero},
};
use crate::jolpica::tests::{
assets::*,
util::{TESTS_DEFAULT_HTTP_RETRIES, retry_http},
};
use crate::tests::asserts::*;
use shadow_asserts::assert_eq;
use super::*;
fn get_response_rate_limited_with_http_retries(
base_url: &str,
resource: &Resource,
page: Option<Page>,
) -> Result<Response> {
retry_http(|| super::get_response_page(base_url, resource, page))
}
#[test]
#[ignore]
fn get_response_page() {
let resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
Some(Page::with_limit(50)),
)
.unwrap();
let seasons = resp.table.as_seasons().unwrap();
assert_eq!(seasons.len(), 50);
assert_eq!(seasons.first().unwrap().season, 1950);
assert_eq!(seasons.last().unwrap().season, 1999);
assert_false!(resp.pagination.is_last_page());
let resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
Some(resp.pagination.next_page().unwrap().into()),
)
.unwrap();
let seasons = resp.table.as_seasons().unwrap();
assert_le!(seasons.len(), 50);
assert_eq!(seasons.first().unwrap().season, 2000);
assert_true!(resp.pagination.is_last_page());
let resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&Resource::DriverInfo(Filters::new().driver_id("leclerc".into())),
None,
)
.unwrap();
assert_eq!(resp.pagination.limit, JOLPICA_API_PAGINATION.default_limit);
let drivers = resp.table.as_drivers().unwrap();
assert_eq!(drivers.len(), 1);
assert_eq!(drivers.first().unwrap().given_name, "Charles");
assert_true!(resp.pagination.is_last_page());
}
#[test]
#[ignore]
fn get_response_page_multi_default() {
let resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
None,
)
.unwrap();
let pagination = resp.pagination;
assert_false!(pagination.is_single_page());
assert_false!(pagination.is_last_page());
assert_eq!(pagination.limit, JOLPICA_API_PAGINATION.default_limit);
assert_eq!(pagination.offset, JOLPICA_API_PAGINATION.default_offset);
assert_ge!(pagination.total, 76);
let seasons = resp.table.as_seasons().unwrap();
assert_eq!(seasons.len(), JOLPICA_API_PAGINATION.default_limit as usize);
assert_eq!(seasons[0], *SEASON_1950);
}
#[test]
#[ignore]
fn get_response_page_single_max_limit() {
let resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
Some(Page::with_max_limit()),
)
.unwrap();
let pagination = resp.pagination;
assert_true!(pagination.is_single_page());
assert_true!(pagination.is_last_page());
assert_eq!(pagination.limit, JOLPICA_API_PAGINATION.max_limit);
assert_eq!(pagination.offset, JOLPICA_API_PAGINATION.default_offset);
assert_ge!(pagination.total, 76);
let seasons = resp.table.as_seasons().unwrap();
assert_ge!(seasons.len(), 76);
assert_eq!(seasons[0], *SEASON_1950);
assert_eq!(seasons[29], *SEASON_1979);
assert_eq!(seasons[50], *SEASON_2000);
assert_eq!(seasons[73], *SEASON_2023);
}
#[test]
#[ignore]
fn get_response_page_multi_page() {
let resource = Resource::SeasonList(Filters::none());
let page = Page::with_limit(5);
let mut resp =
get_response_rate_limited_with_http_retries(&get_jolpica_test_base_url(), &resource, Some(page.clone()))
.unwrap();
assert_false!(resp.pagination.is_last_page());
let mut current_offset: u32 = 0;
while !resp.pagination.is_last_page() {
let pagination = resp.pagination;
assert_false!(pagination.is_single_page());
assert_eq!(pagination.limit, page.limit());
assert_eq!(pagination.offset, current_offset);
assert_ge!(pagination.total, 76);
let seasons = resp.table.as_seasons().unwrap();
assert_eq!(seasons.len(), page.limit() as usize);
match current_offset {
0 => assert_eq!(seasons[0], *SEASON_1950),
25 => assert_eq!(seasons[4], *SEASON_1979),
50 => assert_eq!(seasons[0], *SEASON_2000),
70 => assert_eq!(seasons[3], *SEASON_2023),
_ => (),
}
resp = get_response_rate_limited_with_http_retries(
&get_jolpica_test_base_url(),
&resource,
Some(pagination.next_page().unwrap().into()),
)
.unwrap();
current_offset += page.limit();
}
let pagination = resp.pagination;
assert_false!(pagination.is_single_page());
assert_true!(pagination.is_last_page());
assert_eq!(pagination.limit, page.limit());
assert_eq!(pagination.offset, current_offset);
assert_ge!(pagination.total, 76);
let seasons = resp.table.as_seasons().unwrap();
assert_eq!(seasons.last().unwrap().season, 1950 + current_offset + (seasons.len() as u32) - 1);
}
#[test]
#[ignore]
fn get_response_page_error_wrong_base_url() {
assert!(matches!(
super::get_response_page("http://nonexistent.local", &Resource::SeasonList(Filters::none()), None),
Err(Error::Http(_))
));
}
#[test]
#[ignore]
fn get_response_multi_pages() {
let resource = Resource::SeasonList(Filters::none());
let page = Page::with_limit(5);
let responses = super::get_response_multi_pages(
&get_jolpica_test_base_url(),
&resource,
Some(page.clone()),
None,
get_jolpica_test_rate_limiter(),
Some(TESTS_DEFAULT_HTTP_RETRIES),
)
.unwrap();
assert_false!(responses.first().unwrap().pagination.is_last_page());
assert_true!(responses.last().unwrap().pagination.is_last_page());
assert_ge!(responses.len(), 16);
let mut current_offset: u32 = 0;
for resp in &responses {
let pagination = resp.pagination;
assert_false!(pagination.is_single_page());
assert_eq!(pagination.limit, page.limit());
assert_eq!(pagination.offset, current_offset);
assert_ge!(pagination.total, 76);
let seasons = resp.table.as_seasons().unwrap();
if !resp.pagination.is_last_page() {
assert_eq!(seasons.len(), page.limit() as usize);
} else {
assert_le!(seasons.len(), page.limit() as usize);
}
match current_offset {
0 => assert_eq!(seasons[0], *SEASON_1950),
25 => assert_eq!(seasons[4], *SEASON_1979),
50 => assert_eq!(seasons[0], *SEASON_2000),
70 => assert_eq!(seasons[3], *SEASON_2023),
_ => (),
}
if !resp.pagination.is_last_page() {
current_offset += page.limit();
}
}
let pagination = responses.last().unwrap().pagination;
assert_false!(pagination.is_single_page());
assert_true!(pagination.is_last_page());
assert_eq!(pagination.limit, page.limit());
assert_eq!(pagination.offset, current_offset);
assert_ge!(pagination.total, 76);
let seasons = responses.last().unwrap().table.as_seasons().unwrap();
assert_eq!(seasons.last().unwrap().season, 1950 + current_offset + (seasons.len() as u32) - 1);
}
#[test]
#[ignore]
fn get_response_multi_pages_rate_limiting() {
let rate_limiter = RateLimiter::new(JOLPICA_API_RATE_LIMIT_QUOTA);
let start = std::time::Instant::now();
let _responses = super::get_response_multi_pages(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
Some(Page::with_limit(20)),
None,
Some(&rate_limiter),
None,
);
let elapsed = start.elapsed();
assert_eq!(_responses.unwrap().len(), 4);
assert_lt!(elapsed, Duration::from_millis(get_request_avg_duration_ms() * (4 + 1)));
rate_limiter.wait_until_ready();
let start = std::time::Instant::now();
let _responses = super::get_response_multi_pages(
&get_jolpica_test_base_url(),
&Resource::SeasonList(Filters::none()),
Some(Page::with_limit(20)),
None,
Some(&rate_limiter),
None,
);
let elapsed = start.elapsed();
assert_eq!(_responses.unwrap().len(), 4);
assert_ge!(elapsed, Duration::from_millis(7200 * (4 - 1)));
assert_lt!(elapsed, Duration::from_millis(7200 * (4 + 1)));
}
#[test]
#[ignore]
fn get_response_multi_pages_error_exceeded_max_page_count() {
let rate_limiter = RateLimiter::new(JOLPICA_API_RATE_LIMIT_QUOTA);
let req = Resource::SeasonList(Filters::none());
let start = std::time::Instant::now();
assert!(matches!(
super::get_response_multi_pages(
&get_jolpica_test_base_url(),
&req,
Some(Page::with_limit(5)),
Some(10),
Some(&rate_limiter),
None
),
Err(Error::ExceededMaxPageCount((16, 10)))
));
let elapsed = start.elapsed();
assert_lt!(elapsed, Duration::from_millis(get_request_avg_duration_ms() * (1 + 1)));
}
fn make_counter_f<T>(count: &RefCell<u32>, f: impl Fn() -> Result<T>) -> impl Fn() -> Result<T> {
*count.borrow_mut() = 0;
move || {
*count.borrow_mut() += 1;
f()
}
}
#[test]
fn counter_closure() {
let count = RefCell::<u32>::new(0);
let f = make_counter_f(&count, || Ok(()));
assert_eq!(*count.borrow(), 0);
let _unused = f();
assert_eq!(*count.borrow(), 1);
let _unused = f();
assert_eq!(*count.borrow(), 2);
let f = make_counter_f(&count, || Ok(()));
assert_eq!(*count.borrow(), 0);
let _unused = f();
assert_eq!(*count.borrow(), 1);
}
#[test]
fn retry_on_http_error() {
let count = RefCell::<u32>::new(0);
let f_ok = || Ok(42);
let f_err_http = || Err(Error::Http(ureq::Error::ConnectionFailed));
let f_err_non_http = || Err(Error::NotFound);
let _unused: Result<u32> = f_err_http();
let _unused: Result<u32> = f_err_non_http();
let result = super::retry_on_http_error(make_counter_f(&count, f_ok), None, None);
assert_eq!(result.unwrap(), 42);
assert_eq!(*count.borrow(), 1);
let result = super::retry_on_http_error(make_counter_f(&count, f_err_http), None, None);
assert!(matches!(result, Err(Error::Http(_))));
assert_eq!(*count.borrow(), 1);
let result = super::retry_on_http_error(make_counter_f(&count, f_err_non_http), None, Some(0));
assert!(matches!(result, Err(Error::NotFound)));
assert_eq!(*count.borrow(), 1);
let result = super::retry_on_http_error(make_counter_f(&count, f_ok), None, Some(3));
assert_true!(result.is_ok());
assert_eq!(result.unwrap(), 42);
assert_eq!(*count.borrow(), 1);
let result = super::retry_on_http_error(make_counter_f(&count, f_err_non_http), None, Some(3));
assert!(matches!(result, Err(Error::NotFound)));
assert_eq!(*count.borrow(), 1);
let result = super::retry_on_http_error(
make_counter_f(&count, || if *count.borrow() < 3 { f_err_http() } else { f_ok() }),
None,
Some(3),
);
assert_eq!(result.unwrap(), 42);
assert_eq!(*count.borrow(), 3);
let result = super::retry_on_http_error(
make_counter_f(&count, || {
if *count.borrow() < 3 {
f_err_http()
} else {
f_err_non_http()
}
}),
None,
Some(3),
);
assert!(matches!(result, Err(Error::NotFound)));
assert_eq!(*count.borrow(), 3);
let result = super::retry_on_http_error(make_counter_f(&count, f_err_http), None, Some(3));
assert!(matches!(result, Err(Error::HttpRetries((3, _)))));
assert_eq!(*count.borrow(), 4);
let rate_limiter = RateLimiter::new(Quota::per_second(nonzero!(10u32)).allow_burst(nonzero!(1u32)));
rate_limiter.wait_until_ready();
let start = std::time::Instant::now();
let result = super::retry_on_http_error(make_counter_f(&count, f_err_http), Some(&rate_limiter), Some(3));
let elapsed = start.elapsed();
assert!(matches!(result, Err(Error::HttpRetries((3, _)))));
assert_eq!(*count.borrow(), 4);
assert_ge!(elapsed, Duration::from_millis(100 * 4));
assert_lt!(elapsed, Duration::from_millis(100 * 5));
}
}