1use crate::cmd::{parse_response, ApiError, ApiErrorKind, Cmd, ProtocolError};
2use crate::token::AcquireToken;
3use crate::types::AuthToken;
4use anyhow::{anyhow, Context};
5use std::sync::Arc;
6use std::sync::Mutex;
7use std::time::Duration;
8use thiserror::Error;
9
10#[derive(Debug)]
11pub struct Client {
12 account_id: String,
13 base_url: String,
14 http: reqwest::Client,
15 cached_auth_token: Arc<Mutex<Option<AuthToken>>>,
16 acquiring_auth_token: tokio::sync::Mutex<()>,
17}
18
19#[derive(Error, Debug)]
20pub enum ClientError {
21 #[error("API Error: {0}")]
22 ApiError(#[from] ApiError),
23 #[error("Protocol Error: {0}")]
27 ProtocolError(#[from] ProtocolError),
28 #[error("request processing error: {:?}", .0)]
29 Other(#[from] anyhow::Error),
30}
31
32impl Client {
33 pub fn new(base_url: impl ToString, account_id: String, user_agent: &str) -> anyhow::Result<Self> {
34 let mut base_url = base_url.to_string();
35 if !base_url.ends_with('/') {
36 base_url += "/"
37 }
38 Ok(Self {
39 account_id,
40 base_url,
41 cached_auth_token: Arc::new(Mutex::new(None)),
42 http: reqwest::Client::builder()
43 .timeout(Duration::from_secs(60))
44 .read_timeout(Duration::from_secs(10))
45 .user_agent(user_agent)
46 .build()
47 .context("failed to initialize HTTP client")?,
48 acquiring_auth_token: tokio::sync::Mutex::new(()),
49 })
50 }
51
52 fn clear_auth_token(&self, token: AuthToken) {
53 let mut guard = self.cached_auth_token.lock().unwrap();
54 if guard.as_ref() == Some(&token) {
55 guard.take();
56 }
57 }
58
59 pub async fn acquire_auth_token(&self) -> Result<AuthToken, ClientError> {
60 if let Some(auth_token) = self.get_auth_token() {
61 return Ok(auth_token);
62 }
63
64 let acquiring_auth_token = self.acquiring_auth_token.lock().await;
65
66 if let Some(auth_token) = self.get_auth_token() {
67 return Ok(auth_token);
68 }
69 let account_id = self.account_id.clone();
70 let request = AcquireToken { account_id }.to_request(&self.base_url)?;
71 let res = self.send_http(request).await?;
72 let auth_token: String = parse_response(res).await?;
73 let auth_token: AuthToken = auth_token.into();
74 self.set_auth_token(Some(auth_token.clone()));
75
76 drop(acquiring_auth_token);
77 Ok(auth_token)
78 }
79
80 pub fn get_auth_token(&self) -> Option<AuthToken> {
81 self.cached_auth_token.lock().unwrap().clone()
82 }
83
84 pub fn set_auth_token(&self, token: Option<AuthToken>) {
85 *self.cached_auth_token.lock().unwrap() = token
86 }
87
88 async fn send_http(&self, request: http::Request<String>) -> anyhow::Result<reqwest::Response> {
89 let request = request.try_into().context("could not construct reqwest::Request")?;
90 self.http.execute(request).await.context("error executing request")
91 }
92
93 pub async fn run<C: Cmd>(&self, cmd: C) -> Result<C::Output, ClientError> {
94 for _ in 0..3 {
95 let auth_token = self.acquire_auth_token().await?;
96 if let Some(output) = self.try_run::<C>(&cmd, &auth_token).await? {
97 return Ok(output);
98 }
99 self.clear_auth_token(auth_token);
100 }
101
102 Err(anyhow!("repeatedly acquired invalid auth token").into())
103 }
104
105 async fn try_run<C: Cmd>(&self, body: &C, auth_token: &AuthToken) -> Result<Option<C::Output>, ClientError> {
108 let request = body.to_request(&self.base_url, auth_token)?;
109 let res = self.send_http(request).await?;
110 match parse_response(res).await {
111 Ok(output) => Ok(Some(output)),
112 Err(ClientError::ApiError(error)) => match error.body.error {
113 ApiErrorKind::MissingOrInvalidAuthToken {} => Ok(None),
114 _ => Err(ClientError::ApiError(error)),
115 },
116 Err(err) => Err(err),
117 }
118 }
119}