use crate::diagnostic::{Diagnostic, DiagnosticCode, DiagnosticResult};
use crate::model::{ChangelogCategory, ChangelogEntry, RfcSpec};
use chrono::Local;
use semver::Version;
#[cfg(test)]
mod tests;
#[derive(Debug, Clone)]
pub struct ParsedChange {
pub category: ChangelogCategory,
pub message: String,
pub explicit: bool,
}
pub fn parse_changelog_change(change: &str) -> DiagnosticResult<ParsedChange> {
if let Some(colon_pos) = change.find(':') {
let prefix = change[..colon_pos].trim();
let message = change[colon_pos + 1..].trim();
if !prefix.contains(' ') && !prefix.is_empty() {
if let Some(category) = ChangelogCategory::from_prefix(prefix) {
if message.is_empty() {
return Err(Diagnostic::new(
DiagnosticCode::E0805EmptyValue,
format!("Empty message after prefix '{prefix}:'"),
"changelog",
));
}
return Ok(ParsedChange {
category,
message: message.to_string(),
explicit: true,
});
} else {
return Err(Diagnostic::new(
DiagnosticCode::E0808InvalidPrefix,
format!(
"Unknown changelog prefix '{prefix}'. Valid prefixes: {}",
ChangelogCategory::VALID_PREFIXES.join(", ")
),
"changelog",
));
}
}
}
Ok(ParsedChange {
category: ChangelogCategory::Added,
message: change.trim().to_string(),
explicit: false,
})
}
#[derive(Debug, Clone, Copy)]
pub enum BumpLevel {
Patch,
Minor,
Major,
}
pub fn bump_rfc_version(
rfc: &mut RfcSpec,
level: BumpLevel,
summary: &str,
) -> DiagnosticResult<String> {
let mut version = Version::parse(&rfc.version).map_err(|err| {
Diagnostic::new(
DiagnosticCode::E0101RfcSchemaInvalid,
format!("Invalid RFC version '{}': {err}", rfc.version),
&rfc.rfc_id,
)
})?;
match level {
BumpLevel::Patch => version.patch += 1,
BumpLevel::Minor => {
version.minor += 1;
version.patch = 0;
}
BumpLevel::Major => {
version.major += 1;
version.minor = 0;
version.patch = 0;
}
}
let new_version = version.to_string();
rfc.version = new_version.clone();
rfc.updated = Some(today());
rfc.changelog.insert(
0,
ChangelogEntry {
version: new_version.clone(),
date: today(),
notes: Some(summary.to_string()),
added: vec![],
changed: vec![],
deprecated: vec![],
removed: vec![],
fixed: vec![],
security: vec![],
},
);
Ok(new_version)
}
pub fn add_changelog_change(rfc: &mut RfcSpec, change: &str) -> DiagnosticResult<()> {
let parsed = parse_changelog_change(change)?;
if let Some(entry) = rfc.changelog.first_mut() {
match parsed.category {
ChangelogCategory::Added => entry.added.push(parsed.message),
ChangelogCategory::Changed => entry.changed.push(parsed.message),
ChangelogCategory::Deprecated => entry.deprecated.push(parsed.message),
ChangelogCategory::Removed => entry.removed.push(parsed.message),
ChangelogCategory::Fixed => entry.fixed.push(parsed.message),
ChangelogCategory::Security => entry.security.push(parsed.message),
ChangelogCategory::Chore => {
return Err(Diagnostic::new(
DiagnosticCode::E0809ChoreNotAllowed,
"'chore:' category is not valid for RFC changelogs (use for work items only)",
"changelog",
));
}
}
} else {
return Err(Diagnostic::new(
DiagnosticCode::E0111RfcNoChangelog,
"No changelog entry exists. Bump version first.",
"rfc",
));
}
Ok(())
}
pub fn today() -> String {
Local::now().format("%Y-%m-%d").to_string()
}