use crate::config::{BumpConfig, PartConfig};
use crate::error::BumpError;
use alloc::string::{String, ToString};
use alloc::sync::Arc;
use alloc::vec::Vec;
use indexmap::IndexMap;
use regex::Regex;
#[cfg(feature = "std")]
fn cached_regex(pattern: &str) -> Result<Arc<Regex>, BumpError> {
use std::collections::HashMap;
use std::sync::{OnceLock, RwLock};
static CACHE: OnceLock<RwLock<HashMap<String, Arc<Regex>>>> = OnceLock::new();
let cache = CACHE.get_or_init(|| RwLock::new(HashMap::new()));
{
let guard = cache.read().unwrap_or_else(|p| p.into_inner());
if let Some(re) = guard.get(pattern) {
return Ok(Arc::clone(re));
}
}
let re = Regex::new(pattern).map_err(|e| BumpError::InvalidRegex(pattern.to_string(), e))?;
let re = Arc::new(re);
cache
.write()
.unwrap_or_else(|p| p.into_inner())
.insert(pattern.to_string(), Arc::clone(&re));
Ok(re)
}
#[cfg(not(feature = "std"))]
fn cached_regex(pattern: &str) -> Result<Arc<Regex>, BumpError> {
Regex::new(pattern)
.map_err(|e| BumpError::InvalidRegex(pattern.to_string(), e))
.map(Arc::new)
}
#[derive(Debug, Clone, PartialEq)]
pub struct VersionPart {
pub value: String,
pub cycle_index: Option<usize>,
}
pub type Version = IndexMap<String, VersionPart>;
pub fn parse_version(version_str: &str, cfg: &BumpConfig) -> Result<Version, BumpError> {
let re = cached_regex(&cfg.parse)?;
let caps = re.captures(version_str).ok_or_else(|| {
BumpError::VersionNotFound(version_str.to_string(), "<version string>".to_string())
})?;
let mut version: Version = IndexMap::new();
for name in re.capture_names().flatten() {
let val = caps
.name(name)
.map(|m| m.as_str())
.unwrap_or("")
.to_string();
let cycle_index = cfg.parts.get(name).and_then(|pc: &PartConfig| {
if pc.values.is_empty() {
None
} else {
pc.values.iter().position(|v| v == &val)
}
});
version.insert(
name.to_string(),
VersionPart {
value: val,
cycle_index,
},
);
}
Ok(version)
}
pub fn bump_version(
version: &Version,
part_to_bump: &str,
cfg: &BumpConfig,
) -> Result<Version, BumpError> {
if !version.contains_key(part_to_bump) {
return Err(BumpError::UnknownComponent(part_to_bump.to_string()));
}
let serialize_order: Vec<String> = extract_format_keys(&cfg.serialize[0]);
let mut bumped_version = version.clone();
let mut found = false;
for key in &serialize_order {
let part = match bumped_version.get_mut(key.as_str()) {
Some(p) => p,
None => continue,
};
if key == part_to_bump {
found = true;
if let Some(pc) = cfg.parts.get(key.as_str())
&& !pc.values.is_empty()
{
let current_idx = part.cycle_index.unwrap_or(0);
let next_idx = (current_idx + 1) % pc.values.len();
part.value = pc.values[next_idx].clone();
part.cycle_index = Some(next_idx);
continue;
}
let current: u64 = part
.value
.parse()
.map_err(|_| BumpError::InvalidComponentValue(part.value.clone()))?;
part.value = (current + 1).to_string();
part.cycle_index = None;
} else if found {
if let Some(pc) = cfg.parts.get(key.as_str())
&& !pc.values.is_empty()
{
let first = pc
.first_value
.clone()
.or_else(|| pc.values.first().cloned())
.unwrap_or_default();
let idx = pc.values.iter().position(|v| v == &first);
part.value = first;
part.cycle_index = idx;
continue;
}
part.value = "0".to_string();
part.cycle_index = None;
}
}
Ok(bumped_version)
}
pub fn serialize_version(version: &Version, cfg: &BumpConfig) -> String {
for format in &cfg.serialize {
let keys = extract_format_keys(format);
let all_optional_at_optional = keys.iter().all(|key| {
if let Some(part) = version.get(key.as_str()) {
if let Some(pc) = cfg.parts.get(key.as_str())
&& let Some(ref opt_val) = pc.optional_value
{
return part.value == *opt_val;
}
part.value.is_empty()
} else {
true
}
});
let uses_optional = keys.iter().any(|key| {
cfg.parts
.get(key.as_str())
.is_some_and(|pc| pc.optional_value.is_some())
});
if uses_optional && all_optional_at_optional {
continue;
}
let can_render = keys.iter().all(|key| version.contains_key(key.as_str()));
if can_render {
return render_format(format, version);
}
}
if let Some(last) = cfg.serialize.last() {
return render_format(last, version);
}
String::new()
}
pub fn extract_format_keys(format: &str) -> Vec<String> {
let mut keys = Vec::new();
let mut rest = format;
while let Some(open) = rest.find('{') {
rest = &rest[open + 1..];
if let Some(close) = rest.find('}') {
keys.push(rest[..close].to_string());
rest = &rest[close + 1..];
} else {
break;
}
}
keys
}
pub fn render_format(format: &str, version: &Version) -> String {
let mut result = format.to_string();
for (key, part) in version.iter() {
let placeholder = format!("{{{}}}", key);
result = result.replace(&placeholder, &part.value);
}
result
}