1use crate::effects::{
6 dispatch_github_effect, EffectError, EffectHandler, EffectResult, GitHubEffects,
7};
8use crate::services::github::{CreatePRSpec, GitHubService, IssueFilter, PRFilter, Repo};
9use async_trait::async_trait;
10use exomonad_proto::effects::github::*;
11
12pub struct GitHubHandler {
17 service: GitHubService,
18}
19
20impl GitHubHandler {
21 pub fn new(service: GitHubService) -> Self {
22 Self { service }
23 }
24}
25
26#[async_trait]
27impl EffectHandler for GitHubHandler {
28 fn namespace(&self) -> &str {
29 "github"
30 }
31
32 async fn handle(&self, effect_type: &str, payload: &[u8]) -> EffectResult<Vec<u8>> {
33 dispatch_github_effect(self, effect_type, payload).await
34 }
35}
36
37#[async_trait]
38impl GitHubEffects for GitHubHandler {
39 async fn list_issues(&self, req: ListIssuesRequest) -> EffectResult<ListIssuesResponse> {
40 let repo = make_repo(&req.owner, &req.repo);
41
42 let state_str = issue_state_to_string(req.state());
43 let filter = if state_str.is_empty() && req.labels.is_empty() {
44 None
45 } else {
46 Some(IssueFilter {
47 state: Some(if state_str.is_empty() {
48 "open".to_string()
49 } else {
50 state_str
51 }),
52 labels: if req.labels.is_empty() {
53 None
54 } else {
55 Some(req.labels.clone())
56 },
57 })
58 };
59
60 let raw_issues = self
61 .service
62 .list_issues(&repo, filter.as_ref())
63 .await
64 .map_err(|e| EffectError::network_error(e.to_string()))?;
65
66 let limit = if req.limit <= 0 {
67 30
68 } else {
69 req.limit as usize
70 };
71 let issues: Vec<Issue> = raw_issues
72 .into_iter()
73 .take(limit)
74 .map(convert_issue)
75 .collect();
76
77 Ok(ListIssuesResponse { issues })
78 }
79
80 async fn get_issue(&self, req: GetIssueRequest) -> EffectResult<GetIssueResponse> {
81 let repo = make_repo(&req.owner, &req.repo);
82
83 let raw_issue = self
84 .service
85 .get_issue(&repo, req.number as u64)
86 .await
87 .map_err(|e| EffectError::network_error(e.to_string()))?;
88
89 let issue = convert_issue(raw_issue);
90 let comments: Vec<IssueComment> = Vec::new();
91
92 Ok(GetIssueResponse {
93 issue: Some(issue),
94 comments,
95 })
96 }
97
98 async fn list_pull_requests(
99 &self,
100 req: ListPullRequestsRequest,
101 ) -> EffectResult<ListPullRequestsResponse> {
102 let repo = make_repo(&req.owner, &req.repo);
103
104 let state_str = issue_state_to_string(req.state());
105 let limit = if req.limit <= 0 { 30 } else { req.limit as u32 };
106
107 let filter = Some(PRFilter {
108 state: Some(if state_str.is_empty() {
109 "open".to_string()
110 } else {
111 state_str
112 }),
113 limit: Some(limit),
114 });
115
116 let raw_prs = self
117 .service
118 .list_prs(&repo, filter.as_ref())
119 .await
120 .map_err(|e| EffectError::network_error(e.to_string()))?;
121
122 let pull_requests: Vec<PullRequest> = raw_prs.into_iter().map(convert_pr).collect();
123
124 Ok(ListPullRequestsResponse { pull_requests })
125 }
126
127 async fn get_pull_request(
128 &self,
129 req: GetPullRequestRequest,
130 ) -> EffectResult<GetPullRequestResponse> {
131 let repo = make_repo(&req.owner, &req.repo);
132
133 let filter = Some(PRFilter {
134 state: Some("all".to_string()),
135 limit: Some(100),
136 });
137
138 let raw_prs = self
139 .service
140 .list_prs(&repo, filter.as_ref())
141 .await
142 .map_err(|e| EffectError::network_error(e.to_string()))?;
143
144 let raw_pr = raw_prs
145 .into_iter()
146 .find(|pr| pr.number == req.number as u64)
147 .ok_or_else(|| EffectError::not_found(format!("PR #{}", req.number)))?;
148
149 let pull_request = convert_pr(raw_pr);
150 let reviews: Vec<Review> = Vec::new();
151
152 Ok(GetPullRequestResponse {
153 pull_request: Some(pull_request),
154 reviews,
155 })
156 }
157
158 async fn get_pull_request_for_branch(
159 &self,
160 req: GetPullRequestForBranchRequest,
161 ) -> EffectResult<GetPullRequestForBranchResponse> {
162 let repo = make_repo(&req.owner, &req.repo);
163
164 let result = self
165 .service
166 .get_pr_for_branch(&repo, &req.branch)
167 .await
168 .map_err(|e| EffectError::network_error(e.to_string()))?;
169
170 let found = result.is_some();
171 Ok(GetPullRequestForBranchResponse {
172 pull_request: result.map(convert_pr),
173 found,
174 })
175 }
176
177 async fn create_pull_request(
178 &self,
179 req: CreatePullRequestRequest,
180 ) -> EffectResult<CreatePullRequestResponse> {
181 let repo = make_repo(&req.owner, &req.repo);
182
183 let spec = CreatePRSpec {
184 title: req.title,
185 body: req.body,
186 head: req.head.clone(),
187 base: if req.base.is_empty() {
188 "main".to_string()
189 } else {
190 req.base
191 },
192 };
193
194 let raw_pr = self
195 .service
196 .create_pr(&repo, spec)
197 .await
198 .map_err(|e| EffectError::network_error(e.to_string()))?;
199
200 let url = raw_pr.url.clone();
201 let pull_request = convert_pr(raw_pr);
202
203 Ok(CreatePullRequestResponse {
204 pull_request: Some(pull_request),
205 url,
206 })
207 }
208
209 async fn get_pull_request_review_comments(
210 &self,
211 req: GetPullRequestReviewCommentsRequest,
212 ) -> EffectResult<GetPullRequestReviewCommentsResponse> {
213 tracing::debug!(
215 owner = %req.owner,
216 repo = %req.repo,
217 number = req.number,
218 "get_pull_request_review_comments: not yet implemented"
219 );
220 Ok(GetPullRequestReviewCommentsResponse {
221 comments: Vec::new(),
222 })
223 }
224}
225
226fn make_repo(owner: &str, repo: &str) -> Repo {
227 Repo {
228 owner: owner.into(),
229 name: repo.into(),
230 }
231}
232
233fn issue_state_to_string(state: IssueState) -> String {
234 match state {
235 IssueState::Unspecified => String::new(),
236 IssueState::Open => "open".to_string(),
237 IssueState::Closed => "closed".to_string(),
238 IssueState::All => "all".to_string(),
239 }
240}
241
242fn convert_issue(i: crate::services::github::Issue) -> Issue {
243 Issue {
244 number: i.number as i32,
245 title: i.title,
246 body: i.body,
247 state: IssueState::Open as i32, author: Some(User {
249 login: i.author,
250 id: 0,
251 avatar_url: String::new(),
252 }),
253 labels: i
254 .labels
255 .into_iter()
256 .map(|l| Label {
257 name: l,
258 color: String::new(),
259 description: String::new(),
260 })
261 .collect(),
262 created_at: 0,
263 updated_at: 0,
264 comments_count: 0,
265 }
266}
267
268fn convert_pr(pr: crate::services::github::PullRequest) -> PullRequest {
269 PullRequest {
270 number: pr.number as i32,
271 title: pr.title,
272 body: pr.body,
273 state: IssueState::Open as i32,
274 author: Some(User {
275 login: pr.author,
276 id: 0,
277 avatar_url: String::new(),
278 }),
279 head_ref: pr.head_ref,
280 base_ref: pr.base_ref,
281 merged: pr.merged_at.is_some(),
282 draft: false,
283 labels: Vec::new(),
284 created_at: 0,
285 updated_at: 0,
286 }
287}