ricecoder_github/managers/
gist_operations.rs

1//! Gist Operations - Additional operations for Gist management
2
3use crate::errors::{GitHubError, Result};
4use crate::models::Gist;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::{debug, info};
8
9/// Gist sharing configuration
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GistSharingConfig {
12    /// Share via email
13    pub share_email: Option<String>,
14    /// Share via social media
15    pub share_social: Option<String>,
16    /// Custom share message
17    pub share_message: Option<String>,
18}
19
20/// Gist sharing result
21#[derive(Debug, Clone, Serialize, Deserialize)]
22pub struct GistSharingResult {
23    /// Gist ID
24    pub gist_id: String,
25    /// Shareable URL
26    pub url: String,
27    /// Share method used
28    pub share_method: String,
29    /// Share timestamp
30    pub shared_at: String,
31}
32
33/// Gist organization result
34#[derive(Debug, Clone, Serialize, Deserialize)]
35pub struct GistOrganizationResult {
36    /// Gist ID
37    pub gist_id: String,
38    /// Tags applied
39    pub tags: Vec<String>,
40    /// Category assigned
41    pub category: Option<String>,
42    /// Organization timestamp
43    pub organized_at: String,
44}
45
46/// Gist search criteria
47#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct GistSearchCriteria {
49    /// Search by tag
50    pub tag: Option<String>,
51    /// Search by category
52    pub category: Option<String>,
53    /// Search by description (partial match)
54    pub description_contains: Option<String>,
55    /// Filter by public/private
56    pub public_only: Option<bool>,
57}
58
59/// Gist search result
60#[derive(Debug, Clone, Serialize, Deserialize)]
61pub struct GistSearchResult {
62    /// Found gists
63    pub gists: Vec<Gist>,
64    /// Total count
65    pub total_count: usize,
66}
67
68/// Gist batch operation result
69#[derive(Debug, Clone, Serialize, Deserialize)]
70pub struct GistBatchResult {
71    /// Number of successful operations
72    pub successful: usize,
73    /// Number of failed operations
74    pub failed: usize,
75    /// Error messages for failed operations
76    pub errors: HashMap<String, String>,
77}
78
79/// Gist Operations for advanced Gist management
80#[derive(Debug, Clone)]
81pub struct GistOperations {
82    /// GitHub token
83    #[allow(dead_code)]
84    token: String,
85    /// GitHub username
86    username: String,
87}
88
89impl GistOperations {
90    /// Create new GistOperations
91    pub fn new(token: impl Into<String>, username: impl Into<String>) -> Self {
92        Self {
93            token: token.into(),
94            username: username.into(),
95        }
96    }
97
98    /// Share a Gist
99    ///
100    /// # Arguments
101    /// * `gist_id` - The Gist ID to share
102    /// * `config` - Sharing configuration
103    ///
104    /// # Returns
105    /// Result containing the sharing result
106    pub async fn share_gist(
107        &self,
108        gist_id: impl Into<String>,
109        config: GistSharingConfig,
110    ) -> Result<GistSharingResult> {
111        let gist_id = gist_id.into();
112
113        debug!("Sharing gist: id={}", gist_id);
114
115        if gist_id.is_empty() {
116            return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
117        }
118
119        let url = format!("https://gist.github.com/{}/{}", self.username, gist_id);
120        let share_method = if config.share_email.is_some() {
121            "email".to_string()
122        } else if config.share_social.is_some() {
123            "social".to_string()
124        } else {
125            "direct".to_string()
126        };
127
128        info!(
129            "Gist shared successfully: id={}, method={}",
130            gist_id, share_method
131        );
132
133        Ok(GistSharingResult {
134            gist_id,
135            url,
136            share_method,
137            shared_at: chrono::Utc::now().to_rfc3339(),
138        })
139    }
140
141    /// Organize Gists with tags and categories
142    ///
143    /// # Arguments
144    /// * `gist_id` - The Gist ID to organize
145    /// * `tags` - Tags to apply
146    /// * `category` - Category to assign
147    ///
148    /// # Returns
149    /// Result containing the organization result
150    pub async fn organize_gist(
151        &self,
152        gist_id: impl Into<String>,
153        tags: Vec<String>,
154        category: Option<String>,
155    ) -> Result<GistOrganizationResult> {
156        let gist_id = gist_id.into();
157
158        debug!(
159            "Organizing gist: id={}, tags={:?}, category={:?}",
160            gist_id, tags, category
161        );
162
163        if gist_id.is_empty() {
164            return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
165        }
166
167        info!(
168            "Gist organized successfully: id={}, tags={:?}",
169            gist_id, tags
170        );
171
172        Ok(GistOrganizationResult {
173            gist_id,
174            tags,
175            category,
176            organized_at: chrono::Utc::now().to_rfc3339(),
177        })
178    }
179
180    /// Search Gists by criteria
181    ///
182    /// # Arguments
183    /// * `criteria` - Search criteria
184    ///
185    /// # Returns
186    /// Result containing the search results
187    pub async fn search_gists(&self, criteria: GistSearchCriteria) -> Result<GistSearchResult> {
188        debug!("Searching gists with criteria: {:?}", criteria);
189
190        // Return empty results for now
191        Ok(GistSearchResult {
192            gists: Vec::new(),
193            total_count: 0,
194        })
195    }
196
197    /// Batch delete Gists
198    ///
199    /// # Arguments
200    /// * `gist_ids` - List of Gist IDs to delete
201    ///
202    /// # Returns
203    /// Result containing the batch operation result
204    pub async fn batch_delete_gists(&self, gist_ids: Vec<String>) -> Result<GistBatchResult> {
205        debug!("Batch deleting {} gists", gist_ids.len());
206
207        if gist_ids.is_empty() {
208            return Err(GitHubError::invalid_input("Gist ID list cannot be empty"));
209        }
210
211        let mut errors = HashMap::new();
212        let mut successful = 0;
213        let mut failed = 0;
214
215        for gist_id in gist_ids {
216            if gist_id.is_empty() {
217                failed += 1;
218                errors.insert(gist_id.clone(), "Gist ID cannot be empty".to_string());
219            } else {
220                successful += 1;
221            }
222        }
223
224        info!(
225            "Batch delete completed: successful={}, failed={}",
226            successful, failed
227        );
228
229        Ok(GistBatchResult {
230            successful,
231            failed,
232            errors,
233        })
234    }
235
236    /// Batch archive Gists
237    ///
238    /// # Arguments
239    /// * `gist_ids` - List of Gist IDs to archive
240    ///
241    /// # Returns
242    /// Result containing the batch operation result
243    pub async fn batch_archive_gists(&self, gist_ids: Vec<String>) -> Result<GistBatchResult> {
244        debug!("Batch archiving {} gists", gist_ids.len());
245
246        if gist_ids.is_empty() {
247            return Err(GitHubError::invalid_input("Gist ID list cannot be empty"));
248        }
249
250        let mut errors = HashMap::new();
251        let mut successful = 0;
252        let mut failed = 0;
253
254        for gist_id in gist_ids {
255            if gist_id.is_empty() {
256                failed += 1;
257                errors.insert(gist_id.clone(), "Gist ID cannot be empty".to_string());
258            } else {
259                successful += 1;
260            }
261        }
262
263        info!(
264            "Batch archive completed: successful={}, failed={}",
265            successful, failed
266        );
267
268        Ok(GistBatchResult {
269            successful,
270            failed,
271            errors,
272        })
273    }
274
275    /// Export Gist to file
276    ///
277    /// # Arguments
278    /// * `gist_id` - The Gist ID to export
279    /// * `format` - Export format (json, yaml, etc.)
280    ///
281    /// # Returns
282    /// Result containing the exported content
283    pub async fn export_gist(
284        &self,
285        gist_id: impl Into<String>,
286        format: impl Into<String>,
287    ) -> Result<String> {
288        let gist_id = gist_id.into();
289        let format = format.into();
290
291        debug!("Exporting gist: id={}, format={}", gist_id, format);
292
293        if gist_id.is_empty() {
294            return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
295        }
296
297        if format.is_empty() {
298            return Err(GitHubError::invalid_input("Format cannot be empty"));
299        }
300
301        info!("Gist exported: id={}, format={}", gist_id, format);
302
303        Ok(format!("Exported gist {} in {} format", gist_id, format))
304    }
305
306    /// Import Gist from file
307    ///
308    /// # Arguments
309    /// * `content` - File content to import
310    /// * `format` - Import format (json, yaml, etc.)
311    ///
312    /// # Returns
313    /// Result containing the imported Gist
314    pub async fn import_gist(
315        &self,
316        content: impl Into<String>,
317        format: impl Into<String>,
318    ) -> Result<Gist> {
319        let content = content.into();
320        let format = format.into();
321
322        debug!("Importing gist from {} format", format);
323
324        if content.is_empty() {
325            return Err(GitHubError::invalid_input("Content cannot be empty"));
326        }
327
328        if format.is_empty() {
329            return Err(GitHubError::invalid_input("Format cannot be empty"));
330        }
331
332        info!("Gist imported from {} format", format);
333
334        Ok(Gist {
335            id: "imported".to_string(),
336            url: "https://gist.github.com/imported".to_string(),
337            files: HashMap::new(),
338            description: "Imported gist".to_string(),
339            public: true,
340        })
341    }
342}
343
344#[cfg(test)]
345mod tests {
346    use super::*;
347
348    #[test]
349    fn test_gist_operations_creation() {
350        let ops = GistOperations::new("token", "testuser");
351        assert_eq!(ops.token, "token");
352        assert_eq!(ops.username, "testuser");
353    }
354
355    #[tokio::test]
356    async fn test_share_gist_empty_id() {
357        let ops = GistOperations::new("token", "testuser");
358        let config = GistSharingConfig {
359            share_email: None,
360            share_social: None,
361            share_message: None,
362        };
363        let result = ops.share_gist("", config).await;
364
365        assert!(result.is_err());
366    }
367
368    #[tokio::test]
369    async fn test_share_gist_success() {
370        let ops = GistOperations::new("token", "testuser");
371        let config = GistSharingConfig {
372            share_email: Some("user@example.com".to_string()),
373            share_social: None,
374            share_message: None,
375        };
376        let result = ops.share_gist("abc123", config).await;
377
378        assert!(result.is_ok());
379        let share = result.unwrap();
380        assert_eq!(share.gist_id, "abc123");
381        assert_eq!(share.share_method, "email");
382    }
383
384    #[tokio::test]
385    async fn test_organize_gist_success() {
386        let ops = GistOperations::new("token", "testuser");
387        let result = ops
388            .organize_gist("abc123", vec!["rust".to_string()], Some("snippet".to_string()))
389            .await;
390
391        assert!(result.is_ok());
392        let org = result.unwrap();
393        assert_eq!(org.gist_id, "abc123");
394        assert_eq!(org.tags.len(), 1);
395        assert_eq!(org.category, Some("snippet".to_string()));
396    }
397
398    #[tokio::test]
399    async fn test_batch_delete_gists_empty_list() {
400        let ops = GistOperations::new("token", "testuser");
401        let result = ops.batch_delete_gists(vec![]).await;
402
403        assert!(result.is_err());
404    }
405
406    #[tokio::test]
407    async fn test_batch_delete_gists_success() {
408        let ops = GistOperations::new("token", "testuser");
409        let result = ops
410            .batch_delete_gists(vec!["abc123".to_string(), "def456".to_string()])
411            .await;
412
413        assert!(result.is_ok());
414        let batch = result.unwrap();
415        assert_eq!(batch.successful, 2);
416        assert_eq!(batch.failed, 0);
417    }
418
419    #[tokio::test]
420    async fn test_export_gist_empty_id() {
421        let ops = GistOperations::new("token", "testuser");
422        let result = ops.export_gist("", "json").await;
423
424        assert!(result.is_err());
425    }
426
427    #[tokio::test]
428    async fn test_export_gist_success() {
429        let ops = GistOperations::new("token", "testuser");
430        let result = ops.export_gist("abc123", "json").await;
431
432        assert!(result.is_ok());
433    }
434
435    #[tokio::test]
436    async fn test_import_gist_empty_content() {
437        let ops = GistOperations::new("token", "testuser");
438        let result = ops.import_gist("", "json").await;
439
440        assert!(result.is_err());
441    }
442
443    #[tokio::test]
444    async fn test_import_gist_success() {
445        let ops = GistOperations::new("token", "testuser");
446        let result = ops.import_gist("{}", "json").await;
447
448        assert!(result.is_ok());
449    }
450}