use std::path::Path;
use anyhow::{
Context,
Result,
};
use regex::Regex;
#[derive(Debug)]
pub struct ReadmeUpdateResult {
pub content: String,
pub modified: bool,
}
pub fn update_readme_content(
content: &str,
package_name: &str,
old_version: &str,
new_version: &str,
) -> ReadmeUpdateResult {
let hyphenated = package_name.replace('_', "-");
let underscored = package_name.replace('-', "_");
let mut result = content.to_string();
let mut modified = false;
for name in [&hyphenated, &underscored] {
let pattern = format!(
r#"({})\s*=\s*"{}""#,
regex::escape(name),
regex::escape(old_version)
);
if let Ok(re) = Regex::new(&pattern) {
let replacement = format!(r#"{} = "{}""#, name, new_version);
let new_result = re.replace_all(&result, replacement.as_str());
if new_result != result {
result = new_result.into_owned();
modified = true;
}
}
}
ReadmeUpdateResult {
content: result,
modified,
}
}
pub fn update_readme_file(
readme_path: &Path,
package_name: &str,
old_version: &str,
new_version: &str,
) -> Result<Option<ReadmeUpdateResult>> {
if !readme_path.exists() {
return Ok(None);
}
let content = std::fs::read_to_string(readme_path)
.with_context(|| format!("Failed to read {}", readme_path.display()))?;
let result = update_readme_content(&content, package_name, old_version, new_version);
Ok(Some(result))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_update_simple_version() {
let content = r#"
Add to your Cargo.toml:
```toml
[dependencies]
my-crate = "0.1.0"
```
"#;
let result = update_readme_content(content, "my-crate", "0.1.0", "0.2.0");
assert!(result.modified);
assert!(result.content.contains(r#"my-crate = "0.2.0""#));
assert!(!result.content.contains(r#"my-crate = "0.1.0""#));
}
#[test]
fn test_update_underscored_name() {
let content = r#"my_crate = "1.0.0""#;
let result = update_readme_content(content, "my-crate", "1.0.0", "1.1.0");
assert!(result.modified);
assert!(result.content.contains(r#"my_crate = "1.1.0""#));
}
#[test]
fn test_no_match_different_version() {
let content = r#"my-crate = "0.1.0""#;
let result = update_readme_content(content, "my-crate", "0.2.0", "0.3.0");
assert!(!result.modified);
assert!(result.content.contains(r#"my-crate = "0.1.0""#));
}
#[test]
fn test_no_match_different_crate() {
let content = r#"other-crate = "0.1.0""#;
let result = update_readme_content(content, "my-crate", "0.1.0", "0.2.0");
assert!(!result.modified);
}
#[test]
fn test_multiple_occurrences() {
let content = r#"
my-crate = "0.1.0"
# Also:
my-crate = "0.1.0"
"#;
let result = update_readme_content(content, "my-crate", "0.1.0", "0.2.0");
assert!(result.modified);
assert_eq!(result.content.matches(r#"my-crate = "0.2.0""#).count(), 2);
}
#[test]
fn test_whitespace_variations() {
let content = r#"my-crate="0.1.0""#;
let result = update_readme_content(content, "my-crate", "0.1.0", "0.2.0");
assert!(result.modified);
assert!(result.content.contains(r#"my-crate = "0.2.0""#));
}
}