1use crate::auth::{Credentials, OAuth};
2use crate::endpoints::Tweets;
3use crate::types::{XError, XResult};
4use reqwest::{Client as HttpClient, Request};
5use std::time::Duration;
6
7#[derive(Clone)]
9pub struct XClient {
10 http_client: HttpClient,
11 oauth: OAuth,
12 base_url: String,
13}
14
15impl XClient {
16 pub const DEFAULT_BASE_URL: &'static str = "https://api.twitter.com";
18
19 pub const DEFAULT_TIMEOUT_SECS: u64 = 30;
21
22 pub fn new(credentials: Credentials) -> XResult<Self> {
41 Self::builder(credentials).build()
42 }
43
44 pub fn builder(credentials: Credentials) -> XClientBuilder {
49 XClientBuilder::new(credentials)
50 }
51
52 pub fn tweets(&self) -> Tweets<'_> {
54 Tweets::new(self)
55 }
56
57 pub(crate) async fn execute(&self, mut request: Request) -> XResult<reqwest::Response> {
59 request.headers_mut().insert(
61 "User-Agent",
62 "x-sdk-rust/0.1.0"
63 .parse()
64 .map_err(|e| XError::Unknown(format!("Invalid User-Agent: {}", e)))?,
65 );
66
67 self.oauth.sign_request(&mut request)?;
69
70 let response = self.http_client.execute(request).await?;
72
73 let status = response.status();
75 if !status.is_success() {
76 return Err(self.handle_error_response(response).await);
77 }
78
79 Ok(response)
80 }
81
82 async fn handle_error_response(&self, response: reqwest::Response) -> XError {
84 let status = response.status();
85 let status_code = status.as_u16();
86
87 let retry_after = response
89 .headers()
90 .get("x-rate-limit-reset")
91 .and_then(|v| v.to_str().ok())
92 .and_then(|v| v.parse::<u64>().ok());
93
94 let error_message = response
96 .text()
97 .await
98 .unwrap_or_else(|_| "Unknown error".to_string());
99
100 match status_code {
101 401 | 403 => XError::AuthenticationFailed(error_message),
102 400 | 422 => XError::InvalidRequest(error_message),
103 429 => XError::RateLimitExceeded {
104 retry_after,
105 message: error_message,
106 },
107 _ => XError::ApiError {
108 code: status_code,
109 message: error_message,
110 },
111 }
112 }
113
114 pub(crate) fn base_url(&self) -> &str {
116 &self.base_url
117 }
118
119 pub(crate) fn http_client(&self) -> &HttpClient {
121 &self.http_client
122 }
123}
124
125pub struct XClientBuilder {
127 credentials: Credentials,
128 base_url: Option<String>,
129 timeout: Option<Duration>,
130 http_client: Option<HttpClient>,
131}
132
133impl XClientBuilder {
134 fn new(credentials: Credentials) -> Self {
136 Self {
137 credentials,
138 base_url: None,
139 timeout: None,
140 http_client: None,
141 }
142 }
143
144 pub fn base_url(mut self, url: impl Into<String>) -> Self {
146 self.base_url = Some(url.into());
147 self
148 }
149
150 pub fn timeout(mut self, timeout: Duration) -> Self {
152 self.timeout = Some(timeout);
153 self
154 }
155
156 pub fn http_client(mut self, client: HttpClient) -> Self {
158 self.http_client = Some(client);
159 self
160 }
161
162 pub fn build(self) -> XResult<XClient> {
164 let oauth = OAuth::new(self.credentials)?;
165
166 let http_client = self.http_client.unwrap_or_else(|| {
167 HttpClient::builder()
168 .timeout(
169 self.timeout
170 .unwrap_or_else(|| Duration::from_secs(XClient::DEFAULT_TIMEOUT_SECS)),
171 )
172 .build()
173 .expect("Failed to build HTTP client")
174 });
175
176 Ok(XClient {
177 http_client,
178 oauth,
179 base_url: self
180 .base_url
181 .unwrap_or_else(|| XClient::DEFAULT_BASE_URL.to_string()),
182 })
183 }
184}
185
186#[cfg(test)]
187mod tests {
188 use super::*;
189
190 fn test_credentials() -> Credentials {
191 Credentials::new("test_key", "test_secret", "test_token", "test_token_secret")
192 }
193
194 #[test]
195 fn test_client_new() {
196 let credentials = test_credentials();
197 let client = XClient::new(credentials);
198 assert!(client.is_ok());
199 }
200
201 #[test]
202 fn test_client_builder() {
203 let credentials = test_credentials();
204 let client = XClient::builder(credentials)
205 .base_url("https://test.example.com")
206 .timeout(Duration::from_secs(10))
207 .build();
208
209 assert!(client.is_ok());
210 let client = client.unwrap();
211 assert_eq!(client.base_url(), "https://test.example.com");
212 }
213
214 #[test]
215 fn test_client_with_invalid_credentials() {
216 let credentials = Credentials::new("", "", "", "");
217 let client = XClient::new(credentials);
218 assert!(client.is_err());
219 }
220}