1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeKind, ForgeRemote,
7 GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
8 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
66pub struct RealReviewRequestClient {
68 command_runner: Arc<dyn ForgeCommandRunner>,
69}
70
71impl RealReviewRequestClient {
72 pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
74 Self { command_runner }
75 }
76}
77
78impl Default for RealReviewRequestClient {
79 fn default() -> Self {
80 Self::new(Arc::new(RealForgeCommandRunner))
81 }
82}
83
84impl ReviewRequestClient for RealReviewRequestClient {
85 fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
86 detect_remote(&repo_url)
87 }
88
89 fn find_by_source_branch(
90 &self,
91 remote: ForgeRemote,
92 source_branch: String,
93 ) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
94 match remote.forge_kind {
95 ForgeKind::GitHub => {
96 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
97
98 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
99 }
100 ForgeKind::GitLab => {
101 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
102
103 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
104 }
105 }
106 }
107
108 fn create_review_request(
109 &self,
110 remote: ForgeRemote,
111 input: CreateReviewRequestInput,
112 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
113 match remote.forge_kind {
114 ForgeKind::GitHub => {
115 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
116
117 Box::pin(async move { adapter.create_review_request(remote, input).await })
118 }
119 ForgeKind::GitLab => {
120 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
121
122 Box::pin(async move { adapter.create_review_request(remote, input).await })
123 }
124 }
125 }
126
127 fn refresh_review_request(
128 &self,
129 remote: ForgeRemote,
130 display_id: String,
131 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
132 match remote.forge_kind {
133 ForgeKind::GitHub => {
134 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
135
136 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
137 }
138 ForgeKind::GitLab => {
139 let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
140
141 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
142 }
143 }
144 }
145
146 fn review_request_web_url(
147 &self,
148 review_request: &ReviewRequestSummary,
149 ) -> Result<String, ReviewRequestError> {
150 if review_request.web_url.trim().is_empty() {
151 return Err(ReviewRequestError::OperationFailed {
152 forge_kind: review_request.forge_kind,
153 message: "review request summary is missing a web URL".to_string(),
154 });
155 }
156
157 Ok(review_request.web_url.clone())
158 }
159}
160
161#[cfg(test)]
162mod tests {
163 use super::*;
164 use crate::{ForgeKind, ReviewRequestState};
165
166 #[test]
167 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
168 let client = RealReviewRequestClient::default();
170 let review_request = ReviewRequestSummary {
171 display_id: "#42".to_string(),
172 forge_kind: ForgeKind::GitHub,
173 source_branch: "feature/forge".to_string(),
174 state: ReviewRequestState::Open,
175 status_summary: Some("Mergeable".to_string()),
176 target_branch: "main".to_string(),
177 title: "Add forge boundary".to_string(),
178 web_url: String::new(),
179 };
180
181 let error = client
183 .review_request_web_url(&review_request)
184 .expect_err("missing URL should be rejected");
185
186 assert_eq!(
188 error,
189 ReviewRequestError::OperationFailed {
190 forge_kind: ForgeKind::GitHub,
191 message: "review request summary is missing a web URL".to_string(),
192 }
193 );
194 }
195}