use anyhow::Result;
use reqwest::header::HeaderMap;
use reqwest::Client;
use serde::Deserialize;
use std::collections::HashMap;
use std::sync::Arc;
use std::time::Duration;
use tokio::sync::Mutex;
use crate::apis::configuration::Configuration;
use crate::client::{AuthClient, RateLimiter, Region, SpapiConfig};
pub struct SpapiClient {
client: Client,
auth_client: Arc<Mutex<AuthClient>>,
config: SpapiConfig,
rate_limiter: RateLimiter,
}
impl SpapiClient {
pub fn new(config: SpapiConfig) -> Result<Self> {
let user_agent = if let Some(ua) = &config.user_agent {
ua.clone()
} else {
Self::get_default_user_agent()
};
let mut client_builder = Client::builder()
.timeout(std::time::Duration::from_secs(
config.timeout_sec.unwrap_or(30),
))
.user_agent(&user_agent);
if let Some(proxy_url) = &config.proxy {
let proxy = reqwest::Proxy::all(proxy_url)?;
client_builder = client_builder.proxy(proxy);
}
let client = client_builder.build()?;
let auth_client = AuthClient::new(config.clone())?;
let rate_limiter =
RateLimiter::new_with_safety_factor(config.rate_limit_factor.unwrap_or(1.05));
Ok(Self {
client, auth_client: Arc::new(Mutex::new(auth_client)),
config,
rate_limiter,
})
}
pub fn limiter(&self) -> &RateLimiter {
&self.rate_limiter
}
pub fn get_default_user_agent() -> String {
let platform = format!("{}/{}", std::env::consts::OS, std::env::consts::ARCH);
format!(
"amazon-spapi/v{} (Language=Rust; Platform={})",
env!("CARGO_PKG_VERSION"),
platform
)
}
pub fn get_base_url(&self) -> String {
if self.config.sandbox {
match self.config.region {
Region::NorthAmerica => format!("https://sandbox.sellingpartnerapi-na.amazon.com"),
Region::Europe => format!("https://sandbox.sellingpartnerapi-eu.amazon.com"),
Region::FarEast => format!("https://sandbox.sellingpartnerapi-fe.amazon.com"),
}
} else {
match self.config.region {
Region::NorthAmerica => format!("https://sellingpartnerapi-na.amazon.com"),
Region::Europe => format!("https://sellingpartnerapi-eu.amazon.com"),
Region::FarEast => format!("https://sellingpartnerapi-fe.amazon.com"),
}
}
}
pub async fn get_access_token(&self) -> Result<String> {
let mut auth_client = self.auth_client.lock().await;
auth_client.get_access_token().await
}
pub fn is_sandbox(&self) -> bool {
self.config.sandbox
}
pub async fn upload(&self, url: &str, content: &str, content_type: &str) -> Result<()> {
let response = self
.client
.put(url)
.header("Content-Type", content_type)
.body(content.to_string())
.send()
.await?;
if response.status().is_success() {
log::info!("Feed document content uploaded successfully");
Ok(())
} else {
let status = response.status();
let error_text = response.text().await?;
Err(anyhow::anyhow!(
"Failed to upload feed document content: {} - Response: {}",
status,
error_text
))
}
}
pub async fn download(&self, url: &str) -> Result<String> {
let response = self.get_http_client().get(url).send().await?;
if response.status().is_success() {
let content = response.text().await?;
log::info!("Feed document content downloaded successfully");
Ok(content)
} else {
let status = response.status();
let error_text = response.text().await?;
Err(anyhow::anyhow!(
"Failed to download feed document content: {} - Response: {}",
status,
error_text
))
}
}
#[allow(unused)]
#[deprecated]
pub async fn get_rate_limit_status(&self) -> Result<HashMap<String, (f64, f64, u32)>> {
Ok(self.rate_limiter.get_token_status().await?)
}
#[allow(unused)]
#[deprecated]
pub async fn check_rate_limit_availability(&self, endpoint_id: &String) -> Result<bool> {
Ok(self
.rate_limiter
.check_token_availability(endpoint_id)
.await?)
}
pub async fn refresh_access_token_if_needed(&self) -> Result<()> {
let mut auth_client = self.auth_client.lock().await;
if !auth_client.is_token_valid() {
auth_client.refresh_access_token().await?;
}
Ok(())
}
pub async fn force_refresh_token(&self) -> Result<()> {
let mut auth_client = self.auth_client.lock().await;
auth_client.refresh_access_token().await?;
Ok(())
}
pub fn get_http_client(&self) -> &Client {
&self.client
}
pub async fn create_configuration(&self) -> Result<Configuration> {
let mut headers = reqwest::header::HeaderMap::new();
headers.insert("Content-Type", "application/json; charset=utf-8".parse()?);
headers.insert("host", "sellingpartnerapi-na.amazon.com".parse()?);
headers.insert(
"x-amz-access-token",
self.get_access_token().await?.parse()?,
);
headers.insert(
"x-amz-date",
{
let now = time::OffsetDateTime::now_utc();
format!(
"{:04}{:02}{:02}T{:02}{:02}{:02}Z",
now.year(),
now.month() as u8,
now.day(),
now.hour(),
now.minute(),
now.second()
)
}
.parse()?,
);
headers.insert(
"user-agent",
self.config
.user_agent
.clone()
.unwrap_or_else(|| Self::get_default_user_agent())
.parse()?,
);
let user_agent = if let Some(ua) = &self.config.user_agent {
ua.clone()
} else {
Self::get_default_user_agent()
};
let mut client_builder = Client::builder()
.timeout(std::time::Duration::from_secs(
self.config.timeout_sec.unwrap_or(30),
))
.default_headers(headers)
.user_agent(&user_agent);
if let Some(proxy_url) = &self.config.proxy {
let proxy = reqwest::Proxy::all(proxy_url)?;
client_builder = client_builder.proxy(proxy);
}
let http_client = client_builder.build()?;
let configuration = Configuration {
base_path: self.get_base_url(),
client: crate::apis::configuration::CustomClient::new(http_client, self.config.retry_count.unwrap_or(0)),
user_agent: Some(
self.config
.user_agent
.clone()
.unwrap_or_else(|| Self::get_default_user_agent()),
),
};
Ok(configuration)
}
pub fn from_json<'a, T>(s: &'a str) -> Result<T>
where
T: Deserialize<'a>,
{
serde_json::from_str(s).map_err(|e| anyhow::anyhow!("Failed to parse JSON: {}: {}", e, s))
}
}