Skip to main content

xcom_rs/tweets/commands/
types.rs

1//! Shared types for tweets command arguments, results, and errors.
2
3use anyhow::{anyhow, Result};
4use serde::{Deserialize, Serialize};
5use std::str::FromStr;
6
7use crate::tweets::models::{Tweet, TweetFields, TweetMeta};
8
9/// Custom error type for idempotency conflicts
10#[derive(Debug)]
11pub struct IdempotencyConflictError {
12    pub client_request_id: String,
13}
14
15/// Custom error type for partial thread failures.
16/// Contains information about which tweet in the thread failed,
17/// and which tweets were successfully created before the failure.
18#[derive(Debug)]
19pub struct ThreadPartialFailureError {
20    /// Index of the tweet that failed (0-based)
21    pub failed_index: usize,
22    /// IDs of tweets successfully created before the failure
23    pub created_tweet_ids: Vec<String>,
24    /// Underlying error message
25    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/// Policy for handling existing operations with the same client_request_id
55#[derive(Debug, Clone, Copy, PartialEq, Eq)]
56pub enum IfExistsPolicy {
57    /// Return the existing result without error
58    Return,
59    /// Return an error if operation already exists
60    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/// Arguments for creating a tweet
88#[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/// Arguments for listing tweets
96#[derive(Debug, Clone)]
97pub struct ListArgs {
98    pub fields: Vec<TweetFields>,
99    pub limit: Option<usize>,
100    pub cursor: Option<String>,
101}
102
103/// Arguments for engagement operations (like/unlike/retweet/unretweet)
104#[derive(Debug, Clone)]
105pub struct EngagementArgs {
106    pub tweet_id: String,
107}
108
109/// Result of an engagement operation (like/unlike/retweet/unretweet)
110#[derive(Debug, Clone, Serialize, Deserialize)]
111pub struct EngagementResult {
112    pub tweet_id: String,
113    pub success: bool,
114}
115
116/// Arguments for replying to a tweet
117#[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/// Arguments for creating a thread of tweets
126#[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/// Arguments for showing a single tweet
134#[derive(Debug, Clone)]
135pub struct ShowArgs {
136    pub tweet_id: String,
137}
138
139/// Arguments for retrieving a conversation tree
140#[derive(Debug, Clone)]
141pub struct ConversationArgs {
142    pub tweet_id: String,
143}
144
145/// Result of a create operation
146#[derive(Debug, Clone, Serialize, Deserialize)]
147pub struct CreateResult {
148    pub tweet: Tweet,
149    pub meta: TweetMeta,
150}
151
152/// Result of a reply operation
153#[derive(Debug, Clone, Serialize, Deserialize)]
154pub struct ReplyResult {
155    pub tweet: Tweet,
156    pub meta: TweetMeta,
157}
158
159/// Result of a thread operation
160#[derive(Debug, Clone, Serialize, Deserialize)]
161pub struct ThreadResult {
162    pub tweets: Vec<Tweet>,
163    pub meta: ThreadMeta,
164}
165
166/// Metadata for thread results
167#[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/// Result of a show operation
179#[derive(Debug, Clone, Serialize, Deserialize)]
180pub struct ShowResult {
181    pub tweet: Tweet,
182}
183
184/// Pagination metadata
185#[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/// Result of a list operation
195#[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/// Metadata for list results
203#[derive(Debug, Clone, Serialize, Deserialize)]
204pub struct ListResultMeta {
205    pub pagination: PaginationMeta,
206}
207
208/// Error classification for retry logic
209#[derive(Debug, Clone, Copy, PartialEq, Eq)]
210pub enum ErrorKind {
211    /// Retryable errors (429, 5xx)
212    Retryable,
213    /// Non-retryable client errors (4xx except 429)
214    NonRetryable,
215    /// Network/timeout errors
216    Timeout,
217}
218
219/// Classified error with retry information
220#[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    /// Convert to ErrorCode for protocol
263    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 {}