Skip to main content

xcom_rs/bookmarks/
commands.rs

1use anyhow::Result;
2use serde::{Deserialize, Serialize};
3
4use crate::tweets::models::Tweet;
5
6/// Arguments for bookmark add/remove operations
7#[derive(Debug, Clone)]
8pub struct BookmarkArgs {
9    pub tweet_id: String,
10}
11
12/// Arguments for listing bookmarks
13#[derive(Debug, Clone)]
14pub struct BookmarkListArgs {
15    pub limit: Option<usize>,
16    pub cursor: Option<String>,
17}
18
19/// Result of bookmark add/remove operation
20#[derive(Debug, Clone, Serialize, Deserialize)]
21pub struct BookmarkResult {
22    pub tweet_id: String,
23    pub success: bool,
24}
25
26/// Pagination metadata for bookmark list
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct BookmarkPaginationMeta {
29    #[serde(skip_serializing_if = "Option::is_none")]
30    pub next_token: Option<String>,
31}
32
33/// Metadata for bookmark list results
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct BookmarkListMeta {
36    pub pagination: BookmarkPaginationMeta,
37}
38
39/// Result of listing bookmarks
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct BookmarkListResult {
42    pub tweets: Vec<Tweet>,
43    #[serde(skip_serializing_if = "Option::is_none")]
44    pub meta: Option<BookmarkListMeta>,
45}
46
47/// Bookmark command handler
48pub struct BookmarkCommand;
49
50impl BookmarkCommand {
51    /// Create a new bookmark command handler
52    pub fn new() -> Self {
53        Self
54    }
55
56    /// Add a tweet to bookmarks
57    /// In real implementation, calls POST /2/users/{id}/bookmarks
58    pub fn add(&self, args: BookmarkArgs) -> Result<BookmarkResult> {
59        Ok(BookmarkResult {
60            tweet_id: args.tweet_id,
61            success: true,
62        })
63    }
64
65    /// Remove a tweet from bookmarks
66    /// In real implementation, calls DELETE /2/users/{id}/bookmarks/{tweet_id}
67    pub fn remove(&self, args: BookmarkArgs) -> Result<BookmarkResult> {
68        Ok(BookmarkResult {
69            tweet_id: args.tweet_id,
70            success: true,
71        })
72    }
73
74    /// List bookmarked tweets with pagination
75    /// In real implementation, calls GET /2/users/{id}/bookmarks
76    pub fn list(&self, args: BookmarkListArgs) -> Result<BookmarkListResult> {
77        let limit = args.limit.unwrap_or(10);
78
79        // Parse cursor to determine starting offset
80        let offset = if let Some(cursor) = &args.cursor {
81            // Cursor format is "bookmark_cursor_{offset}"
82            cursor
83                .strip_prefix("bookmark_cursor_")
84                .and_then(|s| s.parse::<usize>().ok())
85                .unwrap_or(0)
86        } else {
87            0
88        };
89
90        // Simulate fetching bookmarked tweets
91        let mut tweets = Vec::new();
92        for i in offset..(offset + limit) {
93            let mut tweet = Tweet::new(format!("bookmark_tweet_{}", i));
94            tweet.text = Some(format!("Bookmarked tweet text {}", i));
95            tweet.author_id = Some(format!("user_{}", i));
96            tweet.created_at = Some("2024-01-01T00:00:00Z".to_string());
97            tweets.push(tweet);
98        }
99
100        // Create pagination metadata
101        let next_token = if tweets.len() == limit {
102            Some(format!("bookmark_cursor_{}", offset + limit))
103        } else {
104            None
105        };
106
107        let meta = Some(BookmarkListMeta {
108            pagination: BookmarkPaginationMeta { next_token },
109        });
110
111        Ok(BookmarkListResult { tweets, meta })
112    }
113}
114
115impl Default for BookmarkCommand {
116    fn default() -> Self {
117        Self::new()
118    }
119}
120
121#[cfg(test)]
122mod tests {
123    use super::*;
124
125    #[test]
126    fn test_bookmark_add() {
127        let cmd = BookmarkCommand::new();
128        let args = BookmarkArgs {
129            tweet_id: "tweet123".to_string(),
130        };
131
132        let result = cmd.add(args).unwrap();
133        assert_eq!(result.tweet_id, "tweet123");
134        assert!(result.success);
135    }
136
137    #[test]
138    fn test_bookmark_remove() {
139        let cmd = BookmarkCommand::new();
140        let args = BookmarkArgs {
141            tweet_id: "tweet456".to_string(),
142        };
143
144        let result = cmd.remove(args).unwrap();
145        assert_eq!(result.tweet_id, "tweet456");
146        assert!(result.success);
147    }
148
149    #[test]
150    fn test_bookmark_list_default() {
151        let cmd = BookmarkCommand::new();
152        let args = BookmarkListArgs {
153            limit: None,
154            cursor: None,
155        };
156
157        let result = cmd.list(args).unwrap();
158        assert_eq!(result.tweets.len(), 10);
159        assert!(result.meta.is_some());
160        let meta = result.meta.unwrap();
161        assert!(meta.pagination.next_token.is_some());
162        assert_eq!(
163            meta.pagination.next_token,
164            Some("bookmark_cursor_10".to_string())
165        );
166    }
167
168    #[test]
169    fn test_bookmark_list_with_limit() {
170        let cmd = BookmarkCommand::new();
171        let args = BookmarkListArgs {
172            limit: Some(5),
173            cursor: None,
174        };
175
176        let result = cmd.list(args).unwrap();
177        assert_eq!(result.tweets.len(), 5);
178        assert!(result.meta.is_some());
179        let meta = result.meta.unwrap();
180        assert_eq!(
181            meta.pagination.next_token,
182            Some("bookmark_cursor_5".to_string())
183        );
184    }
185
186    #[test]
187    fn test_bookmark_list_with_cursor() {
188        let cmd = BookmarkCommand::new();
189        let args = BookmarkListArgs {
190            limit: Some(5),
191            cursor: Some("bookmark_cursor_5".to_string()),
192        };
193
194        let result = cmd.list(args).unwrap();
195        assert_eq!(result.tweets.len(), 5);
196        // First tweet should start at offset 5
197        assert_eq!(result.tweets[0].id, "bookmark_tweet_5");
198    }
199}