use std::sync::LazyLock;
use crate::version_file::{VersionFile, VersionFileError};
static JSON_VERSION_RE: LazyLock<regex::Regex> =
LazyLock::new(|| regex::Regex::new(r#""version"\s*:\s*"([^"]+)""#).expect("valid regex"));
#[derive(Debug, Clone, Copy)]
pub struct ProjectTomlVersionFile;
impl VersionFile for ProjectTomlVersionFile {
fn name(&self) -> &str {
"project.toml"
}
fn filenames(&self) -> &[&str] {
&["project.toml"]
}
fn detect(&self, content: &str) -> bool {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
return false;
}
if trimmed.starts_with("version") && trimmed.contains('=') {
return true;
}
}
false
}
fn read_version(&self, content: &str) -> Option<String> {
for line in content.lines() {
let trimmed = line.trim();
if trimmed.starts_with('[') {
return None;
}
if trimmed.starts_with("version")
&& let Some(eq_pos) = trimmed.find('=')
{
let value = trimmed[eq_pos + 1..].trim();
return Some(value.trim_matches('"').to_string());
}
}
None
}
fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
let mut result = String::new();
let mut replaced = false;
for line in content.lines() {
let trimmed = line.trim();
if !replaced
&& !trimmed.starts_with('[')
&& trimmed.starts_with("version")
&& let Some(eq_pos) = line.find('=')
{
let prefix = &line[..=eq_pos];
result.push_str(prefix);
result.push_str(&format!(" \"{new_version}\""));
result.push('\n');
replaced = true;
continue;
}
result.push_str(line);
result.push('\n');
}
if !replaced {
return Err(VersionFileError::NoVersionField);
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
Ok(result)
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProjectJsonVersionFile;
impl VersionFile for ProjectJsonVersionFile {
fn name(&self) -> &str {
"project.json"
}
fn filenames(&self) -> &[&str] {
&["project.json"]
}
fn detect(&self, content: &str) -> bool {
JSON_VERSION_RE.is_match(content)
}
fn read_version(&self, content: &str) -> Option<String> {
JSON_VERSION_RE
.captures(content)
.and_then(|caps| caps.get(1))
.map(|m| m.as_str().to_string())
}
fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
let re = &*JSON_VERSION_RE;
if !re.is_match(content) {
return Err(VersionFileError::NoVersionField);
}
let mut replaced = false;
let result = re.replace(content, |caps: ®ex::Captures<'_>| {
if replaced {
return caps[0].to_string();
}
replaced = true;
let full = &caps[0];
let version_start = caps.get(1).unwrap().start() - caps.get(0).unwrap().start();
let version_end = caps.get(1).unwrap().end() - caps.get(0).unwrap().start();
format!(
"{}{}{}",
&full[..version_start],
new_version,
&full[version_end..],
)
});
Ok(result.into_owned())
}
}
#[derive(Debug, Clone, Copy)]
pub struct ProjectYamlVersionFile;
impl VersionFile for ProjectYamlVersionFile {
fn name(&self) -> &str {
"project.yaml"
}
fn filenames(&self) -> &[&str] {
&["project.yaml"]
}
fn detect(&self, content: &str) -> bool {
content
.lines()
.any(|line| line.starts_with("version:") && line.len() > "version:".len())
}
fn read_version(&self, content: &str) -> Option<String> {
for line in content.lines() {
if let Some(value) = line.strip_prefix("version:") {
let value = value.trim().trim_matches('"').trim_matches('\'');
if !value.is_empty() {
return Some(value.to_string());
}
}
}
None
}
fn write_version(&self, content: &str, new_version: &str) -> Result<String, VersionFileError> {
let mut result = String::new();
let mut replaced = false;
for line in content.lines() {
if !replaced && line.starts_with("version:") {
result.push_str(&format!("version: \"{new_version}\""));
result.push('\n');
replaced = true;
continue;
}
result.push_str(line);
result.push('\n');
}
if !replaced {
return Err(VersionFileError::NoVersionField);
}
if !content.ends_with('\n') && result.ends_with('\n') {
result.pop();
}
Ok(result)
}
}
#[cfg(test)]
mod tests {
use super::*;
const TOML: &str = r#"name = "io.driftsys.myapp"
description = "My application"
version = "0.1.0"
license = "MIT"
"#;
const TOML_NO_VERSION: &str = "name = \"io.driftsys.myapp\"\n";
const TOML_VERSION_IN_SECTION: &str = r#"name = "io.driftsys.myapp"
[metadata]
version = "0.1.0"
"#;
#[test]
fn toml_detect() {
assert!(ProjectTomlVersionFile.detect(TOML));
}
#[test]
fn toml_detect_no_version() {
assert!(!ProjectTomlVersionFile.detect(TOML_NO_VERSION));
}
#[test]
fn toml_detect_ignores_section() {
assert!(!ProjectTomlVersionFile.detect(TOML_VERSION_IN_SECTION));
}
#[test]
fn toml_read() {
assert_eq!(
ProjectTomlVersionFile.read_version(TOML),
Some("0.1.0".to_string()),
);
}
#[test]
fn toml_write() {
let result = ProjectTomlVersionFile.write_version(TOML, "2.0.0").unwrap();
assert!(result.contains("version = \"2.0.0\""));
assert!(result.contains("license = \"MIT\""));
}
#[test]
fn toml_write_no_version_errors() {
assert!(
ProjectTomlVersionFile
.write_version(TOML_NO_VERSION, "1.0.0")
.is_err()
);
}
const JSON: &str = r#"{
"name": "io.driftsys.myapp",
"version": "0.1.0",
"description": "My application"
}
"#;
const JSON_NO_VERSION: &str = r#"{
"name": "io.driftsys.myapp"
}
"#;
#[test]
fn json_detect() {
assert!(ProjectJsonVersionFile.detect(JSON));
}
#[test]
fn json_detect_no_version() {
assert!(!ProjectJsonVersionFile.detect(JSON_NO_VERSION));
}
#[test]
fn json_read() {
assert_eq!(
ProjectJsonVersionFile.read_version(JSON),
Some("0.1.0".to_string()),
);
}
#[test]
fn json_write() {
let result = ProjectJsonVersionFile.write_version(JSON, "2.0.0").unwrap();
assert!(result.contains(r#""version": "2.0.0""#));
assert!(result.contains(r#""name": "io.driftsys.myapp""#));
}
const YAML: &str = "name: io.driftsys.myapp\nversion: \"0.1.0\"\nlicense: MIT\n";
const YAML_UNQUOTED: &str = "name: io.driftsys.myapp\nversion: 0.1.0\nlicense: MIT\n";
const YAML_NO_VERSION: &str = "name: io.driftsys.myapp\nlicense: MIT\n";
#[test]
fn yaml_detect() {
assert!(ProjectYamlVersionFile.detect(YAML));
}
#[test]
fn yaml_detect_unquoted() {
assert!(ProjectYamlVersionFile.detect(YAML_UNQUOTED));
}
#[test]
fn yaml_detect_no_version() {
assert!(!ProjectYamlVersionFile.detect(YAML_NO_VERSION));
}
#[test]
fn yaml_read_quoted() {
assert_eq!(
ProjectYamlVersionFile.read_version(YAML),
Some("0.1.0".to_string()),
);
}
#[test]
fn yaml_read_unquoted() {
assert_eq!(
ProjectYamlVersionFile.read_version(YAML_UNQUOTED),
Some("0.1.0".to_string()),
);
}
#[test]
fn yaml_write() {
let result = ProjectYamlVersionFile.write_version(YAML, "2.0.0").unwrap();
assert!(result.contains("version: \"2.0.0\""));
assert!(result.contains("license: MIT"));
}
#[test]
fn yaml_write_no_version_errors() {
assert!(
ProjectYamlVersionFile
.write_version(YAML_NO_VERSION, "1.0.0")
.is_err()
);
}
}