Skip to main content

amazon_spapi/client/
client.rs

1use 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::{AuthClient, RateLimiter, Region, SpapiConfig};
12
13pub struct SpapiClient {
14    client: Client,
15    auth_client: Arc<Mutex<AuthClient>>,
16    config: SpapiConfig,
17    rate_limiter: RateLimiter,
18}
19
20impl SpapiClient {
21    /// Create a new SP API client with the given configuration
22    pub fn new(config: SpapiConfig) -> Result<Self> {
23        let user_agent = if let Some(ua) = &config.user_agent {
24            ua.clone()
25        } else {
26            // Default user agent if not provided
27            Self::get_default_user_agent()
28        };
29
30        let mut client_builder = Client::builder()
31            .timeout(std::time::Duration::from_secs(
32                config.timeout_sec.unwrap_or(30),
33            ))
34            .user_agent(&user_agent);
35
36        if let Some(proxy_url) = &config.proxy {
37            let proxy = reqwest::Proxy::all(proxy_url)?;
38            client_builder = client_builder.proxy(proxy);
39        }
40
41        let client = client_builder.build()?;
42
43        let auth_client = AuthClient::new(config.clone())?;
44
45        // Initialize rate limiter if enabled
46        let rate_limiter =
47            RateLimiter::new_with_safety_factor(config.rate_limit_factor.unwrap_or(1.05));
48
49        Ok(Self {
50            client, //: Client::new(),
51            auth_client: Arc::new(Mutex::new(auth_client)),
52            config,
53            rate_limiter,
54        })
55    }
56
57    /// Get a reference to the rate limiter
58    pub fn limiter(&self) -> &RateLimiter {
59        &self.rate_limiter
60    }
61
62    /// Get default user agent for the client
63    pub fn get_default_user_agent() -> String {
64        let platform = format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH);
65        format!(
66            "amazon-spapi/v{} (Language=Rust; Platform={})",
67            env!("CARGO_PKG_VERSION"),
68            platform
69        )
70    }
71
72    /// Get the base URL for the client
73    pub fn get_base_url(&self) -> String {
74        if self.config.sandbox {
75            match self.config.region {
76                Region::NorthAmerica => format!("https://sandbox.sellingpartnerapi-na.amazon.com"),
77                Region::Europe => format!("https://sandbox.sellingpartnerapi-eu.amazon.com"),
78                Region::FarEast => format!("https://sandbox.sellingpartnerapi-fe.amazon.com"),
79            }
80        } else {
81            match self.config.region {
82                Region::NorthAmerica => format!("https://sellingpartnerapi-na.amazon.com"),
83                Region::Europe => format!("https://sellingpartnerapi-eu.amazon.com"),
84                Region::FarEast => format!("https://sellingpartnerapi-fe.amazon.com"),
85            }
86        }
87    }
88
89    /// Get access token from the auth client
90    pub async fn get_access_token(&self) -> Result<String> {
91        let mut auth_client = self.auth_client.lock().await;
92        auth_client.get_access_token().await
93    }
94
95    /// Check if the client is in sandbox mode
96    pub fn is_sandbox(&self) -> bool {
97        self.config.sandbox
98    }
99
100    /// Upload content to the feed document URL (direct S3 upload)
101    pub async fn upload(&self, url: &str, content: &str, content_type: &str) -> Result<()> {
102        let response = self
103            .client
104            .put(url)
105            .header("Content-Type", content_type)
106            .body(content.to_string())
107            .send()
108            .await?;
109
110        if response.status().is_success() {
111            log::info!("Feed document content uploaded successfully");
112            Ok(())
113        } else {
114            let status = response.status();
115            let error_text = response.text().await?;
116            Err(anyhow::anyhow!(
117                "Failed to upload feed document content: {} - Response: {}",
118                status,
119                error_text
120            ))
121        }
122    }
123
124    /// Download content from a feed document URL
125    pub async fn download(&self, url: &str) -> Result<String> {
126        let response = self.get_http_client().get(url).send().await?;
127
128        if response.status().is_success() {
129            let content = response.text().await?;
130            log::info!("Feed document content downloaded successfully");
131            Ok(content)
132        } else {
133            let status = response.status();
134            let error_text = response.text().await?;
135            Err(anyhow::anyhow!(
136                "Failed to download feed document content: {} - Response: {}",
137                status,
138                error_text
139            ))
140        }
141    }
142
143    /// Check if rate limiting is enabled and get token status
144    #[allow(unused)]
145    #[deprecated]
146    pub async fn get_rate_limit_status(&self) -> Result<HashMap<String, (f64, f64, u32)>> {
147        Ok(self.rate_limiter.get_token_status().await?)
148    }
149
150    /// Check if a token is available for a specific endpoint without consuming it
151    #[allow(unused)]
152    #[deprecated]
153    pub async fn check_rate_limit_availability(&self, endpoint_id: &String) -> Result<bool> {
154        Ok(self
155            .rate_limiter
156            .check_token_availability(endpoint_id)
157            .await?)
158    }
159
160    /// Refresh the access token if needed
161    pub async fn refresh_access_token_if_needed(&self) -> Result<()> {
162        let mut auth_client = self.auth_client.lock().await;
163        if !auth_client.is_token_valid() {
164            auth_client.refresh_access_token().await?;
165        }
166        Ok(())
167    }
168
169    /// Force refresh the access token
170    pub async fn force_refresh_token(&self) -> Result<()> {
171        let mut auth_client = self.auth_client.lock().await;
172        auth_client.refresh_access_token().await?;
173        Ok(())
174    }
175
176    /// Get access to the underlying HTTP client for direct requests
177    pub fn get_http_client(&self) -> &Client {
178        &self.client
179    }
180
181    /// Create a new configuration for the generated APIs
182    /// This function refreshes the access token and sets up the configuration
183    pub async fn create_configuration(&self) -> Result<Configuration> {
184        let mut headers = reqwest::header::HeaderMap::new();
185        headers.insert("Content-Type", "application/json; charset=utf-8".parse()?);
186        headers.insert("host", "sellingpartnerapi-na.amazon.com".parse()?);
187        headers.insert(
188            "x-amz-access-token",
189            self.get_access_token().await?.parse()?,
190        );
191        headers.insert(
192            "x-amz-date",
193            {
194                let now = time::OffsetDateTime::now_utc();
195                format!(
196                    "{:04}{:02}{:02}T{:02}{:02}{:02}Z",
197                    now.year(),
198                    now.month() as u8,
199                    now.day(),
200                    now.hour(),
201                    now.minute(),
202                    now.second()
203                )
204            }
205            .parse()?,
206        );
207        headers.insert(
208            "user-agent",
209            self.config
210                .user_agent
211                .clone()
212                .unwrap_or_else(|| Self::get_default_user_agent())
213                .parse()?,
214        );
215
216        let user_agent = if let Some(ua) = &self.config.user_agent {
217            ua.clone()
218        } else {
219            // Default user agent if not provided
220            Self::get_default_user_agent()
221        };
222
223        let mut client_builder = Client::builder()
224            .timeout(std::time::Duration::from_secs(
225                self.config.timeout_sec.unwrap_or(30),
226            ))
227            .default_headers(headers)
228            .user_agent(&user_agent);
229
230        if let Some(proxy_url) = &self.config.proxy {
231            let proxy = reqwest::Proxy::all(proxy_url)?;
232            client_builder = client_builder.proxy(proxy);
233        }
234
235        let http_client = client_builder.build()?;
236
237        let configuration = Configuration {
238            base_path: self.get_base_url(),
239            client: crate::apis::configuration::CustomClient::new(http_client, self.config.retry_count.unwrap_or(0)),
240            user_agent: Some(
241                self.config
242                    .user_agent
243                    .clone()
244                    .unwrap_or_else(|| Self::get_default_user_agent()),
245            ),
246        };
247        Ok(configuration)
248    }
249
250    pub fn from_json<'a, T>(s: &'a str) -> Result<T>
251    where
252        T: Deserialize<'a>,
253    {
254        serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}: {}", e, s))
255    }
256}