use chrono::{DateTime, Utc};
use serde::{Deserialize, Serialize};
use super::version::SemanticVersion;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Release {
pub crate_name: String,
pub version: SemanticVersion,
pub released_at: DateTime<Utc>,
pub tag: Option<String>,
pub commit_hash: Option<String>,
pub notes: Option<String>,
pub published: bool,
pub is_prerelease: bool,
pub downloads: Option<u64>,
}
impl Release {
#[must_use]
pub const fn new(
crate_name: String,
version: SemanticVersion,
released_at: DateTime<Utc>,
) -> Self {
let is_prerelease = version.is_prerelease;
Self {
crate_name,
version,
released_at,
tag: None,
commit_hash: None,
notes: None,
published: false,
is_prerelease,
downloads: None,
}
}
#[must_use]
pub const fn version(&self) -> &SemanticVersion {
&self.version
}
#[must_use]
pub const fn is_stable(&self) -> bool {
!self.is_prerelease
}
#[must_use]
pub fn tag_name(&self) -> String {
self.tag
.clone()
.unwrap_or_else(|| format!("v{}", self.version))
}
pub const fn mark_published(&mut self) {
self.published = true;
}
#[must_use]
pub fn with_tag(mut self, tag: String) -> Self {
self.tag = Some(tag);
self
}
#[must_use]
pub fn with_commit_hash(mut self, hash: String) -> Self {
self.commit_hash = Some(hash);
self
}
#[must_use]
pub fn with_notes(mut self, notes: String) -> Self {
self.notes = Some(notes);
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ReleaseStatus {
Planned,
InProgress,
Completed,
Failed,
RolledBack,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ReleasePlan {
pub version: SemanticVersion,
pub crates: Vec<CrateReleasePlan>,
pub status: ReleaseStatus,
pub created_at: DateTime<Utc>,
pub release_notes: Option<String>,
}
impl ReleasePlan {
#[must_use]
pub fn new(version: SemanticVersion) -> Self {
Self {
version,
crates: Vec::new(),
status: ReleaseStatus::Planned,
created_at: Utc::now(),
release_notes: None,
}
}
pub fn add_crate(&mut self, plan: CrateReleasePlan) {
self.crates.push(plan);
}
#[must_use]
pub fn publishable_crates(&self) -> Vec<&CrateReleasePlan> {
self.crates.iter().filter(|c| c.should_publish).collect()
}
#[must_use]
pub fn publish_order(&self) -> Vec<&CrateReleasePlan> {
let mut crates = self.publishable_crates();
crates.sort_by_key(|c| c.publish_order);
crates
}
#[must_use]
pub fn all_published(&self) -> bool {
self.publishable_crates().iter().all(|c| c.published)
}
pub const fn mark_in_progress(&mut self) {
self.status = ReleaseStatus::InProgress;
}
pub const fn mark_completed(&mut self) {
self.status = ReleaseStatus::Completed;
}
pub const fn mark_failed(&mut self) {
self.status = ReleaseStatus::Failed;
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct CrateReleasePlan {
pub name: String,
pub current_version: SemanticVersion,
pub target_version: SemanticVersion,
pub should_publish: bool,
pub published: bool,
pub publish_order: usize,
pub dependencies: Vec<String>,
pub estimated_time_secs: u64,
}
impl CrateReleasePlan {
#[must_use]
pub const fn new(
name: String,
current_version: SemanticVersion,
target_version: SemanticVersion,
) -> Self {
Self {
name,
current_version,
target_version,
should_publish: false,
published: false,
publish_order: 0,
dependencies: Vec::new(),
estimated_time_secs: 45,
}
}
#[must_use]
pub const fn with_publish(mut self) -> Self {
self.should_publish = true;
self
}
#[must_use]
pub const fn with_order(mut self, order: usize) -> Self {
self.publish_order = order;
self
}
#[must_use]
pub fn with_dependency(mut self, dep: String) -> Self {
self.dependencies.push(dep);
self
}
pub const fn mark_published(&mut self) {
self.published = true;
}
#[must_use]
pub fn crates_io_url(&self) -> String {
format!(
"https://crates.io/crates/{}/{}",
self.name, self.target_version
)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_release_creation() {
let version = SemanticVersion::parse("1.0.0").unwrap();
let release = Release::new("test-crate".to_string(), version.clone(), Utc::now());
assert_eq!(release.crate_name, "test-crate");
assert_eq!(release.version(), &version);
assert!(release.is_stable());
assert!(!release.published);
}
#[test]
fn test_release_plan() {
let version = SemanticVersion::parse("1.0.0").unwrap();
let mut plan = ReleasePlan::new(version.clone());
assert_eq!(plan.status, ReleaseStatus::Planned);
let crate_plan = CrateReleasePlan::new(
"crate1".to_string(),
SemanticVersion::parse("0.1.0").unwrap(),
version,
)
.with_publish()
.with_order(1);
plan.add_crate(crate_plan);
assert_eq!(plan.publishable_crates().len(), 1);
}
#[test]
fn test_prerelease() {
let version = SemanticVersion::parse("1.0.0-beta.1").unwrap();
let release = Release::new("test-crate".to_string(), version, Utc::now());
assert!(release.is_prerelease);
assert!(!release.is_stable());
}
#[test]
fn test_tag_name() {
let version = SemanticVersion::parse("1.2.3").unwrap();
let release = Release::new("test-crate".to_string(), version, Utc::now());
assert_eq!(release.tag_name(), "v1.2.3");
}
}