use std::sync::Arc;
use async_trait::async_trait;
use secrecy::SecretString;
use tokio::io::AsyncRead;
use crate::config::ReleaseSourceConfig;
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub struct Release {
pub name: String,
pub tag: String,
#[serde(default)]
pub body: String,
#[serde(default)]
pub draft: bool,
#[serde(default)]
pub prerelease: bool,
#[serde(with = "time::serde::rfc3339")]
pub created_at: time::OffsetDateTime,
#[serde(default, with = "time::serde::rfc3339::option")]
pub published_at: Option<time::OffsetDateTime>,
#[serde(default)]
pub assets: Vec<ReleaseAsset>,
}
impl Release {
#[must_use]
pub fn new(
name: impl Into<String>,
tag: impl Into<String>,
created_at: time::OffsetDateTime,
) -> Self {
Self {
name: name.into(),
tag: tag.into(),
body: String::new(),
draft: false,
prerelease: false,
created_at,
published_at: None,
assets: Vec::new(),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, serde::Deserialize, serde::Serialize)]
#[non_exhaustive]
pub struct ReleaseAsset {
pub id: String,
pub name: String,
#[serde(default)]
pub size: u64,
#[serde(default)]
pub content_type: Option<String>,
pub download_url: String,
}
impl ReleaseAsset {
#[must_use]
pub fn new(
id: impl Into<String>,
name: impl Into<String>,
download_url: impl Into<String>,
) -> Self {
Self {
id: id.into(),
name: name.into(),
size: 0,
content_type: None,
download_url: download_url.into(),
}
}
}
#[derive(Debug, thiserror::Error, miette::Diagnostic, Clone)]
#[non_exhaustive]
pub enum ProviderError {
#[error("release or asset not found: {what}")]
#[diagnostic(code(rtb::vcs::not_found))]
NotFound {
what: String,
},
#[error("authentication failed for {host}")]
#[diagnostic(
code(rtb::vcs::unauthorized),
help("check the credential registered for this release source")
)]
Unauthorized {
host: String,
},
#[error("rate limited by {host}; retry after {retry_after:?}")]
#[diagnostic(code(rtb::vcs::rate_limited))]
RateLimited {
host: String,
retry_after: Option<std::time::Duration>,
},
#[error("network transport error: {0}")]
#[diagnostic(code(rtb::vcs::transport))]
Transport(String),
#[error("response body could not be parsed: {0}")]
#[diagnostic(code(rtb::vcs::malformed_response))]
MalformedResponse(String),
#[error("operation is not supported by this provider")]
#[diagnostic(
code(rtb::vcs::unsupported),
help("Bitbucket Cloud lacks a native list-releases endpoint; use latest_release or release_by_tag"),
)]
Unsupported,
#[error("provider configuration is invalid: {0}")]
#[diagnostic(code(rtb::vcs::invalid_config))]
InvalidConfig(String),
#[error("I/O error: {0}")]
#[diagnostic(code(rtb::vcs::io))]
Io(#[from] Arc<std::io::Error>),
}
impl From<std::io::Error> for ProviderError {
fn from(err: std::io::Error) -> Self {
Self::Io(Arc::new(err))
}
}
#[async_trait]
pub trait ReleaseProvider: Send + Sync + 'static {
async fn latest_release(&self) -> Result<Release, ProviderError>;
async fn release_by_tag(&self, tag: &str) -> Result<Release, ProviderError>;
async fn list_releases(&self, limit: usize) -> Result<Vec<Release>, ProviderError>;
async fn download_asset(
&self,
asset: &ReleaseAsset,
) -> Result<(Box<dyn AsyncRead + Send + Unpin>, u64), ProviderError>;
}
pub type ProviderFactory = fn(
cfg: &ReleaseSourceConfig,
token: Option<SecretString>,
) -> Result<Arc<dyn ReleaseProvider>, ProviderError>;
#[derive(Clone, Copy)]
pub struct RegisteredProvider {
pub source_type: &'static str,
pub factory: ProviderFactory,
}
impl std::fmt::Debug for RegisteredProvider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("RegisteredProvider")
.field("source_type", &self.source_type)
.finish_non_exhaustive()
}
}
impl ProviderRegistration for RegisteredProvider {
fn source_type(&self) -> &'static str {
self.source_type
}
fn factory(&self) -> ProviderFactory {
self.factory
}
}
pub trait ProviderRegistration: Send + Sync + 'static {
fn source_type(&self) -> &'static str;
fn factory(&self) -> ProviderFactory;
}
#[linkme::distributed_slice]
pub static RELEASE_PROVIDERS: [fn() -> Box<dyn ProviderRegistration>];
#[must_use]
pub fn lookup(source_type: &str) -> Option<ProviderFactory> {
RELEASE_PROVIDERS
.iter()
.map(|make| make())
.find(|r| r.source_type() == source_type)
.map(|r| r.factory())
}
#[must_use]
pub fn registered_types() -> Vec<&'static str> {
let mut out: Vec<&'static str> =
RELEASE_PROVIDERS.iter().map(|make| make().source_type()).collect();
out.sort_unstable();
out.dedup();
out
}