1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeRemote,
7 GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
8 ReviewCommentSnapshot, ReviewRequestError, ReviewRequestSummary, detect_remote,
9};
10
11#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
16pub trait ReviewRequestClient: Send + Sync {
17 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError>;
23
24 fn find_by_source_branch(
30 &self,
31 remote: ForgeRemote,
32 source_branch: String,
33 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>>;
34
35 fn create_review_request(
40 &self,
41 remote: ForgeRemote,
42 input: CreateReviewRequestInput,
43 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
44
45 fn refresh_review_request(
50 &self,
51 remote: ForgeRemote,
52 display_id: String,
53 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
54
55 fn review_request_web_url(
61 &self,
62 review_request: &ReviewRequestSummary,
63 ) -> Result<String, ReviewRequestError>;
64
65 fn fetch_review_comment_snapshot(
75 &self,
76 remote: ForgeRemote,
77 display_id: String,
78 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>>;
79}
80
81pub struct RealReviewRequestClient {
83 command_runner: Arc<dyn ForgeCommandRunner>,
84}
85
86impl RealReviewRequestClient {
87 pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
89 Self { command_runner }
90 }
91}
92
93impl Default for RealReviewRequestClient {
94 fn default() -> Self {
95 Self::new(Arc::new(RealForgeCommandRunner))
96 }
97}
98
99impl ReviewRequestClient for RealReviewRequestClient {
100 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
101 detect_remote(&repo_url)
102 }
103
104 fn find_by_source_branch(
105 &self,
106 remote: ForgeRemote,
107 source_branch: String,
108 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
109 match remote.forge_kind {
110 super::ForgeKind::GitHub => {
111 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
112
113 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
114 }
115 super::ForgeKind::GitLab => {
116 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
117
118 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
119 }
120 }
121 }
122
123 fn create_review_request(
124 &self,
125 remote: ForgeRemote,
126 input: CreateReviewRequestInput,
127 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
128 match remote.forge_kind {
129 super::ForgeKind::GitHub => {
130 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
131
132 Box::pin(async move { adapter.create_review_request(remote, input).await })
133 }
134 super::ForgeKind::GitLab => {
135 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
136
137 Box::pin(async move { adapter.create_review_request(remote, input).await })
138 }
139 }
140 }
141
142 fn refresh_review_request(
143 &self,
144 remote: ForgeRemote,
145 display_id: String,
146 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
147 match remote.forge_kind {
148 super::ForgeKind::GitHub => {
149 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
150
151 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
152 }
153 super::ForgeKind::GitLab => {
154 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
155
156 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
157 }
158 }
159 }
160
161 fn review_request_web_url(
162 &self,
163 review_request: &ReviewRequestSummary,
164 ) -> Result<String, ReviewRequestError> {
165 if review_request.web_url.trim().is_empty() {
166 return Err(ReviewRequestError::OperationFailed {
167 forge_kind: review_request.forge_kind,
168 message: "review request summary is missing a web URL".to_string(),
169 });
170 }
171
172 Ok(review_request.web_url.clone())
173 }
174
175 fn fetch_review_comment_snapshot(
176 &self,
177 remote: ForgeRemote,
178 display_id: String,
179 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>> {
180 match remote.forge_kind {
181 super::ForgeKind::GitHub => {
182 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
183
184 Box::pin(async move {
185 adapter
186 .fetch_review_comment_snapshot(remote, display_id)
187 .await
188 })
189 }
190 super::ForgeKind::GitLab => {
191 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
192
193 Box::pin(async move {
194 adapter
195 .fetch_review_comment_snapshot(remote, display_id)
196 .await
197 })
198 }
199 }
200 }
201}
202
203#[cfg(test)]
204mod tests {
205 use super::*;
206 use crate::{ForgeKind, ReviewRequestState};
207
208 #[test]
209 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
210 let client = RealReviewRequestClient::default();
212 let review_request = ReviewRequestSummary {
213 display_id: "#42".to_string(),
214 forge_kind: ForgeKind::GitHub,
215 source_branch: "feature/forge".to_string(),
216 state: ReviewRequestState::Open,
217 status_summary: Some("Mergeable".to_string()),
218 target_branch: "main".to_string(),
219 title: "Add forge boundary".to_string(),
220 web_url: String::new(),
221 };
222
223 let error = client
225 .review_request_web_url(&review_request)
226 .expect_err("missing URL should be rejected");
227
228 assert_eq!(
230 error,
231 ReviewRequestError::OperationFailed {
232 forge_kind: ForgeKind::GitHub,
233 message: "review request summary is missing a web URL".to_string(),
234 }
235 );
236 }
237
238 #[test]
239 fn review_request_web_url_returns_gitlab_url_without_provider_routing() {
240 let client = RealReviewRequestClient::default();
242 let review_request = ReviewRequestSummary {
243 display_id: "!42".to_string(),
244 forge_kind: ForgeKind::GitLab,
245 source_branch: "feature/forge".to_string(),
246 state: ReviewRequestState::Open,
247 status_summary: Some("Draft".to_string()),
248 target_branch: "main".to_string(),
249 title: "Add forge boundary".to_string(),
250 web_url: "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42".to_string(),
251 };
252
253 let web_url = client
255 .review_request_web_url(&review_request)
256 .expect("gitlab review-request URL should be returned directly");
257
258 assert_eq!(
260 web_url,
261 "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42"
262 );
263 }
264}