Skip to main content

xcom_rs/tweets/
models.rs

1use serde::{Deserialize, Serialize};
2use std::collections::HashMap;
3
4/// Tweet data model
5#[derive(Debug, Clone, Serialize, Deserialize)]
6pub struct Tweet {
7    pub id: String,
8    #[serde(skip_serializing_if = "Option::is_none")]
9    pub text: Option<String>,
10    #[serde(skip_serializing_if = "Option::is_none")]
11    pub author_id: Option<String>,
12    #[serde(skip_serializing_if = "Option::is_none")]
13    pub created_at: Option<String>,
14    #[serde(skip_serializing_if = "Option::is_none")]
15    pub edit_history_tweet_ids: Option<Vec<String>>,
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub conversation_id: Option<String>,
18    #[serde(skip_serializing_if = "Option::is_none")]
19    pub in_reply_to_user_id: Option<String>,
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub referenced_tweets: Option<Vec<ReferencedTweet>>,
22    #[serde(flatten)]
23    pub additional_fields: HashMap<String, serde_json::Value>,
24}
25
26/// Referenced tweet (e.g., reply, quoted)
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct ReferencedTweet {
29    #[serde(rename = "type")]
30    pub ref_type: String,
31    pub id: String,
32}
33
34/// An edge in the conversation tree (parent -> child)
35#[derive(Debug, Clone, Serialize, Deserialize)]
36pub struct ConversationEdge {
37    pub parent_id: String,
38    pub child_id: String,
39}
40
41/// Result of a conversation retrieval
42#[derive(Debug, Clone, Serialize, Deserialize)]
43pub struct ConversationResult {
44    /// The conversation_id that identifies this conversation thread
45    pub conversation_id: String,
46    /// All posts in the conversation (flat list)
47    pub posts: Vec<Tweet>,
48    /// Parent-child edges for tree reconstruction
49    pub edges: Vec<ConversationEdge>,
50}
51
52/// Metadata returned with tweet operations
53#[derive(Debug, Clone, Serialize, Deserialize)]
54#[serde(rename_all = "camelCase")]
55pub struct TweetMeta {
56    pub client_request_id: String,
57    #[serde(skip_serializing_if = "Option::is_none")]
58    pub from_cache: Option<bool>,
59}
60
61/// Fields that can be requested for tweets
62#[derive(Debug, Clone, Copy, PartialEq, Eq)]
63pub enum TweetFields {
64    Id,
65    Text,
66    AuthorId,
67    CreatedAt,
68    EditHistoryTweetIds,
69    ConversationId,
70    InReplyToUserId,
71    ReferencedTweets,
72}
73
74impl TweetFields {
75    pub fn parse(s: &str) -> Option<Self> {
76        match s {
77            "id" => Some(Self::Id),
78            "text" => Some(Self::Text),
79            "author_id" => Some(Self::AuthorId),
80            "created_at" => Some(Self::CreatedAt),
81            "edit_history_tweet_ids" => Some(Self::EditHistoryTweetIds),
82            "conversation_id" => Some(Self::ConversationId),
83            "in_reply_to_user_id" => Some(Self::InReplyToUserId),
84            "referenced_tweets" => Some(Self::ReferencedTweets),
85            _ => None,
86        }
87    }
88
89    pub fn as_str(&self) -> &'static str {
90        match self {
91            Self::Id => "id",
92            Self::Text => "text",
93            Self::AuthorId => "author_id",
94            Self::CreatedAt => "created_at",
95            Self::EditHistoryTweetIds => "edit_history_tweet_ids",
96            Self::ConversationId => "conversation_id",
97            Self::InReplyToUserId => "in_reply_to_user_id",
98            Self::ReferencedTweets => "referenced_tweets",
99        }
100    }
101
102    /// Default fields for list operations (minimal set)
103    pub fn default_fields() -> Vec<Self> {
104        vec![Self::Id, Self::Text]
105    }
106}
107
108impl Tweet {
109    /// Create a new tweet with minimal fields
110    pub fn new(id: String) -> Self {
111        Self {
112            id,
113            text: None,
114            author_id: None,
115            created_at: None,
116            edit_history_tweet_ids: None,
117            conversation_id: None,
118            in_reply_to_user_id: None,
119            referenced_tweets: None,
120            additional_fields: HashMap::new(),
121        }
122    }
123
124    /// Project tweet to only include requested fields
125    pub fn project(&self, fields: &[TweetFields]) -> Self {
126        let mut tweet = Tweet::new(String::new());
127
128        for field in fields {
129            match field {
130                TweetFields::Id => tweet.id = self.id.clone(),
131                TweetFields::Text => tweet.text = self.text.clone(),
132                TweetFields::AuthorId => tweet.author_id = self.author_id.clone(),
133                TweetFields::CreatedAt => tweet.created_at = self.created_at.clone(),
134                TweetFields::EditHistoryTweetIds => {
135                    tweet.edit_history_tweet_ids = self.edit_history_tweet_ids.clone()
136                }
137                TweetFields::ConversationId => tweet.conversation_id = self.conversation_id.clone(),
138                TweetFields::InReplyToUserId => {
139                    tweet.in_reply_to_user_id = self.in_reply_to_user_id.clone()
140                }
141                TweetFields::ReferencedTweets => {
142                    tweet.referenced_tweets = self.referenced_tweets.clone()
143                }
144            }
145        }
146
147        tweet
148    }
149}
150
151#[cfg(test)]
152mod tests {
153    use super::*;
154
155    #[test]
156    fn test_tweet_projection() {
157        let mut tweet = Tweet::new("123".to_string());
158        tweet.text = Some("Hello world".to_string());
159        tweet.author_id = Some("user_123".to_string());
160
161        let projected = tweet.project(&[TweetFields::Id, TweetFields::Text]);
162        assert_eq!(projected.id, "123");
163        assert_eq!(projected.text, Some("Hello world".to_string()));
164        assert_eq!(projected.author_id, None);
165    }
166
167    #[test]
168    fn test_default_fields() {
169        let fields = TweetFields::default_fields();
170        assert_eq!(fields.len(), 2);
171        assert!(fields.contains(&TweetFields::Id));
172        assert!(fields.contains(&TweetFields::Text));
173    }
174
175    #[test]
176    fn test_field_parse() {
177        assert_eq!(TweetFields::parse("id"), Some(TweetFields::Id));
178        assert_eq!(TweetFields::parse("text"), Some(TweetFields::Text));
179        assert_eq!(TweetFields::parse("invalid"), None);
180    }
181}