use super::{ Request, error, token, auth, }; use crate::utils::Bool; #[async_trait::async_trait] trait Interface { fn client(&self) -> &reqwest::Client; fn url(&self, endpoint: &str) -> Result; fn current_token(&self) -> Option>; async fn refresh_token(&self, api: Api) -> Result>, error::Token>; } #[derive(Debug)] struct Internal { client: reqwest::Client, base: reqwest::Url, token_provider: T, } impl Internal { fn new(client: reqwest::Client, base: reqwest::Url, token_provider: T) -> std::sync::Arc { std::sync::Arc::new(Self { client, base, token_provider }) } #[tracing::instrument(skip_all, fields(endpoint=request.endpoint(), request, token))] async fn call(&self, request: &R, token: Option<&token::Token>) -> Result { let url = self.base.join(request.endpoint()).map_err(error::Api::Url)?; let builder = self.client.request(R::METHOD, url); let builder = if let Some(token) = token { token.r#use(builder) } else { builder }; let response = builder.json(request) .send() .await .map_err(error::Api::Reqwest)?; match response.status() { reqwest::StatusCode::OK => (), status => return Err(error::Api::Http(status)), } let result: R::Response = response.json().await?; Ok(result) } } async fn deserialize(response: reqwest::Response) -> Result { let response: R::Response = response.json().await.map_err(error::Api::Reqwest)?; Ok(response) } #[tracing::instrument(skip_all, fields(url, request, token))] async fn call(client: &reqwest::Client, url: reqwest::Url, request: &R, token: Option<&token::Token>) -> Result { let builder = client.request(R::METHOD, url); let builder = if let Some(token) = token { builder.bearer_auth(token.as_ref()) } else { builder }; let response = builder.json(request) .send() .await .map_err(error::Api::Reqwest)?; match response.status() { reqwest::StatusCode::OK => deserialize::(response).await, status => Err(error::Api::Http(status)), } } #[async_trait::async_trait] impl Interface for Internal { fn client(&self) -> &reqwest::Client { &self.client } fn url(&self, endpoint: &str) -> Result { self.base.join(endpoint) } fn current_token(&self) -> Option> { self.token_provider.current() } async fn refresh_token(&self, api: Api) -> Result>, error::Token> { self.token_provider.refresh(api).await } } #[derive(Debug)] struct NoToken; #[async_trait::async_trait] impl token::Provider for NoToken { fn current(&self) -> Option> { None } async fn refresh(&self, _api: Api) -> Result>, error::Token> { Ok(None) } } #[derive(Clone)] pub struct Api(std::sync::Arc); impl Api { pub async fn new(auth: A) -> Result { Self::new_with_base("https://api.spond.com", auth).await } pub async fn new_with_base(base: &str, auth: A) -> Result { let base = reqwest::Url::parse(base)?; let host = base.host_str().unwrap().to_owned(); let client = reqwest::Client::builder() //.https_only(true) .redirect(reqwest::redirect::Policy::limited(1)) .retry(reqwest::retry::for_host(host) .max_retries_per_request(2) .classify_fn(|req_rep| { use reqwest::StatusCode; match req_rep.status() { Some(StatusCode::UNAUTHORIZED) => req_rep.retryable(), _ => req_rep.success(), } })) // TODO //.cookie_store(true) .user_agent("spond-selection-bot") .read_timeout(std::time::Duration::from_secs(5)) .connect_timeout(std::time::Duration::from_secs(15)) .connection_verbose(true) .build()?; let api = Api(Internal::new(client.clone(), base.clone(), NoToken)); let provider = auth.authenticate(api).await?; tracing::info!("{:?}", provider); let api = Api(Internal::new(client, base, provider)); Ok(api) } pub async fn call(&self, request: &R) -> Result { //async fn call(&self, api: &Api, call: &C, request: &C::Request) -> Result> { // let mut token = self.token_provider.current(); // loop { // match self.common.call(call, request, &token) { // Err(error::Call::::Http(reqwest::StatusCode::UNAUTHORIZED)) if token.is_some_and(|token|token.expired()) // => token = self.token_provider.refresh(api).map_err(error::Call::::Token)?, // result => return result, // } // } //} let client = self.0.client(); if R::Authorization::VALUE == false { let url = self.0.url(request.endpoint())?; call(client, url, request, None).await } else { let mut current = self.0.current_token(); loop { use std::ops::Deref; let token = if let Some(ref token) = current { Some(token.deref()) } else { None }; let url = self.0.url(request.endpoint())?; current = match call(client, url, request, token).await { Err(error::Api::Http(reqwest::StatusCode::UNAUTHORIZED)) if current.is_some_and(|t|t.expired()) => self.0.refresh_token(self.clone()).await?, result => break result, } } } } } #[cfg(test)] mod test { use super::*; #[tokio::test] async fn mock() {} }