smbcloud_ascapi_core/
client.rs1use crate::auth::ApiKey;
2use crate::error::{Error, Result};
3use crate::jsonapi::ErrorDocument;
4use reqwest::{Method, StatusCode};
5use serde::de::DeserializeOwned;
6use serde::Serialize;
7use std::sync::Mutex;
8use std::time::{Duration, Instant};
9
10const BASE_URL: &str = "https://api.appstoreconnect.apple.com";
11const TOKEN_REFRESH_MARGIN: Duration = Duration::from_secs(60);
14
15struct CachedToken {
16 value: String,
17 minted_at: Instant,
18 lifetime: Duration,
19}
20
21pub struct Client {
32 http: reqwest::Client,
33 api_key: ApiKey,
34 base_url: String,
35 token: Mutex<Option<CachedToken>>,
36}
37
38impl Client {
39 pub fn new(api_key: ApiKey) -> Self {
40 Self {
41 http: reqwest::Client::new(),
42 api_key,
43 base_url: BASE_URL.to_string(),
44 token: Mutex::new(None),
45 }
46 }
47
48 pub fn with_base_url(mut self, base_url: impl Into<String>) -> Self {
51 self.base_url = base_url.into();
52 self
53 }
54
55 fn bearer_token(&self) -> Result<String> {
56 let mut guard = self
57 .token
58 .lock()
59 .unwrap_or_else(|poisoned| poisoned.into_inner());
60
61 if let Some(cached) = guard.as_ref() {
62 if cached.minted_at.elapsed() + TOKEN_REFRESH_MARGIN < cached.lifetime {
63 return Ok(cached.value.clone());
64 }
65 }
66
67 let value = self.api_key.token()?;
68 *guard = Some(CachedToken {
69 value: value.clone(),
70 minted_at: Instant::now(),
71 lifetime: Duration::from_secs(self.api_key.lifetime_secs()),
72 });
73 Ok(value)
74 }
75
76 pub async fn request<B: Serialize, T: DeserializeOwned>(
80 &self,
81 method: Method,
82 path: &str,
83 query: &[(&str, &str)],
84 body: Option<&B>,
85 ) -> Result<T> {
86 let (status, bytes) = self.send(method, path, query, body).await?;
87 if !status.is_success() {
88 return Err(api_error(status, &bytes));
89 }
90 Ok(serde_json::from_slice(&bytes)?)
91 }
92
93 pub async fn request_no_content<B: Serialize>(
95 &self,
96 method: Method,
97 path: &str,
98 query: &[(&str, &str)],
99 body: Option<&B>,
100 ) -> Result<()> {
101 let (status, bytes) = self.send(method, path, query, body).await?;
102 if !status.is_success() {
103 return Err(api_error(status, &bytes));
104 }
105 Ok(())
106 }
107
108 async fn send<B: Serialize>(
109 &self,
110 method: Method,
111 path: &str,
112 query: &[(&str, &str)],
113 body: Option<&B>,
114 ) -> Result<(StatusCode, Vec<u8>)> {
115 let url = format!("{}{}", self.base_url, path);
116 let token = self.bearer_token()?;
117 let mut req = self.http.request(method, &url).bearer_auth(token);
118 if !query.is_empty() {
119 req = req.query(query);
120 }
121 if let Some(b) = body {
122 req = req.json(b);
123 }
124
125 let response = req.send().await?;
126 let status = response.status();
127 let bytes = response.bytes().await?.to_vec();
128 Ok((status, bytes))
129 }
130
131 pub async fn upload_bytes(
138 &self,
139 method: Method,
140 url: &str,
141 headers: &[(String, String)],
142 body: Vec<u8>,
143 ) -> Result<()> {
144 let mut req = self.http.request(method, url);
145 for (name, value) in headers {
146 req = req.header(name.as_str(), value.as_str());
147 }
148 let response = req.body(body).send().await?;
149 let status = response.status();
150 if !status.is_success() {
151 let bytes = response.bytes().await?.to_vec();
152 return Err(api_error(status, &bytes));
153 }
154 Ok(())
155 }
156}
157
158fn api_error(status: StatusCode, bytes: &[u8]) -> Error {
159 let detail = match serde_json::from_slice::<ErrorDocument>(bytes) {
160 Ok(doc) if !doc.errors.is_empty() => doc
161 .errors
162 .iter()
163 .map(|e| format!("{} ({}): {}", e.title, e.code, e.detail))
164 .collect::<Vec<_>>()
165 .join("; "),
166 _ => String::from_utf8_lossy(bytes).into_owned(),
167 };
168 Error::Api {
169 status: status.as_u16(),
170 detail,
171 }
172}