1use std::sync::Arc;
4
5use super::{
6 CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeRemote,
7 GitHubReviewRequestAdapter, RealForgeCommandRunner, ReviewRequestError, ReviewRequestSummary,
8 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 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
95
96 Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
97 }
98
99 fn create_review_request(
100 &self,
101 remote: ForgeRemote,
102 input: CreateReviewRequestInput,
103 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
104 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
105
106 Box::pin(async move { adapter.create_review_request(remote, input).await })
107 }
108
109 fn refresh_review_request(
110 &self,
111 remote: ForgeRemote,
112 display_id: String,
113 ) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
114 let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
115
116 Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
117 }
118
119 fn review_request_web_url(
120 &self,
121 review_request: &ReviewRequestSummary,
122 ) -> Result<String, ReviewRequestError> {
123 if review_request.web_url.trim().is_empty() {
124 return Err(ReviewRequestError::OperationFailed {
125 forge_kind: review_request.forge_kind,
126 message: "review request summary is missing a web URL".to_string(),
127 });
128 }
129
130 Ok(review_request.web_url.clone())
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137 use crate::{ForgeKind, ReviewRequestState};
138
139 #[test]
140 fn review_request_web_url_returns_error_when_summary_is_missing_url() {
141 let client = RealReviewRequestClient::default();
143 let review_request = ReviewRequestSummary {
144 display_id: "#42".to_string(),
145 forge_kind: ForgeKind::GitHub,
146 source_branch: "feature/forge".to_string(),
147 state: ReviewRequestState::Open,
148 status_summary: Some("Mergeable".to_string()),
149 target_branch: "main".to_string(),
150 title: "Add forge boundary".to_string(),
151 web_url: String::new(),
152 };
153
154 let error = client
156 .review_request_web_url(&review_request)
157 .expect_err("missing URL should be rejected");
158
159 assert_eq!(
161 error,
162 ReviewRequestError::OperationFailed {
163 forge_kind: ForgeKind::GitHub,
164 message: "review request summary is missing a web URL".to_string(),
165 }
166 );
167 }
168}