use crate::error::{VersionError, VersionResult};
use crate::types::Version;
use regex::Regex;
use std::sync::OnceLock;
#[derive(Debug, Clone)]
pub struct SnapshotGenerator {
format: String,
variables: Vec<SnapshotVariable>,
}
impl SnapshotGenerator {
pub fn new(format: &str) -> VersionResult<Self> {
if format.is_empty() {
return Err(VersionError::SnapshotFailed {
package: "unknown".to_string(),
reason: "snapshot format cannot be empty".to_string(),
});
}
let variables = Self::parse_variables(format)?;
let generator = Self { format: format.to_string(), variables };
generator.validate()?;
Ok(generator)
}
pub fn generate(&self, context: &SnapshotContext) -> VersionResult<String> {
let mut result = self.format.clone();
if self.variables.contains(&SnapshotVariable::Version) {
result = result.replace("{version}", &context.version.to_string());
}
if self.variables.contains(&SnapshotVariable::Branch) {
let sanitized_branch = Self::sanitize_branch(&context.branch);
result = result.replace("{branch}", &sanitized_branch);
}
if self.variables.contains(&SnapshotVariable::Commit)
|| self.variables.contains(&SnapshotVariable::ShortCommit)
{
let short_commit = Self::short_hash(&context.commit);
result = result.replace("{commit}", short_commit);
result = result.replace("{short_commit}", short_commit);
}
if self.variables.contains(&SnapshotVariable::Timestamp) {
result = result.replace("{timestamp}", &context.timestamp.to_string());
}
Self::validate_snapshot(&result, &context.version.to_string())?;
Ok(result)
}
pub fn validate(&self) -> VersionResult<()> {
if !self.variables.contains(&SnapshotVariable::Version) {
return Err(VersionError::SnapshotFailed {
package: "unknown".to_string(),
reason: "snapshot format must contain {version} variable".to_string(),
});
}
Ok(())
}
fn parse_variables(format: &str) -> VersionResult<Vec<SnapshotVariable>> {
static VARIABLE_REGEX: OnceLock<Regex> = OnceLock::new();
let regex = VARIABLE_REGEX.get_or_init(|| {
Regex::new(r"\{([^}]+)\}").unwrap_or_else(|_| {
unreachable!("Variable regex pattern is invalid")
})
});
let mut variables = Vec::new();
for cap in regex.captures_iter(format) {
let var_name = &cap[1];
let variable = match var_name {
"version" => SnapshotVariable::Version,
"branch" => SnapshotVariable::Branch,
"commit" => SnapshotVariable::Commit,
"short_commit" => SnapshotVariable::ShortCommit,
"timestamp" => SnapshotVariable::Timestamp,
_ => {
return Err(VersionError::SnapshotFailed {
package: "unknown".to_string(),
reason: format!(
"unsupported variable '{{{}}}' in snapshot format. \
Supported variables: {{version}}, {{branch}}, {{commit}}, {{short_commit}}, {{timestamp}}",
var_name
),
});
}
};
if !variables.contains(&variable) {
variables.push(variable);
}
}
Ok(variables)
}
fn sanitize_branch(branch: &str) -> String {
static SANITIZE_REGEX: OnceLock<Regex> = OnceLock::new();
static MULTIPLE_HYPHENS_REGEX: OnceLock<Regex> = OnceLock::new();
let sanitize = SANITIZE_REGEX.get_or_init(|| {
Regex::new(r"[^a-zA-Z0-9.\-_]")
.unwrap_or_else(|_| unreachable!("Sanitize regex pattern is invalid"))
});
let multiple_hyphens = MULTIPLE_HYPHENS_REGEX.get_or_init(|| {
Regex::new(r"-+")
.unwrap_or_else(|_| unreachable!("Multiple hyphens regex pattern is invalid"))
});
let mut result = branch.replace('/', "-");
result = result.to_lowercase();
result = sanitize.replace_all(&result, "").to_string();
result = multiple_hyphens.replace_all(&result, "-").to_string();
result = result.trim_matches('-').to_string();
result
}
fn short_hash(commit: &str) -> &str {
if commit.len() > 7 { &commit[..7] } else { commit }
}
fn validate_snapshot(snapshot: &str, _base_version: &str) -> VersionResult<()> {
if snapshot.is_empty() {
return Err(VersionError::SnapshotFailed {
package: "unknown".to_string(),
reason: "generated snapshot version is empty".to_string(),
});
}
Ok(())
}
pub fn format(&self) -> &str {
&self.format
}
pub fn variables(&self) -> &[SnapshotVariable] {
&self.variables
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SnapshotVariable {
Version,
Branch,
Commit,
ShortCommit,
Timestamp,
}
#[derive(Debug, Clone)]
pub struct SnapshotContext {
pub version: Version,
pub branch: String,
pub commit: String,
pub timestamp: i64,
}
impl SnapshotContext {
pub fn new(version: Version, branch: String, commit: String) -> Self {
Self { version, branch, commit, timestamp: chrono::Utc::now().timestamp() }
}
pub fn with_timestamp(
version: Version,
branch: String,
commit: String,
timestamp: i64,
) -> Self {
Self { version, branch, commit, timestamp }
}
}