use std::num::NonZeroU32;
use std::time::Duration;
use crate::datasets::http::{DatasetClient, DatasetClientConfig, HttpError};
use crate::datasets::sodir::error::{Result, SodirError};
pub const RATE_LIMIT_PER_SEC: u32 = 5;
const USER_AGENT: &str = "kglite-datasets-sodir/1";
const CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
const BASE_BACKOFF_MS: u64 = 500;
#[derive(Clone)]
pub struct ArcGISClient {
inner: DatasetClient,
retry_count: usize,
}
impl ArcGISClient {
pub fn new() -> Result<Self> {
Self::with_options(RATE_LIMIT_PER_SEC, 3)
}
pub fn with_options(rate_per_sec: u32, retry_count: usize) -> Result<Self> {
let rate = NonZeroU32::new(rate_per_sec)
.ok_or_else(|| SodirError::Decode("rate_per_sec must be > 0".into()))?;
let inner = DatasetClient::new(DatasetClientConfig {
user_agent: USER_AGENT.to_string(),
connect_timeout: CONNECT_TIMEOUT,
overall_timeout: Some(REQUEST_TIMEOUT),
rate_per_sec: Some(rate),
retry_count,
base_backoff_ms: BASE_BACKOFF_MS,
});
Ok(ArcGISClient { inner, retry_count })
}
#[allow(dead_code)]
pub fn fetch_bytes(&self, url: &str) -> Result<Vec<u8>> {
self.inner.fetch_bytes(url).map_err(|e| self.map_http(e))
}
pub fn fetch_json(&self, url: &str) -> Result<serde_json::Value> {
self.inner.fetch_json(url).map_err(|e| self.map_http(e))
}
fn map_http(&self, e: HttpError) -> SodirError {
match e {
HttpError::Status { code, .. } if code == 429 || (500..=599).contains(&code) => {
SodirError::RateLimited {
retries: self.retry_count,
}
}
HttpError::Status { code, url } => SodirError::BadStatus { status: code, url },
HttpError::Transport(msg) => SodirError::Http(msg),
HttpError::Io(io) => SodirError::Io(io),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn client_constructs() {
assert!(ArcGISClient::new().is_ok());
}
#[test]
fn zero_rate_rejected() {
assert!(ArcGISClient::with_options(0, 3).is_err());
}
#[test]
fn client_is_send_sync_clone() {
fn assert_bounds<T: Send + Sync + Clone>() {}
assert_bounds::<ArcGISClient>();
}
}