use std::path::Path;
use async_trait::async_trait;
#[derive(Debug, Clone)]
pub struct PullRequest {
pub number: u64,
pub html_url: String,
}
#[derive(Debug, Clone)]
pub struct ExistingRelease {
pub id: String,
pub is_draft: bool,
}
#[async_trait]
pub trait CodeForgeClient: Send + Sync + std::fmt::Debug {
fn forge_name(&self) -> &'static str;
async fn create_release(
&self,
tag_name: &str,
name: &str,
body: &str,
) -> anyhow::Result<String>;
async fn upload_asset(
&self,
release_id: &str,
file_name: &str,
file_path: &Path,
) -> anyhow::Result<()>;
async fn create_pull_request(
&self,
title: &str,
body: &str,
head: &str,
base: &str,
) -> anyhow::Result<String>;
async fn find_open_pull_request(&self, head: &str) -> anyhow::Result<Option<PullRequest>>;
async fn update_pull_request(
&self,
pull_number: u64,
title: &str,
body: &str,
) -> anyhow::Result<String>;
async fn find_release_by_tag(&self, tag: &str) -> anyhow::Result<Option<ExistingRelease>>;
async fn publish_release(&self, release_id: &str) -> anyhow::Result<()>;
}
#[cfg(any(test, feature = "test-support"))]
pub mod test_support {
use std::path::Path;
use std::sync::Mutex;
use anyhow::bail;
use async_trait::async_trait;
use super::{CodeForgeClient, ExistingRelease, PullRequest};
#[derive(Debug, Clone)]
pub enum CodeForgeInvocation {
CreateRelease {
tag_name: String,
name: String,
body: String,
},
UploadAsset {
release_id: String,
file_name: String,
file_path: std::path::PathBuf,
},
CreatePullRequest {
title: String,
body: String,
head: String,
base: String,
},
FindOpenPullRequest {
head: String,
},
UpdatePullRequest {
pull_number: u64,
title: String,
body: String,
},
PublishRelease {
release_id: String,
},
FindReleaseByTag {
tag: String,
},
}
#[derive(Debug)]
pub struct RecordingCodeForgeClient {
invocations: Mutex<Vec<CodeForgeInvocation>>,
release_id: String,
fail_create: bool,
fail_upload: bool,
fail_create_pr: bool,
existing_pr: Option<PullRequest>,
fail_find_pr: bool,
fail_update_pr: bool,
fail_publish_release: bool,
existing_releases: std::collections::HashMap<String, ExistingRelease>,
fail_find_release: bool,
forge_name: &'static str,
}
impl RecordingCodeForgeClient {
pub fn new() -> Self {
Self {
invocations: Mutex::new(Vec::new()),
release_id: "release-1".to_string(),
fail_create: false,
fail_upload: false,
fail_create_pr: false,
existing_pr: None,
fail_find_pr: false,
fail_update_pr: false,
fail_publish_release: false,
existing_releases: std::collections::HashMap::new(),
fail_find_release: false,
forge_name: "GitHub",
}
}
pub fn with_forge_name(mut self, name: &'static str) -> Self {
self.forge_name = name;
self
}
pub fn with_release_id(mut self, id: impl Into<String>) -> Self {
self.release_id = id.into();
self
}
pub fn with_create_failure(mut self) -> Self {
self.fail_create = true;
self
}
pub fn with_upload_failure(mut self) -> Self {
self.fail_upload = true;
self
}
pub fn with_create_pr_failure(mut self) -> Self {
self.fail_create_pr = true;
self
}
pub fn with_existing_pr(mut self, pr: PullRequest) -> Self {
self.existing_pr = Some(pr);
self
}
pub fn with_find_pr_failure(mut self) -> Self {
self.fail_find_pr = true;
self
}
pub fn with_update_pr_failure(mut self) -> Self {
self.fail_update_pr = true;
self
}
pub fn with_publish_release_failure(mut self) -> Self {
self.fail_publish_release = true;
self
}
pub fn with_existing_release(
mut self,
tag: impl Into<String>,
release: ExistingRelease,
) -> Self {
self.existing_releases.insert(tag.into(), release);
self
}
pub fn with_find_release_failure(mut self) -> Self {
self.fail_find_release = true;
self
}
pub fn invocations(&self) -> Vec<CodeForgeInvocation> {
self.invocations.lock().expect("mutex poisoned").clone()
}
}
impl Default for RecordingCodeForgeClient {
fn default() -> Self {
Self::new()
}
}
#[async_trait]
impl CodeForgeClient for RecordingCodeForgeClient {
fn forge_name(&self) -> &'static str {
self.forge_name
}
async fn create_release(
&self,
tag_name: &str,
name: &str,
body: &str,
) -> anyhow::Result<String> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::CreateRelease {
tag_name: tag_name.to_string(),
name: name.to_string(),
body: body.to_string(),
},
);
if self.fail_create {
bail!("simulated create_release failure");
}
Ok(self.release_id.clone())
}
async fn upload_asset(
&self,
release_id: &str,
file_name: &str,
file_path: &Path,
) -> anyhow::Result<()> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::UploadAsset {
release_id: release_id.to_string(),
file_name: file_name.to_string(),
file_path: file_path.to_path_buf(),
},
);
if self.fail_upload {
bail!("simulated upload_asset failure");
}
Ok(())
}
async fn create_pull_request(
&self,
title: &str,
body: &str,
head: &str,
base: &str,
) -> anyhow::Result<String> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::CreatePullRequest {
title: title.to_string(),
body: body.to_string(),
head: head.to_string(),
base: base.to_string(),
},
);
if self.fail_create_pr {
bail!("simulated create_pull_request failure");
}
Ok("https://example.com/pull/1".to_string())
}
async fn find_open_pull_request(&self, head: &str) -> anyhow::Result<Option<PullRequest>> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::FindOpenPullRequest {
head: head.to_string(),
},
);
if self.fail_find_pr {
bail!("simulated find_open_pull_request failure");
}
Ok(self.existing_pr.clone())
}
async fn update_pull_request(
&self,
pull_number: u64,
title: &str,
body: &str,
) -> anyhow::Result<String> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::UpdatePullRequest {
pull_number,
title: title.to_string(),
body: body.to_string(),
},
);
if self.fail_update_pr {
bail!("simulated update_pull_request failure");
}
Ok(format!("https://example.com/pull/{pull_number}"))
}
async fn find_release_by_tag(&self, tag: &str) -> anyhow::Result<Option<ExistingRelease>> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::FindReleaseByTag {
tag: tag.to_string(),
},
);
if self.fail_find_release {
bail!("simulated find_release_by_tag failure");
}
Ok(self.existing_releases.get(tag).cloned())
}
async fn publish_release(&self, release_id: &str) -> anyhow::Result<()> {
self.invocations.lock().expect("mutex poisoned").push(
CodeForgeInvocation::PublishRelease {
release_id: release_id.to_string(),
},
);
if self.fail_publish_release {
bail!("simulated publish_release failure");
}
Ok(())
}
}
}