mod epoch;
#[cfg(test)]
mod test;
use std::time::Duration;
use epoch::EpochMiddleware;
use log::debug;
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use tokio::time::interval;
use tokio_stream::{wrappers::IntervalStream, Stream, StreamExt};
use serde::de::DeserializeOwned;
use crate::{
alert::Alert,
assoc::{Association, Region, RegionResponse},
spot::Spot,
summit::Summit,
};
pub const BASE_PATH: &str = "https://api-db2.sota.org.uk/api";
pub struct Client {
base_path: String,
client: ClientWithMiddleware,
}
fn build_client(user_agent: &str) -> ClientWithMiddleware {
let retry_policy = ExponentialBackoff::builder().build_with_max_retries(3);
ClientBuilder::new(
reqwest::ClientBuilder::new()
.user_agent(user_agent)
.timeout(Duration::from_secs(10))
.build()
.unwrap(),
)
.with(EpochMiddleware::new())
.with(RetryTransientMiddleware::new_with_policy(retry_policy))
.build()
}
impl Client {
pub fn new(user_agent: &str) -> Self {
Self {
base_path: BASE_PATH.into(),
client: build_client(user_agent),
}
}
pub fn new_with_base(base_path: &str, user_agent: &str) -> Self {
Self {
base_path: base_path.into(),
client: build_client(user_agent),
}
}
async fn get<T: DeserializeOwned>(&self, endpoint: &str) -> Result<T, APIError> {
let url = format!("{}/{endpoint}", self.base_path);
debug!("GET {}", &url);
self.client
.get(url)
.send()
.await?
.json()
.await
.map_err(APIError::from)
}
pub async fn all_alerts(&self) -> Result<Vec<Alert>, APIError> {
handle_epoch(self.get("alerts/12/all/all").await)
}
pub async fn all_spots(&self, hours: u8) -> Result<Vec<Spot>, APIError> {
handle_epoch(self.get(&format!("spots/-{hours}/all/all")).await)
}
pub async fn association(&self, assoc: &str) -> Result<Association, APIError> {
self.get(&format!("associations/{assoc}")).await
}
pub async fn region(&self, assoc: &str, region: &str) -> Result<Region, APIError> {
let resp: RegionResponse = self.get(&format!("regions/{assoc}/{region}")).await?;
Ok(resp.region)
}
pub async fn summit(&self, assoc: &str, summit: &str) -> Result<Summit, APIError> {
self.get(&format!("summits/{assoc}/{summit}")).await
}
pub async fn all_alerts_every_minute(
&self,
) -> impl Stream<Item = Result<Vec<Alert>, APIError>> + use<'_> {
IntervalStream::new(interval(Duration::from_secs(60))).then(|_| self.all_alerts())
}
pub async fn all_spots_every_minute(
&self,
) -> impl Stream<Item = Result<Vec<Spot>, APIError>> + use<'_> {
IntervalStream::new(interval(Duration::from_secs(60))).then(|_| self.all_spots(1))
}
}
fn handle_epoch<T: DeserializeOwned>(result: Result<Vec<T>, APIError>) -> Result<Vec<T>, APIError> {
match result {
Ok(res) => Ok(res),
Err(e) => {
if e.to_string().contains("Current data still fresh") {
debug!("Current data still fresh");
Ok(Vec::new())
} else {
Err(e)
}
}
}
}
#[allow(missing_docs, clippy::large_enum_variant)]
#[derive(thiserror::Error, Debug)]
pub enum APIError {
#[error(transparent)]
RequestError(#[from] reqwest::Error),
#[error(transparent)]
RequestMiddlewareError(#[from] reqwest_middleware::Error),
#[error(transparent)]
DeserializationError(#[from] std::io::Error),
}