1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeKind, ForgeRemote,
7 GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
8 ReviewCommentSnapshot, ReviewRequestError, ReviewRequestMetadata, 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 review_request_metadata(
62 &self,
63 remote: ForgeRemote,
64 display_id: String,
65 ) -> ForgeFuture<Result<ReviewRequestMetadata, ReviewRequestError>>;
66
67 fn sync_review_request_metadata(
77 &self,
78 remote: ForgeRemote,
79 display_id: String,
80 input: UpdateReviewRequestInput,
81 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
82
83 fn review_request_web_url(
89 &self,
90 review_request: &ReviewRequestSummary,
91 ) -> Result<String, ReviewRequestError>;
92
93 fn fetch_review_comment_snapshot(
103 &self,
104 remote: ForgeRemote,
105 display_id: String,
106 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>>;
107
108 fn reply_to_thread(
114 &self,
115 remote: ForgeRemote,
116 display_id: String,
117 thread_id: String,
118 body: String,
119 ) -> ForgeFuture<Result<(), ReviewRequestError>>;
120
121 fn resolve_thread(
127 &self,
128 remote: ForgeRemote,
129 display_id: String,
130 thread_id: String,
131 ) -> ForgeFuture<Result<(), ReviewRequestError>>;
132}
133
134pub struct RealReviewRequestClient {
136 command_runner: Arc<dyn ForgeCommandRunner>,
137}
138
139impl RealReviewRequestClient {
140 pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
142 Self { command_runner }
143 }
144
145 fn call_with_authenticated_adapter<T>(
147 &self,
148 remote: ForgeRemote,
149 call: impl FnOnce(
150 Arc<dyn ReviewRequestAdapter>,
151 ForgeRemote,
152 ) -> ForgeFuture<Result<T, ReviewRequestError>>
153 + Send
154 + 'static,
155 ) -> ForgeFuture<Result<T, ReviewRequestError>>
156 where
157 T: Send + 'static,
158 {
159 let adapter = self.adapter_for(remote.forge_kind);
160
161 Box::pin(async move {
162 adapter.ensure_authenticated(&remote).await?;
163
164 call(adapter, remote).await
165 })
166 }
167
168 fn adapter_for(&self, forge_kind: ForgeKind) -> Arc<dyn ReviewRequestAdapter> {
170 match forge_kind {
171 ForgeKind::GitHub => Arc::new(GitHubReviewRequestAdapter::new(Arc::clone(
172 &self.command_runner,
173 ))),
174 ForgeKind::GitLab => Arc::new(GitLabReviewRequestAdapter::new(Arc::clone(
175 &self.command_runner,
176 ))),
177 }
178 }
179}
180
181impl Default for RealReviewRequestClient {
182 fn default() -> Self {
183 Self::new(Arc::new(RealForgeCommandRunner))
184 }
185}
186
187impl ReviewRequestClient for RealReviewRequestClient {
188 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
189 detect_remote(&repo_url)
190 }
191
192 fn find_by_source_branch(
193 &self,
194 remote: ForgeRemote,
195 source_branch: String,
196 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
197 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
198 adapter.find_authenticated_by_source_branch(remote, source_branch)
199 })
200 }
201
202 fn create_review_request(
203 &self,
204 remote: ForgeRemote,
205 input: CreateReviewRequestInput,
206 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
207 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
208 adapter.create_authenticated_review_request(remote, input)
209 })
210 }
211
212 fn refresh_review_request(
213 &self,
214 remote: ForgeRemote,
215 display_id: String,
216 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
217 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
218 adapter.refresh_authenticated_review_request(remote, display_id)
219 })
220 }
221
222 fn review_request_metadata(
223 &self,
224 remote: ForgeRemote,
225 display_id: String,
226 ) -> ForgeFuture<Result<ReviewRequestMetadata, ReviewRequestError>> {
227 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
228 adapter.authenticated_review_request_metadata(remote, display_id)
229 })
230 }
231
232 fn sync_review_request_metadata(
233 &self,
234 remote: ForgeRemote,
235 display_id: String,
236 input: UpdateReviewRequestInput,
237 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
238 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
239 adapter.sync_authenticated_review_request_metadata(remote, display_id, input)
240 })
241 }
242
243 fn review_request_web_url(
244 &self,
245 review_request: &ReviewRequestSummary,
246 ) -> Result<String, ReviewRequestError> {
247 if review_request.web_url.trim().is_empty() {
248 return Err(ReviewRequestError::OperationFailed {
249 forge_kind: review_request.forge_kind,
250 message: "review request summary is missing a web URL".to_string(),
251 });
252 }
253
254 Ok(review_request.web_url.clone())
255 }
256
257 fn fetch_review_comment_snapshot(
258 &self,
259 remote: ForgeRemote,
260 display_id: String,
261 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>> {
262 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
263 adapter.fetch_authenticated_review_comment_snapshot(remote, display_id)
264 })
265 }
266
267 fn reply_to_thread(
268 &self,
269 remote: ForgeRemote,
270 display_id: String,
271 thread_id: String,
272 body: String,
273 ) -> ForgeFuture<Result<(), ReviewRequestError>> {
274 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
275 adapter.reply_to_authenticated_thread(remote, display_id, thread_id, body)
276 })
277 }
278
279 fn resolve_thread(
280 &self,
281 remote: ForgeRemote,
282 display_id: String,
283 thread_id: String,
284 ) -> ForgeFuture<Result<(), ReviewRequestError>> {
285 self.call_with_authenticated_adapter(remote, move |adapter, remote| {
286 adapter.resolve_authenticated_thread(remote, display_id, thread_id)
287 })
288 }
289}
290
291pub(crate) trait ReviewRequestAdapter: Send + Sync {
298 fn ensure_authenticated(
304 &self,
305 remote: &ForgeRemote,
306 ) -> ForgeFuture<Result<(), ReviewRequestError>>;
307
308 fn find_authenticated_by_source_branch(
310 &self,
311 remote: ForgeRemote,
312 source_branch: String,
313 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>>;
314
315 fn create_authenticated_review_request(
318 &self,
319 remote: ForgeRemote,
320 input: CreateReviewRequestInput,
321 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
322
323 fn refresh_authenticated_review_request(
325 &self,
326 remote: ForgeRemote,
327 display_id: String,
328 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
329
330 fn authenticated_review_request_metadata(
332 &self,
333 remote: ForgeRemote,
334 display_id: String,
335 ) -> ForgeFuture<Result<ReviewRequestMetadata, ReviewRequestError>>;
336
337 fn sync_authenticated_review_request_metadata(
339 &self,
340 remote: ForgeRemote,
341 display_id: String,
342 input: UpdateReviewRequestInput,
343 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
344
345 fn fetch_authenticated_review_comment_snapshot(
347 &self,
348 remote: ForgeRemote,
349 display_id: String,
350 ) -> ForgeFuture<Result<ReviewCommentSnapshot, ReviewRequestError>>;
351
352 fn reply_to_authenticated_thread(
354 &self,
355 remote: ForgeRemote,
356 display_id: String,
357 thread_id: String,
358 body: String,
359 ) -> ForgeFuture<Result<(), ReviewRequestError>>;
360
361 fn resolve_authenticated_thread(
363 &self,
364 remote: ForgeRemote,
365 display_id: String,
366 thread_id: String,
367 ) -> ForgeFuture<Result<(), ReviewRequestError>>;
368}
369
370#[cfg(test)]
371#[path = "client_test.rs"]
372mod tests;