1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeRemote,
7 GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
8 RequestedReview, ReviewCommentSnapshot, ReviewRequestError, ReviewRequestSummary,
9 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 review_request_web_url(
62 &self,
63 review_request: &ReviewRequestSummary,
64 ) -> Result<String, ReviewRequestError>;
65
66 fn fetch_review_comment_snapshot(
76 &self,
77 remote: ForgeRemote,
78 display_id: String,
79 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>>;
80
81 fn list_requested_reviews(
88 &self,
89 remote: ForgeRemote,
90 ) -> ForgeFuture<Result<Vec<RequestedReview>, ReviewRequestError>>;
91}
92
93pub struct RealReviewRequestClient {
95 command_runner: Arc<dyn ForgeCommandRunner>,
96}
97
98impl RealReviewRequestClient {
99 pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
101 Self { command_runner }
102 }
103}
104
105impl Default for RealReviewRequestClient {
106 fn default() -> Self {
107 Self::new(Arc::new(RealForgeCommandRunner))
108 }
109}
110
111impl ReviewRequestClient for RealReviewRequestClient {
112 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
113 detect_remote(&repo_url)
114 }
115
116 fn find_by_source_branch(
117 &self,
118 remote: ForgeRemote,
119 source_branch: String,
120 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
121 match remote.forge_kind {
122 super::ForgeKind::GitHub => {
123 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
124
125 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
126 }
127 super::ForgeKind::GitLab => {
128 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
129
130 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
131 }
132 }
133 }
134
135 fn create_review_request(
136 &self,
137 remote: ForgeRemote,
138 input: CreateReviewRequestInput,
139 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
140 match remote.forge_kind {
141 super::ForgeKind::GitHub => {
142 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
143
144 Box::pin(async move { adapter.create_review_request(remote, input).await })
145 }
146 super::ForgeKind::GitLab => {
147 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
148
149 Box::pin(async move { adapter.create_review_request(remote, input).await })
150 }
151 }
152 }
153
154 fn refresh_review_request(
155 &self,
156 remote: ForgeRemote,
157 display_id: String,
158 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
159 match remote.forge_kind {
160 super::ForgeKind::GitHub => {
161 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
162
163 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
164 }
165 super::ForgeKind::GitLab => {
166 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
167
168 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
169 }
170 }
171 }
172
173 fn review_request_web_url(
174 &self,
175 review_request: &ReviewRequestSummary,
176 ) -> Result<String, ReviewRequestError> {
177 if review_request.web_url.trim().is_empty() {
178 return Err(ReviewRequestError::OperationFailed {
179 forge_kind: review_request.forge_kind,
180 message: "review request summary is missing a web URL".to_string(),
181 });
182 }
183
184 Ok(review_request.web_url.clone())
185 }
186
187 fn fetch_review_comment_snapshot(
188 &self,
189 remote: ForgeRemote,
190 display_id: String,
191 ) -> ForgeFuture<Result<ReviewCommentSnapshot, 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 .fetch_review_comment_snapshot(remote, display_id)
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 .fetch_review_comment_snapshot(remote, display_id)
208 .await
209 })
210 }
211 }
212 }
213
214 fn list_requested_reviews(
215 &self,
216 remote: ForgeRemote,
217 ) -> ForgeFuture<Result<Vec<RequestedReview>, ReviewRequestError>> {
218 match remote.forge_kind {
219 super::ForgeKind::GitHub => {
220 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
221
222 Box::pin(async move { adapter.list_requested_reviews(remote).await })
223 }
224 super::ForgeKind::GitLab => {
225 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
226
227 Box::pin(async move { adapter.list_requested_reviews(remote).await })
228 }
229 }
230 }
231}
232
233#[cfg(test)]
234mod tests {
235 use super::*;
236 use crate::{ForgeKind, ReviewRequestState};
237
238 #[test]
239 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
240 let client = RealReviewRequestClient::default();
242 let review_request = ReviewRequestSummary {
243 display_id: "#42".to_string(),
244 forge_kind: ForgeKind::GitHub,
245 source_branch: "feature/forge".to_string(),
246 state: ReviewRequestState::Open,
247 status_summary: Some("Mergeable".to_string()),
248 target_branch: "main".to_string(),
249 title: "Add forge boundary".to_string(),
250 web_url: String::new(),
251 };
252
253 let error = client
255 .review_request_web_url(&review_request)
256 .expect_err("missing URL should be rejected");
257
258 assert_eq!(
260 error,
261 ReviewRequestError::OperationFailed {
262 forge_kind: ForgeKind::GitHub,
263 message: "review request summary is missing a web URL".to_string(),
264 }
265 );
266 }
267
268 #[test]
269 fn review_request_web_url_returns_gitlab_url_without_provider_routing() {
270 let client = RealReviewRequestClient::default();
272 let review_request = ReviewRequestSummary {
273 display_id: "!42".to_string(),
274 forge_kind: ForgeKind::GitLab,
275 source_branch: "feature/forge".to_string(),
276 state: ReviewRequestState::Open,
277 status_summary: Some("Draft".to_string()),
278 target_branch: "main".to_string(),
279 title: "Add forge boundary".to_string(),
280 web_url: "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42".to_string(),
281 };
282
283 let web_url = client
285 .review_request_web_url(&review_request)
286 .expect("gitlab review-request URL should be returned directly");
287
288 assert_eq!(
290 web_url,
291 "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42"
292 );
293 }
294}