amazon_spapi/client/
client.rs1use anyhow::Result;
2use reqwest::header::HeaderMap;
3use reqwest::Client;
4use serde::Deserialize;
5use std::collections::HashMap;
6use std::sync::Arc;
7use std::time::Duration;
8use tokio::sync::Mutex;
9
10use crate::apis::configuration::Configuration;
11use crate::client::{ApiEndpoint, ApiMethod, AuthClient, RateLimiter, SpapiConfig};
12
13pub struct SpapiClient {
16 client: Client,
17 auth_client: Arc<Mutex<AuthClient>>,
18 config: SpapiConfig,
19 rate_limiter: RateLimiter,
20}
21
22impl SpapiClient {
23 pub fn new(config: SpapiConfig) -> Result<Self> {
25 let user_agent = if let Some(ua) = &config.user_agent {
26 ua.clone()
27 } else {
28 Self::get_user_agent()
30 };
31
32 let client = Client::builder()
33 .timeout(std::time::Duration::from_secs(
34 config.timeout_sec.unwrap_or(30),
35 ))
36 .user_agent(&user_agent)
37 .build()?;
38
39 let auth_client = AuthClient::new(
40 config.client_id.clone(),
41 config.client_secret.clone(),
42 config.refresh_token.clone(),
43 &user_agent,
44 )?;
45
46 let rate_limiter = RateLimiter::new();
48
49 Ok(Self {
50 client, auth_client: Arc::new(Mutex::new(auth_client)),
52 config,
53 rate_limiter,
54 })
55 }
56
57 pub fn limiter(&self) -> &RateLimiter {
58 &self.rate_limiter
59 }
60
61 pub fn get_user_agent() -> String {
63 let platform = format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH);
64 format!(
65 "amazon-spapi/v{} (Language=Rust; Platform={})",
66 env!("CARGO_PKG_VERSION"),
67 platform
68 )
69 }
70
71 pub fn get_base_url(&self) -> String {
73 if self.config.sandbox {
74 format!("https://sandbox.sellingpartnerapi-na.amazon.com")
75 } else {
76 format!("https://sellingpartnerapi-na.amazon.com")
77 }
78 }
79
80 pub fn get_marketplace_id(&self) -> &str {
82 &self.config.marketplace_id
83 }
84
85 pub async fn get_access_token(&self) -> Result<String> {
87 let mut auth_client = self.auth_client.lock().await;
88 auth_client.get_access_token().await
89 }
90
91 pub fn is_sandbox(&self) -> bool {
93 self.config.sandbox
94 }
95
96 pub async fn request(
98 &self,
99 endpoint: &ApiEndpoint,
100 query: Option<Vec<(String, String)>>,
101 header: Option<Vec<(&'static str, String)>>,
102 body: Option<&str>,
103 ) -> Result<String> {
104 let access_token = {
106 let mut auth_client = self.auth_client.lock().await;
107 auth_client.get_access_token().await?
108 };
109
110 let full_url = if query.is_none() {
111 format!("{}{}", self.get_base_url(), endpoint.get_path())
112 } else {
113 let query_str = serde_urlencoded::to_string(&query)?;
114 format!(
115 "{}{}?{}",
116 self.get_base_url(),
117 endpoint.get_path(),
118 query_str
119 )
120 };
121
122 log::debug!("Making {} request to: {}", endpoint.method, full_url);
123
124 let mut headers = HeaderMap::new();
126 headers.insert("Content-Type", "application/json; charset=utf-8".parse()?);
127 headers.insert("host", "sellingpartnerapi-na.amazon.com".parse()?);
128 headers.insert("x-amz-access-token", access_token.parse()?);
129 headers.insert(
130 "x-amz-date",
131 chrono::Utc::now()
133 .format("%Y%m%dT%H%M%SZ")
134 .to_string()
135 .parse()?,
136 );
137 headers.insert("user-agent", Self::get_user_agent().parse()?);
138 if let Some(custom_headers) = header {
139 for (key, value) in custom_headers {
140 headers.insert(key, value.parse()?);
141 }
142 }
143
144 let mut request_builder = match endpoint.method {
146 ApiMethod::Get => self.client.get(&full_url),
147 ApiMethod::Post => self.client.post(&full_url),
148 ApiMethod::Put => self.client.put(&full_url),
149 ApiMethod::Delete => self.client.delete(&full_url),
150 ApiMethod::Patch => self.client.patch(&full_url),
151 };
152
153 request_builder = request_builder.headers(headers);
155
156 if let Some(query_params) = query {
158 request_builder = request_builder.query(&query_params);
159 }
160
161 if let Some(body_content) = body {
163 request_builder = request_builder.body(body_content.to_string());
164 }
165
166 self.rate_limiter
168 .wait_for_token(&endpoint.rate_limit_key(), endpoint.rate, endpoint.burst)
169 .await?;
170
171 let response = request_builder.send().await;
172
173 self.rate_limiter
175 .record_response(&endpoint.rate_limit_key())
176 .await?;
177
178 let response = response?;
179 log::debug!("Response status: {}", response.status());
180
181 let response_status = response.status();
182 if response_status.is_success() {
183 let text = response.text().await?;
184 Ok(text)
185 } else {
186 let error_text = response.text().await?;
187 Err(anyhow::anyhow!(
188 "Request {} failed with status {}: {}",
189 endpoint.get_path(),
190 response_status,
191 error_text
192 ))
193 }
194 }
195
196 pub async fn upload(&self, url: &str, content: &str, content_type: &str) -> Result<()> {
198 let response = self
199 .client
200 .put(url)
201 .header("Content-Type", content_type)
202 .body(content.to_string())
203 .send()
204 .await?;
205
206 if response.status().is_success() {
207 log::info!("Feed document content uploaded successfully");
208 Ok(())
209 } else {
210 let status = response.status();
211 let error_text = response.text().await?;
212 Err(anyhow::anyhow!(
213 "Failed to upload feed document content: {} - Response: {}",
214 status,
215 error_text
216 ))
217 }
218 }
219
220 pub async fn download(&self, url: &str) -> Result<String> {
222 let response = self.get_http_client().get(url).send().await?;
223
224 if response.status().is_success() {
225 let content = response.text().await?;
226 log::info!("Feed document content downloaded successfully");
227 Ok(content)
228 } else {
229 let status = response.status();
230 let error_text = response.text().await?;
231 Err(anyhow::anyhow!(
232 "Failed to download feed document content: {} - Response: {}",
233 status,
234 error_text
235 ))
236 }
237 }
238
239 pub async fn get_rate_limit_status(&self) -> Result<HashMap<String, (f64, f64, u32)>> {
241 Ok(self.rate_limiter.get_token_status().await?)
242 }
243
244 pub async fn check_rate_limit_availability(&self, endpoint_id: &String) -> Result<bool> {
246 Ok(self
247 .rate_limiter
248 .check_token_availability(endpoint_id)
249 .await?)
250 }
251
252 pub async fn refresh_access_token_if_needed(&self) -> Result<()> {
254 let mut auth_client = self.auth_client.lock().await;
255 if !auth_client.is_token_valid() {
256 auth_client.refresh_access_token().await?;
257 }
258 Ok(())
259 }
260
261 pub async fn force_refresh_token(&self) -> Result<()> {
263 let mut auth_client = self.auth_client.lock().await;
264 auth_client.refresh_access_token().await?;
265 Ok(())
266 }
267
268 pub fn get_http_client(&self) -> &Client {
270 &self.client
271 }
272
273 pub async fn create_configuration(&self) -> Result<Configuration> {
276 let mut headers = reqwest::header::HeaderMap::new();
277 headers.insert("Content-Type", "application/json; charset=utf-8".parse()?);
278 headers.insert("host", "sellingpartnerapi-na.amazon.com".parse()?);
279 headers.insert(
280 "x-amz-access-token",
281 self.get_access_token().await?.parse()?,
282 );
283 headers.insert(
284 "x-amz-date",
285 chrono::Utc::now()
286 .format("%Y%m%dT%H%M%SZ")
287 .to_string()
288 .parse()?,
289 );
290 headers.insert(
291 "user-agent",
292 self.config
293 .user_agent
294 .clone()
295 .unwrap_or_else(|| Self::get_user_agent())
296 .parse()?,
297 );
298
299 let http_client = reqwest::Client::builder()
300 .timeout(std::time::Duration::from_secs(
301 self.config.timeout_sec.unwrap_or(30),
302 ))
303 .default_headers(headers)
304 .build()?;
305
306 let configuration = Configuration {
307 base_path: self.get_base_url(),
308 client: http_client,
309 ..Default::default()
310 };
311 Ok(configuration)
312 }
313
314 pub fn from_json<'a, T>(s: &'a str) -> Result<T>
315 where
316 T: Deserialize<'a>,
317 {
318 serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}: {}", e, s))
319 }
320}