use std::sync::Arc;
use super::{
CreateReviewRequestInput, ForgeCommandRunner, ForgeFuture, ForgeRemote,
GitHubReviewRequestAdapter, GitLabReviewRequestAdapter, RealForgeCommandRunner,
ReviewRequestError, ReviewRequestSummary, detect_remote,
};
#[cfg_attr(any(test, feature = "test-utils"), mockall::automock)]
pub trait ReviewRequestClient: Send + Sync {
fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError>;
fn find_by_source_branch(
&self,
remote: ForgeRemote,
source_branch: String,
) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>>;
fn create_review_request(
&self,
remote: ForgeRemote,
input: CreateReviewRequestInput,
) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
fn refresh_review_request(
&self,
remote: ForgeRemote,
display_id: String,
) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>>;
fn review_request_web_url(
&self,
review_request: &ReviewRequestSummary,
) -> Result<String, ReviewRequestError>;
}
pub struct RealReviewRequestClient {
command_runner: Arc<dyn ForgeCommandRunner>,
}
impl RealReviewRequestClient {
pub(crate) fn new(command_runner: Arc<dyn ForgeCommandRunner>) -> Self {
Self { command_runner }
}
}
impl Default for RealReviewRequestClient {
fn default() -> Self {
Self::new(Arc::new(RealForgeCommandRunner))
}
}
impl ReviewRequestClient for RealReviewRequestClient {
fn detect_remote(&self, repo_url: String) -> Result<ForgeRemote, ReviewRequestError> {
detect_remote(&repo_url)
}
fn find_by_source_branch(
&self,
remote: ForgeRemote,
source_branch: String,
) -> ForgeFuture<Result<Option<ReviewRequestSummary>, ReviewRequestError>> {
match remote.forge_kind {
super::ForgeKind::GitHub => {
let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
}
super::ForgeKind::GitLab => {
let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.find_by_source_branch(remote, source_branch).await })
}
}
}
fn create_review_request(
&self,
remote: ForgeRemote,
input: CreateReviewRequestInput,
) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
match remote.forge_kind {
super::ForgeKind::GitHub => {
let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.create_review_request(remote, input).await })
}
super::ForgeKind::GitLab => {
let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.create_review_request(remote, input).await })
}
}
}
fn refresh_review_request(
&self,
remote: ForgeRemote,
display_id: String,
) -> ForgeFuture<Result<ReviewRequestSummary, ReviewRequestError>> {
match remote.forge_kind {
super::ForgeKind::GitHub => {
let adapter = GitHubReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
}
super::ForgeKind::GitLab => {
let adapter = GitLabReviewRequestAdapter::new(Arc::clone(&self.command_runner));
Box::pin(async move { adapter.refresh_review_request(remote, display_id).await })
}
}
}
fn review_request_web_url(
&self,
review_request: &ReviewRequestSummary,
) -> Result<String, ReviewRequestError> {
if review_request.web_url.trim().is_empty() {
return Err(ReviewRequestError::OperationFailed {
forge_kind: review_request.forge_kind,
message: "review request summary is missing a web URL".to_string(),
});
}
Ok(review_request.web_url.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{ForgeKind, ReviewRequestState};
#[test]
fn review_request_web_url_returns_error_when_summary_is_missing_url() {
let client = RealReviewRequestClient::default();
let review_request = ReviewRequestSummary {
display_id: "#42".to_string(),
forge_kind: ForgeKind::GitHub,
source_branch: "feature/forge".to_string(),
state: ReviewRequestState::Open,
status_summary: Some("Mergeable".to_string()),
target_branch: "main".to_string(),
title: "Add forge boundary".to_string(),
web_url: String::new(),
};
let error = client
.review_request_web_url(&review_request)
.expect_err("missing URL should be rejected");
assert_eq!(
error,
ReviewRequestError::OperationFailed {
forge_kind: ForgeKind::GitHub,
message: "review request summary is missing a web URL".to_string(),
}
);
}
#[test]
fn review_request_web_url_returns_gitlab_url_without_provider_routing() {
let client = RealReviewRequestClient::default();
let review_request = ReviewRequestSummary {
display_id: "!42".to_string(),
forge_kind: ForgeKind::GitLab,
source_branch: "feature/forge".to_string(),
state: ReviewRequestState::Open,
status_summary: Some("Draft".to_string()),
target_branch: "main".to_string(),
title: "Add forge boundary".to_string(),
web_url: "https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42".to_string(),
};
let web_url = client
.review_request_web_url(&review_request)
.expect("gitlab review-request URL should be returned directly");
assert_eq!(
web_url,
"https://gitlab.com/agentty-xyz/agentty/-/merge_requests/42"
);
}
}