1use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8
9use crate::client::{parse_link_header, InstallationClient, PagedResponse};
10use crate::error::ApiError;
11
12#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum MilestoneState {
18 Open,
19 Closed,
20}
21
22#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct Issue {
29 pub id: u64,
31
32 pub node_id: String,
34
35 pub number: u64,
37
38 pub title: String,
40
41 pub body: Option<String>,
43
44 pub state: String, #[serde(default)]
49 pub locked: bool,
50
51 pub user: IssueUser,
53
54 pub assignees: Vec<IssueUser>,
56
57 pub labels: Vec<Label>,
59
60 pub milestone: Option<Milestone>,
62
63 pub comments: u64,
65
66 pub created_at: DateTime<Utc>,
68
69 pub updated_at: DateTime<Utc>,
71
72 pub closed_at: Option<DateTime<Utc>>,
74
75 pub html_url: String,
77}
78
79#[derive(Debug, Clone, Serialize, Deserialize)]
81pub struct IssueUser {
82 pub login: String,
84
85 pub id: u64,
87
88 pub node_id: String,
90
91 #[serde(rename = "type")]
93 pub user_type: String,
94}
95
96#[derive(Debug, Clone, Serialize, Deserialize)]
98pub struct Milestone {
99 pub id: u64,
101
102 pub node_id: String,
104
105 pub number: u64,
107
108 pub title: String,
110
111 pub description: Option<String>,
113
114 pub state: MilestoneState,
116
117 pub open_issues: u32,
119
120 pub closed_issues: u32,
122
123 pub due_on: Option<DateTime<Utc>>,
125
126 pub created_at: DateTime<Utc>,
128
129 pub updated_at: DateTime<Utc>,
131
132 pub closed_at: Option<DateTime<Utc>>,
134}
135
136#[derive(Debug, Clone, Serialize, Deserialize)]
140pub struct Label {
141 pub id: u64,
143
144 pub node_id: String,
146
147 pub name: String,
149
150 pub description: Option<String>,
152
153 pub color: String,
155
156 pub default: bool,
158}
159
160#[derive(Debug, Clone, Serialize, Deserialize)]
162pub struct Comment {
163 pub id: u64,
165
166 pub node_id: String,
168
169 pub body: String,
171
172 pub user: IssueUser,
174
175 pub created_at: DateTime<Utc>,
177
178 pub updated_at: DateTime<Utc>,
180
181 pub html_url: String,
183}
184
185#[derive(Debug, Clone, Serialize)]
187pub struct CreateIssueRequest {
188 pub title: String,
190
191 #[serde(skip_serializing_if = "Option::is_none")]
193 pub body: Option<String>,
194
195 #[serde(skip_serializing_if = "Option::is_none")]
197 pub assignees: Option<Vec<String>>,
198
199 #[serde(skip_serializing_if = "Option::is_none")]
201 pub milestone: Option<u64>,
202
203 #[serde(skip_serializing_if = "Option::is_none")]
205 pub labels: Option<Vec<String>>,
206}
207
208#[derive(Debug, Clone, Serialize, Default)]
210pub struct UpdateIssueRequest {
211 #[serde(skip_serializing_if = "Option::is_none")]
213 pub title: Option<String>,
214
215 #[serde(skip_serializing_if = "Option::is_none")]
217 pub body: Option<String>,
218
219 #[serde(skip_serializing_if = "Option::is_none")]
221 pub state: Option<String>, #[serde(skip_serializing_if = "Option::is_none")]
225 pub assignees: Option<Vec<String>>,
226
227 #[serde(skip_serializing_if = "Option::is_none")]
229 pub milestone: Option<u64>,
230
231 #[serde(skip_serializing_if = "Option::is_none")]
233 pub labels: Option<Vec<String>>,
234}
235
236#[derive(Debug, Clone, Serialize)]
238pub struct CreateLabelRequest {
239 pub name: String,
241
242 pub color: String,
244
245 #[serde(skip_serializing_if = "Option::is_none")]
247 pub description: Option<String>,
248}
249
250#[derive(Debug, Clone, Serialize, Default)]
252pub struct UpdateLabelRequest {
253 #[serde(rename = "name", skip_serializing_if = "Option::is_none")]
255 pub new_name: Option<String>,
256
257 #[serde(skip_serializing_if = "Option::is_none")]
259 pub color: Option<String>,
260
261 #[serde(skip_serializing_if = "Option::is_none")]
263 pub description: Option<String>,
264}
265
266#[derive(Debug, Clone, Serialize)]
268pub struct CreateCommentRequest {
269 pub body: String,
271}
272
273#[derive(Debug, Clone, Serialize)]
275pub struct UpdateCommentRequest {
276 pub body: String,
278}
279
280#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
288#[serde(rename_all = "kebab-case")]
289pub enum LockReason {
290 OffTopic,
291 TooHeated,
292 Resolved,
293 Spam,
294}
295
296#[derive(Debug, Clone, Serialize, Deserialize)]
302pub struct IssueActivityEvent {
303 pub id: u64,
304 pub event: String, pub actor: IssueUser,
306 pub created_at: DateTime<Utc>,
307 pub label: Option<Label>,
308 pub assignee: Option<IssueUser>,
309 pub milestone: Option<MilestoneSummary>,
310 pub rename: Option<IssueRename>,
311}
312
313#[derive(Debug, Clone, Serialize, Deserialize)]
315pub struct MilestoneSummary {
316 pub title: String,
317}
318
319#[derive(Debug, Clone, Serialize, Deserialize)]
321pub struct IssueRename {
322 pub from: String,
323 pub to: String,
324}
325
326#[derive(Debug, Clone, Serialize, Deserialize)]
333#[serde(tag = "event", rename_all = "snake_case")]
334pub enum TimelineEvent {
335 #[serde(rename = "commented")]
339 Commented {
340 id: u64,
341 user: IssueUser,
342 body: Option<String>,
343 created_at: DateTime<Utc>,
344 updated_at: DateTime<Utc>,
345 html_url: String,
346 },
347 Labeled {
348 id: u64,
349 actor: IssueUser,
350 label: Label,
351 created_at: DateTime<Utc>,
352 },
353 Unlabeled {
354 id: u64,
355 actor: IssueUser,
356 label: Label,
357 created_at: DateTime<Utc>,
358 },
359 Assigned {
360 id: u64,
361 actor: IssueUser,
362 assignee: IssueUser,
363 created_at: DateTime<Utc>,
364 },
365 Unassigned {
366 id: u64,
367 actor: IssueUser,
368 assignee: IssueUser,
369 created_at: DateTime<Utc>,
370 },
371 Milestoned {
372 id: u64,
373 actor: IssueUser,
374 milestone: MilestoneSummary,
375 created_at: DateTime<Utc>,
376 },
377 Demilestoned {
378 id: u64,
379 actor: IssueUser,
380 milestone: MilestoneSummary,
381 created_at: DateTime<Utc>,
382 },
383 Closed {
384 id: u64,
385 actor: IssueUser,
386 created_at: DateTime<Utc>,
387 },
388 Reopened {
389 id: u64,
390 actor: IssueUser,
391 created_at: DateTime<Utc>,
392 },
393 Locked {
394 id: u64,
395 actor: IssueUser,
396 lock_reason: Option<String>,
397 created_at: DateTime<Utc>,
398 },
399 Unlocked {
400 id: u64,
401 actor: IssueUser,
402 created_at: DateTime<Utc>,
403 },
404 Renamed {
405 id: u64,
406 actor: IssueUser,
407 rename: IssueRename,
408 created_at: DateTime<Utc>,
409 },
410 Referenced {
411 id: u64,
412 actor: IssueUser,
413 created_at: DateTime<Utc>,
414 },
415 #[serde(other)]
417 Unknown,
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
427#[serde(rename_all = "snake_case")]
428pub enum ReactionContent {
429 #[serde(rename = "+1")]
431 PlusOne,
432 #[serde(rename = "-1")]
434 MinusOne,
435 Laugh,
437 Confused,
439 Heart,
441 Hooray,
443 Rocket,
445 Eyes,
447}
448
449#[derive(Debug, Clone, Serialize, Deserialize)]
453pub struct Reaction {
454 pub id: u64,
455 pub node_id: String,
456 pub user: IssueUser,
457 pub content: ReactionContent,
458 pub created_at: DateTime<Utc>,
459}
460
461#[derive(Debug, Clone)]
469pub enum SortDirection {
470 Asc,
471 Desc,
472}
473
474#[derive(Debug, Clone)]
478pub enum MilestoneSortField {
479 DueOn,
480 Completeness,
481}
482
483#[derive(Debug, Clone, Default)]
487pub struct ListMilestonesQuery {
488 pub state: Option<MilestoneState>,
489 pub sort: Option<MilestoneSortField>,
490 pub direction: Option<SortDirection>,
491}
492
493#[derive(Debug, Clone, Serialize)]
497pub struct CreateMilestoneRequest {
498 pub title: String,
499 #[serde(skip_serializing_if = "Option::is_none")]
500 pub state: Option<MilestoneState>,
501 #[serde(skip_serializing_if = "Option::is_none")]
502 pub description: Option<String>,
503 #[serde(skip_serializing_if = "Option::is_none")]
504 pub due_on: Option<DateTime<Utc>>,
505}
506
507#[derive(Debug, Clone, Default, Serialize)]
511pub struct UpdateMilestoneRequest {
512 #[serde(skip_serializing_if = "Option::is_none")]
513 pub title: Option<String>,
514 #[serde(skip_serializing_if = "Option::is_none")]
515 pub state: Option<MilestoneState>,
516 #[serde(skip_serializing_if = "Option::is_none")]
517 pub description: Option<String>,
518 #[serde(skip_serializing_if = "Option::is_none")]
519 pub due_on: Option<DateTime<Utc>>,
520}
521
522#[derive(Debug, Clone, Serialize)]
527pub(crate) struct LabelsRequest {
528 pub(crate) labels: Vec<String>,
529}
530
531#[derive(Debug, Clone, Serialize)]
532struct LockIssueRequest {
533 #[serde(skip_serializing_if = "Option::is_none")]
534 lock_reason: Option<LockReason>,
535}
536
537#[derive(Debug, Clone, Serialize)]
538struct AssigneesRequest {
539 assignees: Vec<String>,
540}
541
542#[derive(Debug, Clone, Serialize)]
543struct CreateReactionRequest {
544 content: ReactionContent,
545}
546
547#[derive(Debug, Clone)]
560pub struct IssuesClient {
561 client: InstallationClient,
562}
563
564impl IssuesClient {
565 pub(crate) fn new(client: InstallationClient) -> Self {
566 Self { client }
567 }
568
569 pub async fn list(
577 &self,
578 owner: &str,
579 repo: &str,
580 state: Option<&str>,
581 page: Option<u32>,
582 ) -> Result<PagedResponse<Issue>, ApiError> {
583 let mut path = format!("/repos/{}/{}/issues", owner, repo);
584 let mut params: Vec<String> = Vec::new();
585 if let Some(s) = state {
586 params.push(format!("state={}", s));
587 }
588 if let Some(p) = page {
589 params.push(format!("page={}", p));
590 }
591 if !params.is_empty() {
592 path = format!("{}?{}", path, params.join("&"));
593 }
594
595 let response = self.client.get(&path).await?;
596 let status = response.status();
597 if !status.is_success() {
598 return Err(super::map_http_error(status, response).await);
599 }
600
601 let pagination = response
602 .headers()
603 .get("Link")
604 .and_then(|h| h.to_str().ok())
605 .map(|h| parse_link_header(Some(h)))
606 .unwrap_or_default();
607
608 let items: Vec<Issue> = response.json().await.map_err(ApiError::from)?;
609 Ok(PagedResponse {
610 items,
611 total_count: None,
612 pagination,
613 })
614 }
615
616 pub async fn get(&self, owner: &str, repo: &str, issue_number: u64) -> Result<Issue, ApiError> {
621 let path = format!("/repos/{}/{}/issues/{}", owner, repo, issue_number);
622 let response = self.client.get(&path).await?;
623 let status = response.status();
624 if !status.is_success() {
625 return Err(super::map_http_error(status, response).await);
626 }
627 response.json().await.map_err(ApiError::from)
628 }
629
630 pub async fn create(
635 &self,
636 owner: &str,
637 repo: &str,
638 request: CreateIssueRequest,
639 ) -> Result<Issue, ApiError> {
640 let path = format!("/repos/{}/{}/issues", owner, repo);
641 let response = self.client.post(&path, &request).await?;
642 let status = response.status();
643 if !status.is_success() {
644 return Err(super::map_http_error(status, response).await);
645 }
646 response.json().await.map_err(ApiError::from)
647 }
648
649 pub async fn update(
654 &self,
655 owner: &str,
656 repo: &str,
657 issue_number: u64,
658 request: UpdateIssueRequest,
659 ) -> Result<Issue, ApiError> {
660 let path = format!("/repos/{}/{}/issues/{}", owner, repo, issue_number);
661 let response = self.client.patch(&path, &request).await?;
662 let status = response.status();
663 if !status.is_success() {
664 return Err(super::map_http_error(status, response).await);
665 }
666 response.json().await.map_err(ApiError::from)
667 }
668
669 pub async fn set_milestone(
673 &self,
674 owner: &str,
675 repo: &str,
676 issue_number: u64,
677 milestone_number: Option<u64>,
678 ) -> Result<Issue, ApiError> {
679 self.update(
680 owner,
681 repo,
682 issue_number,
683 UpdateIssueRequest {
684 milestone: milestone_number,
685 ..Default::default()
686 },
687 )
688 .await
689 }
690
691 pub async fn list_comments(
700 &self,
701 owner: &str,
702 repo: &str,
703 issue_number: u64,
704 ) -> Result<Vec<Comment>, ApiError> {
705 let base = format!("/repos/{}/{}/issues/{}/comments", owner, repo, issue_number);
706 self.fetch_all(&base).await
707 }
708
709 pub async fn get_comment(
711 &self,
712 owner: &str,
713 repo: &str,
714 comment_id: u64,
715 ) -> Result<Comment, ApiError> {
716 let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
717 let response = self.client.get(&path).await?;
718 let status = response.status();
719 if !status.is_success() {
720 return Err(super::map_http_error(status, response).await);
721 }
722 response.json().await.map_err(ApiError::from)
723 }
724
725 pub async fn create_comment(
727 &self,
728 owner: &str,
729 repo: &str,
730 issue_number: u64,
731 request: CreateCommentRequest,
732 ) -> Result<Comment, ApiError> {
733 let path = format!("/repos/{}/{}/issues/{}/comments", owner, repo, issue_number);
734 let response = self.client.post(&path, &request).await?;
735 let status = response.status();
736 if !status.is_success() {
737 return Err(super::map_http_error(status, response).await);
738 }
739 response.json().await.map_err(ApiError::from)
740 }
741
742 pub async fn update_comment(
744 &self,
745 owner: &str,
746 repo: &str,
747 comment_id: u64,
748 request: UpdateCommentRequest,
749 ) -> Result<Comment, ApiError> {
750 let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
751 let response = self.client.patch(&path, &request).await?;
752 let status = response.status();
753 if !status.is_success() {
754 return Err(super::map_http_error(status, response).await);
755 }
756 response.json().await.map_err(ApiError::from)
757 }
758
759 pub async fn delete_comment(
761 &self,
762 owner: &str,
763 repo: &str,
764 comment_id: u64,
765 ) -> Result<(), ApiError> {
766 let path = format!("/repos/{}/{}/issues/comments/{}", owner, repo, comment_id);
767 let response = self.client.delete(&path).await?;
768 let status = response.status();
769 if !status.is_success() {
770 return Err(super::map_http_error(status, response).await);
771 }
772 Ok(())
773 }
774
775 pub async fn list_labels(
779 &self,
780 owner: &str,
781 repo: &str,
782 issue_number: u64,
783 ) -> Result<Vec<Label>, ApiError> {
784 let base = format!("/repos/{}/{}/issues/{}/labels", owner, repo, issue_number);
785 self.fetch_all(&base).await
786 }
787
788 pub async fn add_labels(
792 &self,
793 owner: &str,
794 repo: &str,
795 issue_number: u64,
796 labels: Vec<String>,
797 ) -> Result<Vec<Label>, ApiError> {
798 let path = format!("/repos/{}/{}/issues/{}/labels", owner, repo, issue_number);
799 let body = LabelsRequest { labels };
800 let response = self.client.post(&path, &body).await?;
801 let status = response.status();
802 if !status.is_success() {
803 return Err(super::map_http_error(status, response).await);
804 }
805 response.json().await.map_err(ApiError::from)
806 }
807
808 pub async fn remove_label(
815 &self,
816 owner: &str,
817 repo: &str,
818 issue_number: u64,
819 label_name: &str,
820 ) -> Result<Vec<Label>, ApiError> {
821 let path = format!(
822 "/repos/{}/{}/issues/{}/labels/{}",
823 owner,
824 repo,
825 issue_number,
826 urlencoding::encode(label_name)
827 );
828 let response = self.client.delete(&path).await?;
829 let status = response.status();
830 if !status.is_success() {
831 return Err(super::map_http_error(status, response).await);
832 }
833 response.json().await.map_err(ApiError::from)
834 }
835
836 pub async fn replace_labels(
841 &self,
842 owner: &str,
843 repo: &str,
844 issue_number: u64,
845 labels: Vec<String>,
846 ) -> Result<Vec<Label>, ApiError> {
847 let path = format!("/repos/{}/{}/issues/{}/labels", owner, repo, issue_number);
848 let body = LabelsRequest { labels };
849 let response = self.client.put(&path, &body).await?;
850 let status = response.status();
851 if !status.is_success() {
852 return Err(super::map_http_error(status, response).await);
853 }
854 response.json().await.map_err(ApiError::from)
855 }
856
857 pub async fn list_reactions(
861 &self,
862 owner: &str,
863 repo: &str,
864 issue_number: u64,
865 ) -> Result<Vec<Reaction>, ApiError> {
866 let base = format!(
867 "/repos/{}/{}/issues/{}/reactions",
868 owner, repo, issue_number
869 );
870 self.fetch_all(&base).await
871 }
872
873 pub async fn create_reaction(
878 &self,
879 owner: &str,
880 repo: &str,
881 issue_number: u64,
882 content: ReactionContent,
883 ) -> Result<Reaction, ApiError> {
884 let path = format!(
885 "/repos/{}/{}/issues/{}/reactions",
886 owner, repo, issue_number
887 );
888 let body = CreateReactionRequest { content };
889 let response = self.client.post(&path, &body).await?;
890 let status = response.status();
891 if !status.is_success() {
892 return Err(super::map_http_error(status, response).await);
893 }
894 response.json().await.map_err(ApiError::from)
895 }
896
897 pub async fn delete_reaction(
899 &self,
900 owner: &str,
901 repo: &str,
902 issue_number: u64,
903 reaction_id: u64,
904 ) -> Result<(), ApiError> {
905 let path = format!(
906 "/repos/{}/{}/issues/{}/reactions/{}",
907 owner, repo, issue_number, reaction_id
908 );
909 let response = self.client.delete(&path).await?;
910 let status = response.status();
911 if !status.is_success() {
912 return Err(super::map_http_error(status, response).await);
913 }
914 Ok(())
915 }
916
917 pub async fn list_comment_reactions(
919 &self,
920 owner: &str,
921 repo: &str,
922 comment_id: u64,
923 ) -> Result<Vec<Reaction>, ApiError> {
924 let base = format!(
925 "/repos/{}/{}/issues/comments/{}/reactions",
926 owner, repo, comment_id
927 );
928 self.fetch_all(&base).await
929 }
930
931 pub async fn create_comment_reaction(
933 &self,
934 owner: &str,
935 repo: &str,
936 comment_id: u64,
937 content: ReactionContent,
938 ) -> Result<Reaction, ApiError> {
939 let path = format!(
940 "/repos/{}/{}/issues/comments/{}/reactions",
941 owner, repo, comment_id
942 );
943 let body = CreateReactionRequest { content };
944 let response = self.client.post(&path, &body).await?;
945 let status = response.status();
946 if !status.is_success() {
947 return Err(super::map_http_error(status, response).await);
948 }
949 response.json().await.map_err(ApiError::from)
950 }
951
952 pub async fn delete_comment_reaction(
954 &self,
955 owner: &str,
956 repo: &str,
957 comment_id: u64,
958 reaction_id: u64,
959 ) -> Result<(), ApiError> {
960 let path = format!(
961 "/repos/{}/{}/issues/comments/{}/reactions/{}",
962 owner, repo, comment_id, reaction_id
963 );
964 let response = self.client.delete(&path).await?;
965 let status = response.status();
966 if !status.is_success() {
967 return Err(super::map_http_error(status, response).await);
968 }
969 Ok(())
970 }
971
972 pub async fn list_available_assignees(
976 &self,
977 owner: &str,
978 repo: &str,
979 ) -> Result<Vec<IssueUser>, ApiError> {
980 let base = format!("/repos/{}/{}/assignees", owner, repo);
981 self.fetch_all(&base).await
982 }
983
984 pub async fn add_assignees(
989 &self,
990 owner: &str,
991 repo: &str,
992 issue_number: u64,
993 assignees: Vec<String>,
994 ) -> Result<Issue, ApiError> {
995 let path = format!(
996 "/repos/{}/{}/issues/{}/assignees",
997 owner, repo, issue_number
998 );
999 let body = AssigneesRequest { assignees };
1000 let response = self.client.post(&path, &body).await?;
1001 let status = response.status();
1002 if !status.is_success() {
1003 return Err(super::map_http_error(status, response).await);
1004 }
1005 response.json().await.map_err(ApiError::from)
1006 }
1007
1008 pub async fn remove_assignees(
1013 &self,
1014 owner: &str,
1015 repo: &str,
1016 issue_number: u64,
1017 assignees: Vec<String>,
1018 ) -> Result<Issue, ApiError> {
1019 let path = format!(
1020 "/repos/{}/{}/issues/{}/assignees",
1021 owner, repo, issue_number
1022 );
1023 let body = AssigneesRequest { assignees };
1024 let response = self.client.delete_with_body(&path, &body).await?;
1025 let status = response.status();
1026 if !status.is_success() {
1027 return Err(super::map_http_error(status, response).await);
1028 }
1029 response.json().await.map_err(ApiError::from)
1030 }
1031
1032 pub async fn lock(
1042 &self,
1043 owner: &str,
1044 repo: &str,
1045 issue_number: u64,
1046 reason: Option<LockReason>,
1047 ) -> Result<(), ApiError> {
1048 let path = format!("/repos/{}/{}/issues/{}/lock", owner, repo, issue_number);
1049 let body = LockIssueRequest {
1050 lock_reason: reason,
1051 };
1052 let response = self.client.put(&path, &body).await?;
1053 let status = response.status();
1054 if !status.is_success() {
1055 return Err(super::map_http_error(status, response).await);
1056 }
1057 Ok(())
1058 }
1059
1060 pub async fn unlock(&self, owner: &str, repo: &str, issue_number: u64) -> Result<(), ApiError> {
1065 let path = format!("/repos/{}/{}/issues/{}/lock", owner, repo, issue_number);
1066 let response = self.client.delete(&path).await?;
1067 let status = response.status();
1068 if !status.is_success() {
1069 return Err(super::map_http_error(status, response).await);
1070 }
1071 Ok(())
1072 }
1073
1074 pub async fn list_activity_events(
1080 &self,
1081 owner: &str,
1082 repo: &str,
1083 issue_number: u64,
1084 ) -> Result<Vec<IssueActivityEvent>, ApiError> {
1085 let base = format!("/repos/{}/{}/issues/{}/events", owner, repo, issue_number);
1086 self.fetch_all(&base).await
1087 }
1088
1089 pub async fn list_timeline(
1094 &self,
1095 owner: &str,
1096 repo: &str,
1097 issue_number: u64,
1098 ) -> Result<Vec<TimelineEvent>, ApiError> {
1099 let base = format!("/repos/{}/{}/issues/{}/timeline", owner, repo, issue_number);
1100 self.fetch_all(&base).await
1101 }
1102
1103 async fn fetch_all<T: serde::de::DeserializeOwned>(
1110 &self,
1111 base_path: &str,
1112 ) -> Result<Vec<T>, ApiError> {
1113 let first_page = format!("{}?per_page=100", base_path);
1114 self.client.fetch_all_pages(&first_page).await
1115 }
1116}
1117
1118#[derive(Debug, Clone)]
1131pub struct LabelsClient {
1132 client: InstallationClient,
1133}
1134
1135impl LabelsClient {
1136 pub(crate) fn new(client: InstallationClient) -> Self {
1137 Self { client }
1138 }
1139
1140 pub async fn list(&self, owner: &str, repo: &str) -> Result<Vec<Label>, ApiError> {
1145 let first_page = format!("/repos/{}/{}/labels?per_page=100", owner, repo);
1146 self.client.fetch_all_pages(&first_page).await
1147 }
1148
1149 pub async fn get(&self, owner: &str, repo: &str, name: &str) -> Result<Label, ApiError> {
1154 let path = format!(
1155 "/repos/{}/{}/labels/{}",
1156 owner,
1157 repo,
1158 urlencoding::encode(name)
1159 );
1160 let response = self.client.get(&path).await?;
1161 let status = response.status();
1162 if !status.is_success() {
1163 return Err(super::map_http_error(status, response).await);
1164 }
1165 response.json().await.map_err(ApiError::from)
1166 }
1167
1168 pub async fn create(
1174 &self,
1175 owner: &str,
1176 repo: &str,
1177 request: CreateLabelRequest,
1178 ) -> Result<Label, ApiError> {
1179 let path = format!("/repos/{}/{}/labels", owner, repo);
1180 let response = self.client.post(&path, &request).await?;
1181 let status = response.status();
1182 if !status.is_success() {
1183 return Err(super::map_http_error(status, response).await);
1184 }
1185 response.json().await.map_err(ApiError::from)
1186 }
1187
1188 pub async fn update(
1196 &self,
1197 owner: &str,
1198 repo: &str,
1199 name: &str,
1200 request: UpdateLabelRequest,
1201 ) -> Result<Label, ApiError> {
1202 let path = format!(
1203 "/repos/{}/{}/labels/{}",
1204 owner,
1205 repo,
1206 urlencoding::encode(name)
1207 );
1208 let response = self.client.patch(&path, &request).await?;
1209 let status = response.status();
1210 if !status.is_success() {
1211 return Err(super::map_http_error(status, response).await);
1212 }
1213 response.json().await.map_err(ApiError::from)
1214 }
1215
1216 pub async fn delete(&self, owner: &str, repo: &str, name: &str) -> Result<(), ApiError> {
1224 let path = format!(
1225 "/repos/{}/{}/labels/{}",
1226 owner,
1227 repo,
1228 urlencoding::encode(name)
1229 );
1230 let response = self.client.delete(&path).await?;
1231 let status = response.status();
1232 if !status.is_success() {
1233 return Err(super::map_http_error(status, response).await);
1234 }
1235 Ok(())
1236 }
1237}
1238
1239#[derive(Debug, Clone)]
1252pub struct MilestonesClient {
1253 client: InstallationClient,
1254}
1255
1256impl MilestonesClient {
1257 pub(crate) fn new(client: InstallationClient) -> Self {
1258 Self { client }
1259 }
1260
1261 pub async fn list(
1266 &self,
1267 owner: &str,
1268 repo: &str,
1269 query: Option<ListMilestonesQuery>,
1270 ) -> Result<Vec<Milestone>, ApiError> {
1271 let base = format!("/repos/{}/{}/milestones", owner, repo);
1272 let mut params: Vec<String> = vec!["per_page=100".to_string()];
1273
1274 if let Some(q) = query {
1275 if let Some(state) = q.state {
1276 let s = match state {
1277 MilestoneState::Open => "open",
1278 MilestoneState::Closed => "closed",
1279 };
1280 params.push(format!("state={}", s));
1281 }
1282 if let Some(sort) = q.sort {
1283 let s = match sort {
1284 MilestoneSortField::DueOn => "due_on",
1285 MilestoneSortField::Completeness => "completeness",
1286 };
1287 params.push(format!("sort={}", s));
1288 }
1289 if let Some(dir) = q.direction {
1290 let d = match dir {
1291 SortDirection::Asc => "asc",
1292 SortDirection::Desc => "desc",
1293 };
1294 params.push(format!("direction={}", d));
1295 }
1296 }
1297
1298 let first_page = format!("{}?{}", base, params.join("&"));
1299 self.client.fetch_all_pages(&first_page).await
1300 }
1301
1302 pub async fn get(
1307 &self,
1308 owner: &str,
1309 repo: &str,
1310 milestone_number: u64,
1311 ) -> Result<Milestone, ApiError> {
1312 let path = format!("/repos/{}/{}/milestones/{}", owner, repo, milestone_number);
1313 let response = self.client.get(&path).await?;
1314 let status = response.status();
1315 if !status.is_success() {
1316 return Err(super::map_http_error(status, response).await);
1317 }
1318 response.json().await.map_err(ApiError::from)
1319 }
1320
1321 pub async fn create(
1327 &self,
1328 owner: &str,
1329 repo: &str,
1330 request: CreateMilestoneRequest,
1331 ) -> Result<Milestone, ApiError> {
1332 let path = format!("/repos/{}/{}/milestones", owner, repo);
1333 let response = self.client.post(&path, &request).await?;
1334 let status = response.status();
1335 if !status.is_success() {
1336 return Err(super::map_http_error(status, response).await);
1337 }
1338 response.json().await.map_err(ApiError::from)
1339 }
1340
1341 pub async fn update(
1347 &self,
1348 owner: &str,
1349 repo: &str,
1350 milestone_number: u64,
1351 request: UpdateMilestoneRequest,
1352 ) -> Result<Milestone, ApiError> {
1353 let path = format!("/repos/{}/{}/milestones/{}", owner, repo, milestone_number);
1354 let response = self.client.patch(&path, &request).await?;
1355 let status = response.status();
1356 if !status.is_success() {
1357 return Err(super::map_http_error(status, response).await);
1358 }
1359 response.json().await.map_err(ApiError::from)
1360 }
1361
1362 pub async fn delete(
1370 &self,
1371 owner: &str,
1372 repo: &str,
1373 milestone_number: u64,
1374 ) -> Result<(), ApiError> {
1375 let path = format!("/repos/{}/{}/milestones/{}", owner, repo, milestone_number);
1376 let response = self.client.delete(&path).await?;
1377 let status = response.status();
1378 if !status.is_success() {
1379 return Err(super::map_http_error(status, response).await);
1380 }
1381 Ok(())
1382 }
1383}
1384
1385#[cfg(test)]
1386#[path = "issue_tests.rs"]
1387mod tests;