Skip to main content

github_bot_sdk/client/
issue.rs

1// GENERATED FROM: docs/spec/interfaces/issue-operations.md
2// Issue, label, and comment operations for GitHub API
3
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7use crate::client::InstallationClient;
8use crate::error::ApiError;
9
10/// GitHub issue.
11///
12/// Represents a GitHub issue with all its metadata.
13///
14/// See docs/spec/interfaces/issue-operations.md
15#[derive(Debug, Clone, Serialize, Deserialize)]
16pub struct Issue {
17    /// Unique issue identifier
18    pub id: u64,
19
20    /// Node ID for GraphQL API
21    pub node_id: String,
22
23    /// Issue number (repository-specific)
24    pub number: u64,
25
26    /// Issue title
27    pub title: String,
28
29    /// Issue body content (Markdown)
30    pub body: Option<String>,
31
32    /// Issue state
33    pub state: String, // "open" or "closed"
34
35    /// User who created the issue
36    pub user: IssueUser,
37
38    /// Assigned users
39    pub assignees: Vec<IssueUser>,
40
41    /// Applied labels
42    pub labels: Vec<Label>,
43
44    /// Milestone
45    pub milestone: Option<Milestone>,
46
47    /// Number of comments
48    pub comments: u64,
49
50    /// Creation timestamp
51    pub created_at: DateTime<Utc>,
52
53    /// Last update timestamp
54    pub updated_at: DateTime<Utc>,
55
56    /// Close timestamp
57    pub closed_at: Option<DateTime<Utc>>,
58
59    /// Issue URL
60    pub html_url: String,
61}
62
63/// User associated with an issue.
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct IssueUser {
66    /// User login name
67    pub login: String,
68
69    /// User ID
70    pub id: u64,
71
72    /// User node ID
73    pub node_id: String,
74
75    /// User type
76    #[serde(rename = "type")]
77    pub user_type: String,
78}
79
80/// Milestone associated with an issue or pull request.
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct Milestone {
83    /// Unique milestone identifier
84    pub id: u64,
85
86    /// Node ID for GraphQL API
87    pub node_id: String,
88
89    /// Milestone number (repository-specific)
90    pub number: u64,
91
92    /// Milestone title
93    pub title: String,
94
95    /// Milestone description
96    pub description: Option<String>,
97
98    /// Milestone state
99    pub state: String, // "open" or "closed"
100
101    /// Number of open issues
102    pub open_issues: u64,
103
104    /// Number of closed issues
105    pub closed_issues: u64,
106
107    /// Due date
108    pub due_on: Option<DateTime<Utc>>,
109
110    /// Creation timestamp
111    pub created_at: DateTime<Utc>,
112
113    /// Last update timestamp
114    pub updated_at: DateTime<Utc>,
115
116    /// Close timestamp
117    pub closed_at: Option<DateTime<Utc>>,
118}
119
120/// GitHub label.
121///
122/// Labels are used to categorize issues and pull requests.
123#[derive(Debug, Clone, Serialize, Deserialize)]
124pub struct Label {
125    /// Unique label identifier
126    pub id: u64,
127
128    /// Node ID for GraphQL API
129    pub node_id: String,
130
131    /// Label name
132    pub name: String,
133
134    /// Label description
135    pub description: Option<String>,
136
137    /// Label color (6-digit hex code without #)
138    pub color: String,
139
140    /// Whether this is a default label
141    pub default: bool,
142}
143
144/// Comment on an issue.
145#[derive(Debug, Clone, Serialize, Deserialize)]
146pub struct Comment {
147    /// Unique comment identifier
148    pub id: u64,
149
150    /// Node ID for GraphQL API
151    pub node_id: String,
152
153    /// Comment body content (Markdown)
154    pub body: String,
155
156    /// User who created the comment
157    pub user: IssueUser,
158
159    /// Creation timestamp
160    pub created_at: DateTime<Utc>,
161
162    /// Last update timestamp
163    pub updated_at: DateTime<Utc>,
164
165    /// Comment URL
166    pub html_url: String,
167}
168
169/// Request to create a new issue.
170#[derive(Debug, Clone, Serialize)]
171pub struct CreateIssueRequest {
172    /// Issue title (required)
173    pub title: String,
174
175    /// Issue body content (Markdown)
176    #[serde(skip_serializing_if = "Option::is_none")]
177    pub body: Option<String>,
178
179    /// Usernames to assign
180    #[serde(skip_serializing_if = "Option::is_none")]
181    pub assignees: Option<Vec<String>>,
182
183    /// Milestone number
184    #[serde(skip_serializing_if = "Option::is_none")]
185    pub milestone: Option<u64>,
186
187    /// Label names to apply
188    #[serde(skip_serializing_if = "Option::is_none")]
189    pub labels: Option<Vec<String>>,
190}
191
192/// Request to update an existing issue.
193#[derive(Debug, Clone, Serialize, Default)]
194pub struct UpdateIssueRequest {
195    /// Issue title
196    #[serde(skip_serializing_if = "Option::is_none")]
197    pub title: Option<String>,
198
199    /// Issue body content (Markdown)
200    #[serde(skip_serializing_if = "Option::is_none")]
201    pub body: Option<String>,
202
203    /// Issue state
204    #[serde(skip_serializing_if = "Option::is_none")]
205    pub state: Option<String>, // "open" or "closed"
206
207    /// Usernames to assign (replaces existing assignees)
208    #[serde(skip_serializing_if = "Option::is_none")]
209    pub assignees: Option<Vec<String>>,
210
211    /// Milestone number (None to clear milestone)
212    #[serde(skip_serializing_if = "Option::is_none")]
213    pub milestone: Option<u64>,
214
215    /// Label names (replaces existing labels)
216    #[serde(skip_serializing_if = "Option::is_none")]
217    pub labels: Option<Vec<String>>,
218}
219
220/// Request to create a label.
221#[derive(Debug, Clone, Serialize)]
222pub struct CreateLabelRequest {
223    /// Label name (required)
224    pub name: String,
225
226    /// Label color (6-digit hex code without #)
227    pub color: String,
228
229    /// Label description
230    #[serde(skip_serializing_if = "Option::is_none")]
231    pub description: Option<String>,
232}
233
234/// Request to update a label.
235#[derive(Debug, Clone, Serialize, Default)]
236pub struct UpdateLabelRequest {
237    /// New label name
238    #[serde(skip_serializing_if = "Option::is_none")]
239    pub new_name: Option<String>,
240
241    /// Label color (6-digit hex code without #)
242    #[serde(skip_serializing_if = "Option::is_none")]
243    pub color: Option<String>,
244
245    /// Label description
246    #[serde(skip_serializing_if = "Option::is_none")]
247    pub description: Option<String>,
248}
249
250/// Request to create a comment.
251#[derive(Debug, Clone, Serialize)]
252pub struct CreateCommentRequest {
253    /// Comment body content (Markdown, required)
254    pub body: String,
255}
256
257/// Request to update a comment.
258#[derive(Debug, Clone, Serialize)]
259pub struct UpdateCommentRequest {
260    /// Comment body content (Markdown, required)
261    pub body: String,
262}
263
264/// Request to set milestone on an issue.
265#[derive(Debug, Clone, Serialize)]
266pub struct SetIssueMilestoneRequest {
267    /// Milestone number (None to clear milestone)
268    pub milestone: Option<u64>,
269}
270
271impl InstallationClient {
272    // ========================================================================
273    // Issue Operations
274    // ========================================================================
275
276    /// List issues in a repository.
277    ///
278    /// Returns a paginated response with issues and pagination metadata.
279    /// Use the pagination information to fetch subsequent pages if needed.
280    ///
281    /// # Arguments
282    ///
283    /// * `owner` - Repository owner
284    /// * `repo` - Repository name
285    /// * `state` - Filter by state (`"open"`, `"closed"`, or `"all"`)
286    /// * `page` - Page number (1-indexed, omit for first page)
287    ///
288    /// # Examples
289    ///
290    /// ```no_run
291    /// # use github_bot_sdk::client::InstallationClient;
292    /// # async fn example(client: &InstallationClient) -> Result<(), Box<dyn std::error::Error>> {
293    /// // Get first page
294    /// let response = client.list_issues("owner", "repo", None, None).await?;
295    /// println!("Got {} issues", response.items.len());
296    ///
297    /// // Check if more pages exist
298    /// if response.has_next() {
299    ///     if let Some(next_page) = response.next_page_number() {
300    ///         let next_response = client.list_issues("owner", "repo", None, Some(next_page)).await?;
301    ///         println!("Got {} more issues", next_response.items.len());
302    ///     }
303    /// }
304    /// # Ok(())
305    /// # }
306    /// ```
307    pub async fn list_issues(
308        &self,
309        owner: &str,
310        repo: &str,
311        state: Option<&str>,
312        page: Option<u32>,
313    ) -> Result<crate::client::PagedResponse<Issue>, ApiError> {
314        let mut path = format!("/repos/{}/{}/issues", owner, repo);
315        let mut query_params = Vec::new();
316
317        if let Some(state_value) = state {
318            query_params.push(format!("state={}", state_value));
319        }
320        if let Some(page_num) = page {
321            query_params.push(format!("page={}", page_num));
322        }
323
324        if !query_params.is_empty() {
325            path = format!("{}?{}", path, query_params.join("&"));
326        }
327
328        let response = self.get(&path).await?;
329        let status = response.status();
330
331        if !status.is_success() {
332            return Err(match status.as_u16() {
333                404 => ApiError::NotFound,
334                403 => ApiError::AuthorizationFailed,
335                401 => ApiError::AuthenticationFailed,
336                _ => {
337                    let message = response
338                        .text()
339                        .await
340                        .unwrap_or_else(|_| "Unknown error".to_string());
341                    ApiError::HttpError {
342                        status: status.as_u16(),
343                        message,
344                    }
345                }
346            });
347        }
348
349        // Parse Link header for pagination
350        let pagination = response
351            .headers()
352            .get("Link")
353            .and_then(|h| h.to_str().ok())
354            .map(|h| crate::client::parse_link_header(Some(h)))
355            .unwrap_or_default();
356
357        // Parse response body
358        let items: Vec<Issue> = response.json().await.map_err(ApiError::from)?;
359
360        Ok(crate::client::PagedResponse {
361            items,
362            total_count: None, // GitHub doesn't provide total count in list responses
363            pagination,
364        })
365    }
366
367    /// Get a specific issue by number.
368    pub async fn get_issue(
369        &self,
370        owner: &str,
371        repo: &str,
372        issue_number: u64,
373    ) -> Result<Issue, ApiError> {
374        let path = format!("/repos/{}/{}/issues/{}", owner, repo, issue_number);
375        let response = self.get(&path).await?;
376
377        let status = response.status();
378        if !status.is_success() {
379            return Err(match status.as_u16() {
380                404 => ApiError::NotFound,
381                403 => ApiError::AuthorizationFailed,
382                401 => ApiError::AuthenticationFailed,
383                _ => {
384                    let message = response
385                        .text()
386                        .await
387                        .unwrap_or_else(|_| "Unknown error".to_string());
388                    ApiError::HttpError {
389                        status: status.as_u16(),
390                        message,
391                    }
392                }
393            });
394        }
395        response.json().await.map_err(ApiError::from)
396    }
397
398    /// Create a new issue.
399    pub async fn create_issue(
400        &self,
401        owner: &str,
402        repo: &str,
403        request: CreateIssueRequest,
404    ) -> Result<Issue, ApiError> {
405        let path = format!("/repos/{}/{}/issues", owner, repo);
406        let response = self.post(&path, &request).await?;
407
408        let status = response.status();
409        if !status.is_success() {
410            return Err(match status.as_u16() {
411                422 => {
412                    let message = response
413                        .text()
414                        .await
415                        .unwrap_or_else(|_| "Validation failed".to_string());
416                    ApiError::InvalidRequest { message }
417                }
418                404 => ApiError::NotFound,
419                403 => ApiError::AuthorizationFailed,
420                401 => ApiError::AuthenticationFailed,
421                _ => {
422                    let message = response
423                        .text()
424                        .await
425                        .unwrap_or_else(|_| "Unknown error".to_string());
426                    ApiError::HttpError {
427                        status: status.as_u16(),
428                        message,
429                    }
430                }
431            });
432        }
433        response.json().await.map_err(ApiError::from)
434    }
435
436    /// Update an existing issue.
437    pub async fn update_issue(
438        &self,
439        owner: &str,
440        repo: &str,
441        issue_number: u64,
442        request: UpdateIssueRequest,
443    ) -> Result<Issue, ApiError> {
444        let path = format!("/repos/{}/{}/issues/{}", owner, repo, issue_number);
445        let response = self.patch(&path, &request).await?;
446
447        let status = response.status();
448        if !status.is_success() {
449            return Err(match status.as_u16() {
450                422 => {
451                    let message = response
452                        .text()
453                        .await
454                        .unwrap_or_else(|_| "Validation failed".to_string());
455                    ApiError::InvalidRequest { message }
456                }
457                404 => ApiError::NotFound,
458                403 => ApiError::AuthorizationFailed,
459                401 => ApiError::AuthenticationFailed,
460                _ => {
461                    let message = response
462                        .text()
463                        .await
464                        .unwrap_or_else(|_| "Unknown error".to_string());
465                    ApiError::HttpError {
466                        status: status.as_u16(),
467                        message,
468                    }
469                }
470            });
471        }
472        response.json().await.map_err(ApiError::from)
473    }
474
475    /// Set the milestone on an issue.
476    pub async fn set_issue_milestone(
477        &self,
478        owner: &str,
479        repo: &str,
480        issue_number: u64,
481        milestone_number: Option<u64>,
482    ) -> Result<Issue, ApiError> {
483        let request = UpdateIssueRequest {
484            milestone: milestone_number,
485            ..Default::default()
486        };
487        self.update_issue(owner, repo, issue_number, request).await
488    }
489
490    // ========================================================================
491    // Label Operations
492    // ========================================================================
493
494    /// List all labels in a repository.
495    ///
496    /// See docs/spec/interfaces/issue-operations.md
497    pub async fn list_labels(&self, owner: &str, repo: &str) -> Result<Vec<Label>, ApiError> {
498        let path = format!("/repos/{}/{}/labels", owner, repo);
499        let response = self.get(&path).await?;
500
501        let status = response.status();
502        if !status.is_success() {
503            return Err(match status.as_u16() {
504                404 => ApiError::NotFound,
505                403 => ApiError::AuthorizationFailed,
506                401 => ApiError::AuthenticationFailed,
507                _ => {
508                    let message = response
509                        .text()
510                        .await
511                        .unwrap_or_else(|_| "Unknown error".to_string());
512                    ApiError::HttpError {
513                        status: status.as_u16(),
514                        message,
515                    }
516                }
517            });
518        }
519        response.json().await.map_err(ApiError::from)
520    }
521
522    /// Get a specific label by name.
523    ///
524    /// See docs/spec/interfaces/issue-operations.md
525    pub async fn get_label(&self, owner: &str, repo: &str, name: &str) -> Result<Label, ApiError> {
526        let path = format!("/repos/{}/{}/labels/{}", owner, repo, name);
527        let response = self.get(&path).await?;
528
529        let status = response.status();
530        if !status.is_success() {
531            return Err(match status.as_u16() {
532                404 => ApiError::NotFound,
533                403 => ApiError::AuthorizationFailed,
534                401 => ApiError::AuthenticationFailed,
535                _ => {
536                    let message = response
537                        .text()
538                        .await
539                        .unwrap_or_else(|_| "Unknown error".to_string());
540                    ApiError::HttpError {
541                        status: status.as_u16(),
542                        message,
543                    }
544                }
545            });
546        }
547        response.json().await.map_err(ApiError::from)
548    }
549
550    /// Create a new label.
551    ///
552    /// See docs/spec/interfaces/issue-operations.md
553    pub async fn create_label(
554        &self,
555        owner: &str,
556        repo: &str,
557        request: CreateLabelRequest,
558    ) -> Result<Label, ApiError> {
559        let path = format!("/repos/{}/{}/labels", owner, repo);
560        let response = self.post(&path, &request).await?;
561
562        let status = response.status();
563        if !status.is_success() {
564            return Err(match status.as_u16() {
565                422 => {
566                    let message = response
567                        .text()
568                        .await
569                        .unwrap_or_else(|_| "Validation failed".to_string());
570                    ApiError::InvalidRequest { message }
571                }
572                404 => ApiError::NotFound,
573                403 => ApiError::AuthorizationFailed,
574                401 => ApiError::AuthenticationFailed,
575                _ => {
576                    let message = response
577                        .text()
578                        .await
579                        .unwrap_or_else(|_| "Unknown error".to_string());
580                    ApiError::HttpError {
581                        status: status.as_u16(),
582                        message,
583                    }
584                }
585            });
586        }
587        response.json().await.map_err(ApiError::from)
588    }
589
590    /// Update an existing label.
591    ///
592    /// See docs/spec/interfaces/issue-operations.md
593    pub async fn update_label(
594        &self,
595        owner: &str,
596        repo: &str,
597        name: &str,
598        request: UpdateLabelRequest,
599    ) -> Result<Label, ApiError> {
600        let path = format!("/repos/{}/{}/labels/{}", owner, repo, name);
601        let response = self.patch(&path, &request).await?;
602
603        let status = response.status();
604        if !status.is_success() {
605            return Err(match status.as_u16() {
606                422 => {
607                    let message = response
608                        .text()
609                        .await
610                        .unwrap_or_else(|_| "Validation failed".to_string());
611                    ApiError::InvalidRequest { message }
612                }
613                404 => ApiError::NotFound,
614                403 => ApiError::AuthorizationFailed,
615                401 => ApiError::AuthenticationFailed,
616                _ => {
617                    let message = response
618                        .text()
619                        .await
620                        .unwrap_or_else(|_| "Unknown error".to_string());
621                    ApiError::HttpError {
622                        status: status.as_u16(),
623                        message,
624                    }
625                }
626            });
627        }
628        response.json().await.map_err(ApiError::from)
629    }
630
631    /// Delete a label.
632    ///
633    /// See docs/spec/interfaces/issue-operations.md
634    pub async fn delete_label(&self, owner: &str, repo: &str, name: &str) -> Result<(), ApiError> {
635        let path = format!("/repos/{}/{}/labels/{}", owner, repo, name);
636        let response = self.delete(&path).await?;
637
638        let status = response.status();
639        if !status.is_success() {
640            return Err(match status.as_u16() {
641                404 => ApiError::NotFound,
642                403 => ApiError::AuthorizationFailed,
643                401 => ApiError::AuthenticationFailed,
644                _ => {
645                    let message = response
646                        .text()
647                        .await
648                        .unwrap_or_else(|_| "Unknown error".to_string());
649                    ApiError::HttpError {
650                        status: status.as_u16(),
651                        message,
652                    }
653                }
654            });
655        }
656        Ok(())
657    }
658
659    /// Add labels to an issue.
660    ///
661    /// See docs/spec/interfaces/issue-operations.md
662    pub async fn add_labels_to_issue(
663        &self,
664        owner: &str,
665        repo: &str,
666        issue_number: u64,
667        labels: Vec<String>,
668    ) -> Result<Vec<Label>, ApiError> {
669        let path = format!("/repos/{}/{}/issues/{}/labels", owner, repo, issue_number);
670        let response = self.post(&path, &labels).await?;
671
672        let status = response.status();
673        if !status.is_success() {
674            return Err(match status.as_u16() {
675                422 => {
676                    let message = response
677                        .text()
678                        .await
679                        .unwrap_or_else(|_| "Validation failed".to_string());
680                    ApiError::InvalidRequest { message }
681                }
682                404 => ApiError::NotFound,
683                403 => ApiError::AuthorizationFailed,
684                401 => ApiError::AuthenticationFailed,
685                _ => {
686                    let message = response
687                        .text()
688                        .await
689                        .unwrap_or_else(|_| "Unknown error".to_string());
690                    ApiError::HttpError {
691                        status: status.as_u16(),
692                        message,
693                    }
694                }
695            });
696        }
697        response.json().await.map_err(ApiError::from)
698    }
699
700    /// Remove a label from an issue.
701    ///
702    /// See docs/spec/interfaces/issue-operations.md
703    pub async fn remove_label_from_issue(
704        &self,
705        owner: &str,
706        repo: &str,
707        issue_number: u64,
708        name: &str,
709    ) -> Result<Vec<Label>, ApiError> {
710        let path = format!(
711            "/repos/{}/{}/issues/{}/labels/{}",
712            owner, repo, issue_number, name
713        );
714        let response = self.delete(&path).await?;
715
716        let status = response.status();
717        if !status.is_success() {
718            return Err(match status.as_u16() {
719                404 => ApiError::NotFound,
720                403 => ApiError::AuthorizationFailed,
721                401 => ApiError::AuthenticationFailed,
722                _ => {
723                    let message = response
724                        .text()
725                        .await
726                        .unwrap_or_else(|_| "Unknown error".to_string());
727                    ApiError::HttpError {
728                        status: status.as_u16(),
729                        message,
730                    }
731                }
732            });
733        }
734        response.json().await.map_err(ApiError::from)
735    }
736
737    // ========================================================================
738    // Comment Operations
739    // ========================================================================
740
741    /// List comments on an issue.
742    ///
743    /// See docs/spec/interfaces/issue-operations.md
744    pub async fn list_issue_comments(
745        &self,
746        owner: &str,
747        repo: &str,
748        issue_number: u64,
749    ) -> Result<Vec<Comment>, ApiError> {
750        let path = format!("/repos/{}/{}/issues/{}/comments", owner, repo, issue_number);
751        let response = self.get(&path).await?;
752
753        let status = response.status();
754        if !status.is_success() {
755            return Err(match status.as_u16() {
756                404 => ApiError::NotFound,
757                403 => ApiError::AuthorizationFailed,
758                401 => ApiError::AuthenticationFailed,
759                _ => {
760                    let message = response
761                        .text()
762                        .await
763                        .unwrap_or_else(|_| "Unknown error".to_string());
764                    ApiError::HttpError {
765                        status: status.as_u16(),
766                        message,
767                    }
768                }
769            });
770        }
771        response.json().await.map_err(ApiError::from)
772    }
773
774    /// Get a specific comment by ID.
775    ///
776    /// See docs/spec/interfaces/issue-operations.md
777    pub async fn get_issue_comment(
778        &self,
779        owner: &str,
780        repo: &str,
781        comment_id: u64,
782    ) -> Result<Comment, ApiError> {
783        let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
784        let response = self.get(&path).await?;
785
786        let status = response.status();
787        if !status.is_success() {
788            return Err(match status.as_u16() {
789                404 => ApiError::NotFound,
790                403 => ApiError::AuthorizationFailed,
791                401 => ApiError::AuthenticationFailed,
792                _ => {
793                    let message = response
794                        .text()
795                        .await
796                        .unwrap_or_else(|_| "Unknown error".to_string());
797                    ApiError::HttpError {
798                        status: status.as_u16(),
799                        message,
800                    }
801                }
802            });
803        }
804        response.json().await.map_err(ApiError::from)
805    }
806
807    /// Create a comment on an issue.
808    ///
809    /// See docs/spec/interfaces/issue-operations.md
810    pub async fn create_issue_comment(
811        &self,
812        owner: &str,
813        repo: &str,
814        issue_number: u64,
815        request: CreateCommentRequest,
816    ) -> Result<Comment, ApiError> {
817        let path = format!("/repos/{}/{}/issues/{}/comments", owner, repo, issue_number);
818        let response = self.post(&path, &request).await?;
819
820        let status = response.status();
821        if !status.is_success() {
822            return Err(match status.as_u16() {
823                422 => {
824                    let message = response
825                        .text()
826                        .await
827                        .unwrap_or_else(|_| "Validation failed".to_string());
828                    ApiError::InvalidRequest { message }
829                }
830                404 => ApiError::NotFound,
831                403 => ApiError::AuthorizationFailed,
832                401 => ApiError::AuthenticationFailed,
833                _ => {
834                    let message = response
835                        .text()
836                        .await
837                        .unwrap_or_else(|_| "Unknown error".to_string());
838                    ApiError::HttpError {
839                        status: status.as_u16(),
840                        message,
841                    }
842                }
843            });
844        }
845        response.json().await.map_err(ApiError::from)
846    }
847
848    /// Update an existing comment.
849    ///
850    /// See docs/spec/interfaces/issue-operations.md
851    pub async fn update_issue_comment(
852        &self,
853        owner: &str,
854        repo: &str,
855        comment_id: u64,
856        request: UpdateCommentRequest,
857    ) -> Result<Comment, ApiError> {
858        let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
859        let response = self.patch(&path, &request).await?;
860
861        let status = response.status();
862        if !status.is_success() {
863            return Err(match status.as_u16() {
864                422 => {
865                    let message = response
866                        .text()
867                        .await
868                        .unwrap_or_else(|_| "Validation failed".to_string());
869                    ApiError::InvalidRequest { message }
870                }
871                404 => ApiError::NotFound,
872                403 => ApiError::AuthorizationFailed,
873                401 => ApiError::AuthenticationFailed,
874                _ => {
875                    let message = response
876                        .text()
877                        .await
878                        .unwrap_or_else(|_| "Unknown error".to_string());
879                    ApiError::HttpError {
880                        status: status.as_u16(),
881                        message,
882                    }
883                }
884            });
885        }
886        response.json().await.map_err(ApiError::from)
887    }
888
889    /// Delete a comment.
890    ///
891    /// See docs/spec/interfaces/issue-operations.md
892    pub async fn delete_issue_comment(
893        &self,
894        owner: &str,
895        repo: &str,
896        comment_id: u64,
897    ) -> Result<(), ApiError> {
898        let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
899        let response = self.delete(&path).await?;
900
901        let status = response.status();
902        if !status.is_success() {
903            return Err(match status.as_u16() {
904                404 => ApiError::NotFound,
905                403 => ApiError::AuthorizationFailed,
906                401 => ApiError::AuthenticationFailed,
907                _ => {
908                    let message = response
909                        .text()
910                        .await
911                        .unwrap_or_else(|_| "Unknown error".to_string());
912                    ApiError::HttpError {
913                        status: status.as_u16(),
914                        message,
915                    }
916                }
917            });
918        }
919        Ok(())
920    }
921}
922
923#[cfg(test)]
924#[path = "issue_tests.rs"]
925mod tests;