1use anyhow::{anyhow, Result};
4use serde::{Deserialize, Serialize};
5use std::str::FromStr;
6
7use crate::tweets::models::{Tweet, TweetFields, TweetMeta};
8
9#[derive(Debug)]
11pub struct IdempotencyConflictError {
12 pub client_request_id: String,
13}
14
15#[derive(Debug)]
19pub struct ThreadPartialFailureError {
20 pub failed_index: usize,
22 pub created_tweet_ids: Vec<String>,
24 pub message: String,
26}
27
28impl std::fmt::Display for ThreadPartialFailureError {
29 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
30 write!(
31 f,
32 "Thread posting failed at index {}: {} ({} tweets created before failure)",
33 self.failed_index,
34 self.message,
35 self.created_tweet_ids.len()
36 )
37 }
38}
39
40impl std::error::Error for ThreadPartialFailureError {}
41
42impl std::fmt::Display for IdempotencyConflictError {
43 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44 write!(
45 f,
46 "Operation with client_request_id '{}' already exists",
47 self.client_request_id
48 )
49 }
50}
51
52impl std::error::Error for IdempotencyConflictError {}
53
54#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum IfExistsPolicy {
57 Return,
59 Error,
61}
62
63impl FromStr for IfExistsPolicy {
64 type Err = anyhow::Error;
65
66 fn from_str(s: &str) -> Result<Self> {
67 match s {
68 "return" => Ok(Self::Return),
69 "error" => Ok(Self::Error),
70 _ => Err(anyhow!(
71 "Invalid if-exists policy: {}. Valid values: return, error",
72 s
73 )),
74 }
75 }
76}
77
78impl IfExistsPolicy {
79 pub fn as_str(&self) -> &'static str {
80 match self {
81 Self::Return => "return",
82 Self::Error => "error",
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
89pub struct CreateArgs {
90 pub text: String,
91 pub client_request_id: Option<String>,
92 pub if_exists: IfExistsPolicy,
93}
94
95#[derive(Debug, Clone)]
97pub struct ListArgs {
98 pub fields: Vec<TweetFields>,
99 pub limit: Option<usize>,
100 pub cursor: Option<String>,
101}
102
103#[derive(Debug, Clone)]
105pub struct EngagementArgs {
106 pub tweet_id: String,
107}
108
109#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct EngagementResult {
112 pub tweet_id: String,
113 pub success: bool,
114}
115
116#[derive(Debug, Clone)]
118pub struct ReplyArgs {
119 pub tweet_id: String,
120 pub text: String,
121 pub client_request_id: Option<String>,
122 pub if_exists: IfExistsPolicy,
123}
124
125#[derive(Debug, Clone)]
127pub struct ThreadArgs {
128 pub texts: Vec<String>,
129 pub client_request_id_prefix: Option<String>,
130 pub if_exists: IfExistsPolicy,
131}
132
133#[derive(Debug, Clone)]
135pub struct ShowArgs {
136 pub tweet_id: String,
137}
138
139#[derive(Debug, Clone)]
141pub struct ConversationArgs {
142 pub tweet_id: String,
143}
144
145#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CreateResult {
148 pub tweet: Tweet,
149 pub meta: TweetMeta,
150}
151
152#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ReplyResult {
155 pub tweet: Tweet,
156 pub meta: TweetMeta,
157}
158
159#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ThreadResult {
162 pub tweets: Vec<Tweet>,
163 pub meta: ThreadMeta,
164}
165
166#[derive(Debug, Clone, Serialize, Deserialize)]
168#[serde(rename_all = "camelCase")]
169pub struct ThreadMeta {
170 pub count: usize,
171 #[serde(skip_serializing_if = "Option::is_none")]
172 pub failed_index: Option<usize>,
173 pub created_tweet_ids: Vec<String>,
174 #[serde(skip_serializing_if = "Option::is_none")]
175 pub from_cache: Option<bool>,
176}
177
178#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ShowResult {
181 pub tweet: Tweet,
182}
183
184#[derive(Debug, Clone, Serialize, Deserialize)]
186#[serde(rename_all = "camelCase")]
187pub struct PaginationMeta {
188 #[serde(skip_serializing_if = "Option::is_none")]
189 pub next_cursor: Option<String>,
190 #[serde(skip_serializing_if = "Option::is_none")]
191 pub prev_cursor: Option<String>,
192}
193
194#[derive(Debug, Clone, Serialize, Deserialize)]
196pub struct ListResult {
197 pub tweets: Vec<Tweet>,
198 #[serde(skip_serializing_if = "Option::is_none")]
199 pub meta: Option<ListResultMeta>,
200}
201
202#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ListResultMeta {
205 pub pagination: PaginationMeta,
206}
207
208#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum ErrorKind {
211 Retryable,
213 NonRetryable,
215 Timeout,
217}
218
219#[derive(Debug)]
221pub struct ClassifiedError {
222 pub kind: ErrorKind,
223 pub status_code: Option<u16>,
224 pub message: String,
225 pub is_retryable: bool,
226 pub retry_after_ms: Option<u64>,
227}
228
229impl ClassifiedError {
230 pub fn from_status_code(status_code: u16, message: String) -> Self {
231 let (kind, is_retryable) = match status_code {
232 429 => (ErrorKind::Retryable, true),
233 500..=599 => (ErrorKind::Retryable, true),
234 400..=499 => (ErrorKind::NonRetryable, false),
235 _ => (ErrorKind::NonRetryable, false),
236 };
237
238 Self {
239 kind,
240 status_code: Some(status_code),
241 message,
242 is_retryable,
243 retry_after_ms: None,
244 }
245 }
246
247 pub fn timeout(message: String) -> Self {
248 Self {
249 kind: ErrorKind::Timeout,
250 status_code: None,
251 message,
252 is_retryable: true,
253 retry_after_ms: None,
254 }
255 }
256
257 pub fn with_retry_after(mut self, retry_after_ms: u64) -> Self {
258 self.retry_after_ms = Some(retry_after_ms);
259 self
260 }
261
262 pub fn to_error_code(&self) -> crate::protocol::ErrorCode {
264 use crate::protocol::ErrorCode;
265 match self.kind {
266 ErrorKind::Retryable => {
267 if let Some(429) = self.status_code {
268 ErrorCode::RateLimitExceeded
269 } else {
270 ErrorCode::ServiceUnavailable
271 }
272 }
273 ErrorKind::Timeout => ErrorCode::NetworkError,
274 ErrorKind::NonRetryable => ErrorCode::InternalError,
275 }
276 }
277}
278
279impl std::fmt::Display for ClassifiedError {
280 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
281 write!(f, "{}", self.message)
282 }
283}
284
285impl std::error::Error for ClassifiedError {}