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 => Box::pin(async move {
191 Err(ReviewRequestError::OperationFailed {
192 forge_kind: remote.forge_kind,
193 message: "GitLab review-comment preview is not yet supported".to_string(),
194 })
195 }),
196 }
197 }
198}
199
200#[cfg(test)]
201mod tests {
202 use super::*;
203 use crate::{ForgeKind, ReviewRequestState};
204
205 #[test]
206 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
207 let client = RealReviewRequestClient::default();
209 let review_request = ReviewRequestSummary {
210 display_id: "#42".to_string(),
211 forge_kind: ForgeKind::GitHub,
212 source_branch: "feature/forge".to_string(),
213 state: ReviewRequestState::Open,
214 status_summary: Some("Mergeable".to_string()),
215 target_branch: "main".to_string(),
216 title: "Add forge boundary".to_string(),
217 web_url: String::new(),
218 };
219
220 let error = client
222 .review_request_web_url(&review_request)
223 .expect_err("missing URL should be rejected");
224
225 assert_eq!(
227 error,
228 ReviewRequestError::OperationFailed {
229 forge_kind: ForgeKind::GitHub,
230 message: "review request summary is missing a web URL".to_string(),
231 }
232 );
233 }
234
235 #[test]
236 fn review_request_web_url_returns_gitlab_url_without_provider_routing() {
237 let client = RealReviewRequestClient::default();
239 let review_request = ReviewRequestSummary {
240 display_id: "!42".to_string(),
241 forge_kind: ForgeKind::GitLab,
242 source_branch: "feature/forge".to_string(),
243 state: ReviewRequestState::Open,
244 status_summary: Some("Draft".to_string()),
245 target_branch: "main".to_string(),
246 title: "Add forge boundary".to_string(),
247 web_url: "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42".to_string(),
248 };
249
250 let web_url = client
252 .review_request_web_url(&review_request)
253 .expect("gitlab review-request URL should be returned directly");
254
255 assert_eq!(
257 web_url,
258 "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42"
259 );
260 }
261}