use chrono::{DateTime, Utc};
use clap::{Error, error::ErrorKind};
pub const DEFAULT_FORMAT: &str = "%Y.%m.%d";
pub const DEFAULT_SEPARATOR: &str = "-";
#[derive(Debug, Clone, PartialEq)]
pub struct Version {
pub date: DateTime<Utc>,
pub format: String,
pub separator: String,
pub patch: u16,
}
impl Version {
pub fn new(format: Option<String>, separator: Option<String>, patch: Option<u16>) -> Self {
Version {
date: Utc::now(),
format: format.unwrap_or_else(|| DEFAULT_FORMAT.to_string()),
separator: separator.unwrap_or_else(|| DEFAULT_SEPARATOR.to_string()),
patch: patch.unwrap_or(0),
}
}
pub fn set_patch_from_last(&mut self, last: &str) -> Result<(), Error> {
let formatted_date = self.date.format(&self.format).to_string();
if last.len() <= formatted_date.len() {
return Ok(()); }
let last_split = &last[..formatted_date.len()];
if last_split != formatted_date {
return Ok(()); }
let suffix = &last[formatted_date.len()..];
if let Some(patch_str) = suffix.strip_prefix(&self.separator)
&& let Ok(patch_num) = patch_str.parse::<u16>()
{
if patch_num == u16::MAX {
return Err(Error::raw(
ErrorKind::ValueValidation,
format!(
"The patch calculated exceeds the maximum allowed value ({})",
u16::MAX
),
));
}
self.patch = patch_num + 1;
}
Ok(())
}
pub fn generate(&self) -> String {
format!(
"{}{}{}",
self.date.format(&self.format),
self.separator,
self.patch
)
}
pub fn get_prefix_without_patch(&self) -> String {
format!("{}{}", self.date.format(&self.format), self.separator)
}
}
#[cfg(test)]
mod tests {
use super::*;
use chrono::{TimeZone, Utc};
fn create_version_with_date(year: i32, month: u32, day: u32) -> Version {
let mut version = Version::new(None, None, None);
version.date = Utc.with_ymd_and_hms(year, month, day, 0, 0, 0).unwrap();
version
}
#[test]
fn test_new_with_all_defaults() {
let version = Version::new(None, None, None);
assert_eq!(version.format, DEFAULT_FORMAT);
assert_eq!(version.separator, DEFAULT_SEPARATOR);
assert_eq!(version.patch, 0);
let now = Utc::now();
let diff = (now - version.date).num_seconds().abs();
assert!(diff < 2, "Date should be within 2 seconds of now");
}
#[test]
fn test_new_with_custom_format() {
let custom_format = "%Y-%m-%d %H:%M:%S".to_string();
let version = Version::new(Some(custom_format.clone()), None, None);
assert_eq!(version.format, custom_format);
assert_eq!(version.separator, DEFAULT_SEPARATOR);
assert_eq!(version.patch, 0);
}
#[test]
fn test_new_with_custom_separator() {
let custom_separator = "_".to_string();
let version = Version::new(None, Some(custom_separator.clone()), None);
assert_eq!(version.format, DEFAULT_FORMAT);
assert_eq!(version.separator, custom_separator);
assert_eq!(version.patch, 0);
}
#[test]
fn test_new_with_custom_patch() {
let patch_number = 42;
let version = Version::new(None, None, Some(patch_number));
assert_eq!(version.format, DEFAULT_FORMAT);
assert_eq!(version.separator, DEFAULT_SEPARATOR);
assert_eq!(version.patch, patch_number);
}
#[test]
fn test_new_with_all_custom_values() {
let custom_format = "%Y.%m.%d".to_string();
let custom_separator = "+".to_string();
let patch_number = 123;
let version = Version::new(
Some(custom_format.clone()),
Some(custom_separator.clone()),
Some(patch_number),
);
assert_eq!(version.format, custom_format);
assert_eq!(version.separator, custom_separator);
assert_eq!(version.patch, patch_number);
}
#[test]
fn test_set_patch_from_last_matching_date_with_patch() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15-5";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 6);
}
#[test]
fn test_set_patch_from_last_matching_date_zero_patch() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15-0";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 1);
}
#[test]
fn test_set_patch_from_last_different_date() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.14-5";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_no_separator() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_empty_string() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_shorter_than_date() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_invalid_patch_number() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15-abc";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_max_patch_overflow() {
let mut version = create_version_with_date(2025, 9, 15);
let last = format!("2025.09.15-{}", u16::MAX);
let result = version.set_patch_from_last(&last);
assert!(result.is_err());
if let Err(error) = result {
assert_eq!(error.kind(), ErrorKind::ValueValidation);
assert!(
error
.to_string()
.contains("exceeds the maximum allowed value")
);
}
}
#[test]
fn test_set_patch_from_last_near_max_patch() {
let mut version = create_version_with_date(2025, 9, 15);
let last = format!("2025.09.15-{}", u16::MAX - 1);
let result = version.set_patch_from_last(&last);
assert!(result.is_ok());
assert_eq!(version.patch, u16::MAX);
}
#[test]
fn test_set_patch_from_last_custom_format() {
let mut version = Version::new(Some("%d/%m/%Y".to_string()), None, None);
version.date = Utc.with_ymd_and_hms(2025, 9, 15, 0, 0, 0).unwrap();
let last = "15/09/2025-3";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 4);
}
#[test]
fn test_set_patch_from_last_custom_separator() {
let mut version = Version::new(None, Some("_".to_string()), None);
version.date = Utc.with_ymd_and_hms(2025, 9, 15, 0, 0, 0).unwrap();
let last = "2025.09.15_7";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 8);
}
#[test]
fn test_set_patch_from_last_wrong_separator() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15_5";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_multiple_separators() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15-5-extra";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_negative_patch() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15--5";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 0); }
#[test]
fn test_set_patch_from_last_large_valid_patch() {
let mut version = create_version_with_date(2025, 9, 15);
let last = "2025.09.15-1000";
let result = version.set_patch_from_last(last);
assert!(result.is_ok());
assert_eq!(version.patch, 1001);
}
#[test]
fn test_generate_with_defaults() {
let version = Version::new(None, None, None);
let generated = version.generate();
let expected_date_part = version.date.format(DEFAULT_FORMAT).to_string();
let expected = format!("{}{}{}", expected_date_part, DEFAULT_SEPARATOR, 0);
assert_eq!(generated, expected);
}
#[test]
fn test_generate_with_custom_values() {
let custom_format = "%Y%m%d";
let custom_separator = "_v";
let patch_number = 5;
let version = Version::new(
Some(custom_format.to_string()),
Some(custom_separator.to_string()),
Some(patch_number),
);
let generated = version.generate();
let expected_date_part = version.date.format(custom_format).to_string();
let expected = format!("{}{}{}", expected_date_part, custom_separator, patch_number);
assert_eq!(generated, expected);
}
#[test]
fn test_generate_with_complex_format() {
let complex_format = "%Y-%m-%d_%H%M%S";
let separator = ".patch.";
let patch_number = 999;
let version = Version::new(
Some(complex_format.to_string()),
Some(separator.to_string()),
Some(patch_number),
);
let generated = version.generate();
let expected_date_part = version.date.format(complex_format).to_string();
let expected = format!("{}{}{}", expected_date_part, separator, patch_number);
assert_eq!(generated, expected);
}
#[test]
fn test_patch_boundary_values() {
let version_zero = Version::new(None, None, Some(0));
assert_eq!(version_zero.patch, 0);
let version_max = Version::new(None, None, Some(u16::MAX));
assert_eq!(version_max.patch, u16::MAX);
let generated_max = version_max.generate();
assert!(generated_max.ends_with(&u16::MAX.to_string()));
}
#[test]
fn test_empty_separator() {
let version = Version::new(None, Some("".to_string()), Some(42));
let generated = version.generate();
let expected_date_part = version.date.format(DEFAULT_FORMAT).to_string();
let expected = format!("{}{}", expected_date_part, 42);
assert_eq!(generated, expected);
}
#[test]
fn test_empty_format_string() {
let version = Version::new(Some("".to_string()), None, Some(1));
let generated = version.generate();
let expected = format!("{}{}", DEFAULT_SEPARATOR, 1);
assert_eq!(generated, expected);
}
#[test]
fn test_version_debug_trait() {
let version = Version::new(Some("%Y".to_string()), Some("-".to_string()), Some(1));
let debug_output = format!("{:?}", version);
assert!(debug_output.contains("Version"));
assert!(debug_output.contains("date"));
assert!(debug_output.contains("format"));
assert!(debug_output.contains("separator"));
assert!(debug_output.contains("patch"));
}
#[test]
fn test_multiple_versions_have_close_dates() {
let version1 = Version::new(None, None, None);
let version2 = Version::new(None, None, None);
let diff = (version2.date - version1.date).num_milliseconds().abs();
assert!(
diff < 100,
"Versions created consecutively should have very close dates"
);
}
#[test]
fn test_generate_consistency() {
let version = Version::new(Some("%Y%m%d".to_string()), Some("-".to_string()), Some(42));
let result1 = version.generate();
let result2 = version.generate();
let result3 = version.generate();
assert_eq!(result1, result2);
assert_eq!(result2, result3);
}
#[test]
fn test_get_prefix_without_patch_with_defaults() {
let version = Version::new(None, None, None);
let generated = version.get_prefix_without_patch();
let expected_date_part = version.date.format(DEFAULT_FORMAT).to_string();
let expected = format!("{}{}", expected_date_part, DEFAULT_SEPARATOR);
assert_eq!(generated, expected);
}
#[test]
fn test_get_prefix_without_patch_with_custom_values() {
let version = Version::new(Some("%Y%m%d".to_string()), Some("_v".to_string()), Some(10));
let generated = version.get_prefix_without_patch();
let expected_date_part = version.date.format("%Y%m%d").to_string();
let expected = format!("{}{}", expected_date_part, "_v".to_string());
assert_eq!(generated, expected);
}
}