1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeRemote,
7 GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
8 RequestedReview, ReviewCommentSnapshot, ReviewRequestError, ReviewRequestSummary,
9 UpdateReviewRequestInput, detect_remote,
10};
11
12#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
17pub trait ReviewRequestClient: Send + Sync {
18 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError>;
24
25 fn find_by_source_branch(
31 &self,
32 remote: ForgeRemote,
33 source_branch: String,
34 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>>;
35
36 fn create_review_request(
41 &self,
42 remote: ForgeRemote,
43 input: CreateReviewRequestInput,
44 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
45
46 fn refresh_review_request(
51 &self,
52 remote: ForgeRemote,
53 display_id: String,
54 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
55
56 fn sync_review_request_metadata(
63 &self,
64 remote: ForgeRemote,
65 display_id: String,
66 input: UpdateReviewRequestInput,
67 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
68
69 fn review_request_web_url(
75 &self,
76 review_request: &ReviewRequestSummary,
77 ) -> Result<String, ReviewRequestError>;
78
79 fn fetch_review_comment_snapshot(
89 &self,
90 remote: ForgeRemote,
91 display_id: String,
92 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>>;
93
94 fn list_requested_reviews(
101 &self,
102 remote: ForgeRemote,
103 ) -> ForgeFuture<Result<Vec<RequestedReview>, ReviewRequestError>>;
104}
105
106pub struct RealReviewRequestClient {
108 command_runner: Arc<dyn ForgeCommandRunner>,
109}
110
111impl RealReviewRequestClient {
112 pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
114 Self { command_runner }
115 }
116}
117
118impl Default for RealReviewRequestClient {
119 fn default() -> Self {
120 Self::new(Arc::new(RealForgeCommandRunner))
121 }
122}
123
124impl ReviewRequestClient for RealReviewRequestClient {
125 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
126 detect_remote(&repo_url)
127 }
128
129 fn find_by_source_branch(
130 &self,
131 remote: ForgeRemote,
132 source_branch: String,
133 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
134 match remote.forge_kind {
135 super::ForgeKind::GitHub => {
136 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
137
138 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
139 }
140 super::ForgeKind::GitLab => {
141 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
142
143 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
144 }
145 }
146 }
147
148 fn create_review_request(
149 &self,
150 remote: ForgeRemote,
151 input: CreateReviewRequestInput,
152 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
153 match remote.forge_kind {
154 super::ForgeKind::GitHub => {
155 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
156
157 Box::pin(async move { adapter.create_review_request(remote, input).await })
158 }
159 super::ForgeKind::GitLab => {
160 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
161
162 Box::pin(async move { adapter.create_review_request(remote, input).await })
163 }
164 }
165 }
166
167 fn refresh_review_request(
168 &self,
169 remote: ForgeRemote,
170 display_id: String,
171 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
172 match remote.forge_kind {
173 super::ForgeKind::GitHub => {
174 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
175
176 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
177 }
178 super::ForgeKind::GitLab => {
179 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
180
181 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
182 }
183 }
184 }
185
186 fn sync_review_request_metadata(
187 &self,
188 remote: ForgeRemote,
189 display_id: String,
190 input: UpdateReviewRequestInput,
191 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
192 match remote.forge_kind {
193 super::ForgeKind::GitHub => {
194 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
195
196 Box::pin(async move {
197 adapter
198 .sync_review_request_metadata(remote, display_id, input)
199 .await
200 })
201 }
202 super::ForgeKind::GitLab => {
203 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
204
205 Box::pin(async move {
206 adapter
207 .sync_review_request_metadata(remote, display_id, input)
208 .await
209 })
210 }
211 }
212 }
213
214 fn review_request_web_url(
215 &self,
216 review_request: &ReviewRequestSummary,
217 ) -> Result<String, ReviewRequestError> {
218 if review_request.web_url.trim().is_empty() {
219 return Err(ReviewRequestError::OperationFailed {
220 forge_kind: review_request.forge_kind,
221 message: "review request summary is missing a web URL".to_string(),
222 });
223 }
224
225 Ok(review_request.web_url.clone())
226 }
227
228 fn fetch_review_comment_snapshot(
229 &self,
230 remote: ForgeRemote,
231 display_id: String,
232 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>> {
233 match remote.forge_kind {
234 super::ForgeKind::GitHub => {
235 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
236
237 Box::pin(async move {
238 adapter
239 .fetch_review_comment_snapshot(remote, display_id)
240 .await
241 })
242 }
243 super::ForgeKind::GitLab => {
244 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
245
246 Box::pin(async move {
247 adapter
248 .fetch_review_comment_snapshot(remote, display_id)
249 .await
250 })
251 }
252 }
253 }
254
255 fn list_requested_reviews(
256 &self,
257 remote: ForgeRemote,
258 ) -> ForgeFuture<Result<Vec<RequestedReview>, ReviewRequestError>> {
259 match remote.forge_kind {
260 super::ForgeKind::GitHub => {
261 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
262
263 Box::pin(async move { adapter.list_requested_reviews(remote).await })
264 }
265 super::ForgeKind::GitLab => {
266 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
267
268 Box::pin(async move { adapter.list_requested_reviews(remote).await })
269 }
270 }
271 }
272}
273
274#[cfg(test)]
275mod tests {
276 use super::*;
277 use crate::{ForgeKind, ReviewRequestState};
278
279 #[test]
280 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
281 let client = RealReviewRequestClient::default();
283 let review_request = ReviewRequestSummary {
284 display_id: "#42".to_string(),
285 forge_kind: ForgeKind::GitHub,
286 source_branch: "feature/forge".to_string(),
287 state: ReviewRequestState::Open,
288 status_summary: Some("Mergeable".to_string()),
289 target_branch: "main".to_string(),
290 title: "Add forge boundary".to_string(),
291 web_url: String::new(),
292 };
293
294 let error = client
296 .review_request_web_url(&review_request)
297 .expect_err("missing URL should be rejected");
298
299 assert_eq!(
301 error,
302 ReviewRequestError::OperationFailed {
303 forge_kind: ForgeKind::GitHub,
304 message: "review request summary is missing a web URL".to_string(),
305 }
306 );
307 }
308
309 #[test]
310 fn review_request_web_url_returns_gitlab_url_without_provider_routing() {
311 let client = RealReviewRequestClient::default();
313 let review_request = ReviewRequestSummary {
314 display_id: "!42".to_string(),
315 forge_kind: ForgeKind::GitLab,
316 source_branch: "feature/forge".to_string(),
317 state: ReviewRequestState::Open,
318 status_summary: Some("Draft".to_string()),
319 target_branch: "main".to_string(),
320 title: "Add forge boundary".to_string(),
321 web_url: "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42".to_string(),
322 };
323
324 let web_url = client
326 .review_request_web_url(&review_request)
327 .expect("gitlab review-request URL should be returned directly");
328
329 assert_eq!(
331 web_url,
332 "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42"
333 );
334 }
335}