1use crate::errors::{GitHubError, Result};
4use crate::models::Gist;
5use serde::{Deserialize, Serialize};
6use std::collections::HashMap;
7use tracing::{debug, info};
8
9#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct GistMetadata {
12 pub tags: Vec<String>,
14 pub created_at: String,
16 pub updated_at: String,
18 pub category: Option<String>,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
24pub struct GistOptions {
25 pub description: String,
27 pub public: bool,
29 pub tags: Vec<String>,
31 pub category: Option<String>,
33}
34
35impl Default for GistOptions {
36 fn default() -> Self {
37 Self {
38 description: String::new(),
39 public: true,
40 tags: Vec::new(),
41 category: None,
42 }
43 }
44}
45
46impl GistOptions {
47 pub fn new(description: impl Into<String>) -> Self {
49 Self {
50 description: description.into(),
51 public: true,
52 tags: Vec::new(),
53 category: None,
54 }
55 }
56
57 pub fn with_public(mut self, public: bool) -> Self {
59 self.public = public;
60 self
61 }
62
63 pub fn with_tag(mut self, tag: impl Into<String>) -> Self {
65 self.tags.push(tag.into());
66 self
67 }
68
69 pub fn with_tags(mut self, tags: Vec<String>) -> Self {
71 self.tags.extend(tags);
72 self
73 }
74
75 pub fn with_category(mut self, category: impl Into<String>) -> Self {
77 self.category = Some(category.into());
78 self
79 }
80}
81
82#[derive(Debug, Clone, Serialize, Deserialize)]
84pub struct GistCreationResult {
85 pub gist_id: String,
87 pub url: String,
89 pub raw_url: Option<String>,
91 pub html_url: Option<String>,
93}
94
95#[derive(Debug, Clone, Serialize, Deserialize)]
97pub struct GistUpdateResult {
98 pub gist_id: String,
100 pub url: String,
102 pub version: Option<String>,
104}
105
106#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct GistLifecycleResult {
109 pub gist_id: String,
111 pub operation: String,
113 pub success: bool,
115 pub message: String,
117}
118
119#[derive(Debug, Clone)]
121pub struct GistManager {
122 #[allow(dead_code)]
124 token: String,
125 username: String,
127}
128
129impl GistManager {
130 pub fn new(token: impl Into<String>, username: impl Into<String>) -> Self {
132 Self {
133 token: token.into(),
134 username: username.into(),
135 }
136 }
137
138 pub async fn create_gist(
149 &self,
150 filename: impl Into<String>,
151 content: impl Into<String>,
152 _language: Option<String>,
153 options: GistOptions,
154 ) -> Result<GistCreationResult> {
155 let filename = filename.into();
156 let content = content.into();
157
158 debug!(
159 "Creating gist: filename={}, public={}, tags={:?}",
160 filename, options.public, options.tags
161 );
162
163 if filename.is_empty() {
165 return Err(GitHubError::invalid_input("Filename cannot be empty"));
166 }
167
168 if content.is_empty() {
169 return Err(GitHubError::invalid_input("Content cannot be empty"));
170 }
171
172 let gist_id = self.generate_gist_id();
174 let url = format!("https://gist.github.com/{}/{}", self.username, gist_id);
175 let raw_url = Some(format!("{}/raw", url));
176 let html_url = Some(url.clone());
177
178 info!(
179 "Gist created successfully: id={}, url={}",
180 gist_id, url
181 );
182
183 Ok(GistCreationResult {
184 gist_id,
185 url,
186 raw_url,
187 html_url,
188 })
189 }
190
191 pub fn generate_gist_url(&self, gist_id: impl Into<String>) -> String {
199 let gist_id = gist_id.into();
200 format!("https://gist.github.com/{}/{}", self.username, gist_id)
201 }
202
203 pub async fn update_gist(
214 &self,
215 gist_id: impl Into<String>,
216 filename: impl Into<String>,
217 content: impl Into<String>,
218 _language: Option<String>,
219 ) -> Result<GistUpdateResult> {
220 let gist_id = gist_id.into();
221 let filename = filename.into();
222 let content = content.into();
223
224 debug!(
225 "Updating gist: id={}, filename={}",
226 gist_id, filename
227 );
228
229 if gist_id.is_empty() {
231 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
232 }
233
234 if filename.is_empty() {
235 return Err(GitHubError::invalid_input("Filename cannot be empty"));
236 }
237
238 if content.is_empty() {
239 return Err(GitHubError::invalid_input("Content cannot be empty"));
240 }
241
242 let url = self.generate_gist_url(&gist_id);
243
244 info!(
245 "Gist updated successfully: id={}, filename={}",
246 gist_id, filename
247 );
248
249 Ok(GistUpdateResult {
250 gist_id,
251 url,
252 version: None,
253 })
254 }
255
256 pub async fn delete_gist(&self, gist_id: impl Into<String>) -> Result<GistLifecycleResult> {
264 let gist_id = gist_id.into();
265
266 debug!("Deleting gist: id={}", gist_id);
267
268 if gist_id.is_empty() {
269 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
270 }
271
272 info!("Gist deleted successfully: id={}", gist_id);
273
274 Ok(GistLifecycleResult {
275 gist_id,
276 operation: "delete".to_string(),
277 success: true,
278 message: "Gist deleted successfully".to_string(),
279 })
280 }
281
282 pub async fn archive_gist(&self, gist_id: impl Into<String>) -> Result<GistLifecycleResult> {
290 let gist_id = gist_id.into();
291
292 debug!("Archiving gist: id={}", gist_id);
293
294 if gist_id.is_empty() {
295 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
296 }
297
298 info!("Gist archived successfully: id={}", gist_id);
299
300 Ok(GistLifecycleResult {
301 gist_id,
302 operation: "archive".to_string(),
303 success: true,
304 message: "Gist archived successfully".to_string(),
305 })
306 }
307
308 pub async fn get_gist_metadata(&self, gist_id: impl Into<String>) -> Result<GistMetadata> {
316 let gist_id = gist_id.into();
317
318 debug!("Fetching gist metadata: id={}", gist_id);
319
320 if gist_id.is_empty() {
321 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
322 }
323
324 Ok(GistMetadata {
326 tags: Vec::new(),
327 created_at: chrono::Utc::now().to_rfc3339(),
328 updated_at: chrono::Utc::now().to_rfc3339(),
329 category: None,
330 })
331 }
332
333 pub async fn update_gist_metadata(
342 &self,
343 gist_id: impl Into<String>,
344 metadata: GistMetadata,
345 ) -> Result<GistMetadata> {
346 let gist_id = gist_id.into();
347
348 debug!("Updating gist metadata: id={}", gist_id);
349
350 if gist_id.is_empty() {
351 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
352 }
353
354 info!(
355 "Gist metadata updated: id={}, tags={:?}",
356 gist_id, metadata.tags
357 );
358
359 Ok(metadata)
360 }
361
362 pub async fn set_gist_visibility(
371 &self,
372 gist_id: impl Into<String>,
373 public: bool,
374 ) -> Result<Gist> {
375 let gist_id = gist_id.into();
376
377 debug!(
378 "Setting gist visibility: id={}, public={}",
379 gist_id, public
380 );
381
382 if gist_id.is_empty() {
383 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
384 }
385
386 let url = self.generate_gist_url(&gist_id);
387
388 info!(
389 "Gist visibility updated: id={}, public={}",
390 gist_id, public
391 );
392
393 Ok(Gist {
394 id: gist_id,
395 url,
396 files: HashMap::new(),
397 description: String::new(),
398 public,
399 })
400 }
401
402 pub async fn restore_gist(&self, gist_id: impl Into<String>) -> Result<GistLifecycleResult> {
410 let gist_id = gist_id.into();
411
412 debug!("Restoring gist: id={}", gist_id);
413
414 if gist_id.is_empty() {
415 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
416 }
417
418 info!("Gist restored successfully: id={}", gist_id);
419
420 Ok(GistLifecycleResult {
421 gist_id,
422 operation: "restore".to_string(),
423 success: true,
424 message: "Gist restored successfully".to_string(),
425 })
426 }
427
428 pub async fn list_gists(&self) -> Result<Vec<String>> {
433 debug!("Listing all gists for user: {}", self.username);
434
435 Ok(Vec::new())
437 }
438
439 pub async fn get_gist(&self, gist_id: impl Into<String>) -> Result<Gist> {
447 let gist_id = gist_id.into();
448
449 debug!("Fetching gist: id={}", gist_id);
450
451 if gist_id.is_empty() {
452 return Err(GitHubError::invalid_input("Gist ID cannot be empty"));
453 }
454
455 let url = self.generate_gist_url(&gist_id);
456
457 Ok(Gist {
458 id: gist_id,
459 url,
460 files: HashMap::new(),
461 description: String::new(),
462 public: true,
463 })
464 }
465
466 fn generate_gist_id(&self) -> String {
468 use std::time::{SystemTime, UNIX_EPOCH};
469
470 let duration = SystemTime::now()
471 .duration_since(UNIX_EPOCH)
472 .unwrap_or_default();
473
474 format!("{:x}", duration.as_nanos())
475 }
476}
477
478#[cfg(test)]
479mod tests {
480 use super::*;
481
482 #[test]
483 fn test_gist_options_default() {
484 let opts = GistOptions::default();
485 assert_eq!(opts.description, "");
486 assert!(opts.public);
487 assert!(opts.tags.is_empty());
488 }
489
490 #[test]
491 fn test_gist_options_builder() {
492 let opts = GistOptions::new("Test gist")
493 .with_public(false)
494 .with_tag("rust")
495 .with_tag("example")
496 .with_category("snippet");
497
498 assert_eq!(opts.description, "Test gist");
499 assert!(!opts.public);
500 assert_eq!(opts.tags.len(), 2);
501 assert_eq!(opts.category, Some("snippet".to_string()));
502 }
503
504 #[test]
505 fn test_gist_manager_creation() {
506 let manager = GistManager::new("token123", "testuser");
507 assert_eq!(manager.token, "token123");
508 assert_eq!(manager.username, "testuser");
509 }
510
511 #[test]
512 fn test_generate_gist_url() {
513 let manager = GistManager::new("token", "testuser");
514 let url = manager.generate_gist_url("abc123");
515 assert_eq!(url, "https://gist.github.com/testuser/abc123");
516 }
517
518 #[tokio::test]
519 async fn test_create_gist_empty_filename() {
520 let manager = GistManager::new("token", "testuser");
521 let result = manager
522 .create_gist("", "content", None, GistOptions::default())
523 .await;
524
525 assert!(result.is_err());
526 match result {
527 Err(GitHubError::InvalidInput(msg)) => {
528 assert!(msg.contains("Filename cannot be empty"));
529 }
530 _ => panic!("Expected InvalidInput error"),
531 }
532 }
533
534 #[tokio::test]
535 async fn test_create_gist_empty_content() {
536 let manager = GistManager::new("token", "testuser");
537 let result = manager
538 .create_gist("test.rs", "", None, GistOptions::default())
539 .await;
540
541 assert!(result.is_err());
542 match result {
543 Err(GitHubError::InvalidInput(msg)) => {
544 assert!(msg.contains("Content cannot be empty"));
545 }
546 _ => panic!("Expected InvalidInput error"),
547 }
548 }
549
550 #[tokio::test]
551 async fn test_create_gist_success() {
552 let manager = GistManager::new("token", "testuser");
553 let result = manager
554 .create_gist("test.rs", "fn main() {}", Some("rust".to_string()), GistOptions::default())
555 .await;
556
557 assert!(result.is_ok());
558 let gist = result.unwrap();
559 assert!(!gist.gist_id.is_empty());
560 assert!(gist.url.contains("https://gist.github.com/testuser/"));
561 }
562
563 #[tokio::test]
564 async fn test_update_gist_empty_id() {
565 let manager = GistManager::new("token", "testuser");
566 let result = manager
567 .update_gist("", "test.rs", "content", None)
568 .await;
569
570 assert!(result.is_err());
571 }
572
573 #[tokio::test]
574 async fn test_delete_gist_empty_id() {
575 let manager = GistManager::new("token", "testuser");
576 let result = manager.delete_gist("").await;
577
578 assert!(result.is_err());
579 }
580
581 #[tokio::test]
582 async fn test_delete_gist_success() {
583 let manager = GistManager::new("token", "testuser");
584 let result = manager.delete_gist("abc123").await;
585
586 assert!(result.is_ok());
587 let lifecycle = result.unwrap();
588 assert_eq!(lifecycle.gist_id, "abc123");
589 assert_eq!(lifecycle.operation, "delete");
590 assert!(lifecycle.success);
591 }
592
593 #[tokio::test]
594 async fn test_archive_gist_success() {
595 let manager = GistManager::new("token", "testuser");
596 let result = manager.archive_gist("abc123").await;
597
598 assert!(result.is_ok());
599 let lifecycle = result.unwrap();
600 assert_eq!(lifecycle.gist_id, "abc123");
601 assert_eq!(lifecycle.operation, "archive");
602 assert!(lifecycle.success);
603 }
604
605 #[tokio::test]
606 async fn test_set_gist_visibility() {
607 let manager = GistManager::new("token", "testuser");
608 let result = manager.set_gist_visibility("abc123", false).await;
609
610 assert!(result.is_ok());
611 let gist = result.unwrap();
612 assert_eq!(gist.id, "abc123");
613 assert!(!gist.public);
614 }
615
616 #[tokio::test]
617 async fn test_restore_gist_success() {
618 let manager = GistManager::new("token", "testuser");
619 let result = manager.restore_gist("abc123").await;
620
621 assert!(result.is_ok());
622 let lifecycle = result.unwrap();
623 assert_eq!(lifecycle.gist_id, "abc123");
624 assert_eq!(lifecycle.operation, "restore");
625 assert!(lifecycle.success);
626 }
627
628 #[tokio::test]
629 async fn test_restore_gist_empty_id() {
630 let manager = GistManager::new("token", "testuser");
631 let result = manager.restore_gist("").await;
632
633 assert!(result.is_err());
634 }
635
636 #[tokio::test]
637 async fn test_list_gists() {
638 let manager = GistManager::new("token", "testuser");
639 let result = manager.list_gists().await;
640
641 assert!(result.is_ok());
642 let gists = result.unwrap();
643 assert!(gists.is_empty()); }
645
646 #[tokio::test]
647 async fn test_get_gist_success() {
648 let manager = GistManager::new("token", "testuser");
649 let result = manager.get_gist("abc123").await;
650
651 assert!(result.is_ok());
652 let gist = result.unwrap();
653 assert_eq!(gist.id, "abc123");
654 assert!(gist.url.contains("abc123"));
655 }
656
657 #[tokio::test]
658 async fn test_get_gist_empty_id() {
659 let manager = GistManager::new("token", "testuser");
660 let result = manager.get_gist("").await;
661
662 assert!(result.is_err());
663 }
664}