use regex::Regex;
use std::sync::LazyLock;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AdrFormat {
Nygard,
Madr4,
#[default]
Auto,
}
impl std::fmt::Display for AdrFormat {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
AdrFormat::Nygard => write!(f, "nygard"),
AdrFormat::Madr4 => write!(f, "madr"),
AdrFormat::Auto => write!(f, "auto"),
}
}
}
impl std::str::FromStr for AdrFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s.to_lowercase().as_str() {
"nygard" => Ok(AdrFormat::Nygard),
"madr" | "madr4" => Ok(AdrFormat::Madr4),
"auto" => Ok(AdrFormat::Auto),
_ => Err(format!("Unknown ADR format: {}", s)),
}
}
}
static NYGARD_TITLE_REGEX: LazyLock<Regex> =
LazyLock::new(|| Regex::new(r"^#\s+(\d+)[.\-\s]+\s*(.+)$").expect("Invalid regex"));
pub fn detect_format(content: &str) -> AdrFormat {
let trimmed = content.trim_start();
if trimmed.starts_with("---") {
return AdrFormat::Madr4;
}
AdrFormat::Nygard
}
pub fn is_adr_document(content: &str, file_path: Option<&std::path::Path>) -> bool {
if let Some(path) = file_path {
let path_str = path.to_string_lossy().to_lowercase();
if path_str.contains("/adr/")
|| path_str.contains("/adrs/")
|| path_str.contains("\\adr\\")
|| path_str.contains("\\adrs\\")
|| path_str.starts_with("adr/")
|| path_str.starts_with("adrs/")
|| path_str.starts_with("adr\\")
|| path_str.starts_with("adrs\\")
{
return true;
}
}
let trimmed = content.trim_start();
if let Some(after_open) = trimmed.strip_prefix("---") {
if let Some(end) = after_open.find("---") {
let frontmatter = &after_open[..end];
if frontmatter.lines().any(|line| {
let line = line.trim();
line.starts_with("status:") || line.starts_with("status :")
}) {
return true;
}
}
}
for line in content.lines().take(5) {
if is_nygard_title(line) {
return true;
}
}
false
}
pub fn extract_nygard_number(title_line: &str) -> Option<u32> {
NYGARD_TITLE_REGEX
.captures(title_line)
.and_then(|caps| caps.get(1))
.and_then(|m| m.as_str().parse().ok())
}
pub fn extract_nygard_title(title_line: &str) -> Option<&str> {
NYGARD_TITLE_REGEX
.captures(title_line)
.and_then(|caps| caps.get(2))
.map(|m| m.as_str().trim())
}
pub fn is_nygard_title(line: &str) -> bool {
NYGARD_TITLE_REGEX.is_match(line)
}
#[derive(Debug, Clone)]
pub struct ParsedAdr {
pub format: AdrFormat,
pub number: Option<u32>,
pub title: Option<String>,
pub status: Option<String>,
pub date: Option<String>,
pub title_line: Option<usize>,
pub status_line: Option<usize>,
pub date_line: Option<usize>,
}
impl ParsedAdr {
pub fn new(format: AdrFormat) -> Self {
Self {
format,
number: None,
title: None,
status: None,
date: None,
title_line: None,
status_line: None,
date_line: None,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_detect_format_madr() {
let content = r#"---
status: accepted
date: 2024-01-15
---
# Use PostgreSQL
"#;
assert_eq!(detect_format(content), AdrFormat::Madr4);
}
#[test]
fn test_detect_format_nygard() {
let content = r#"# 1. Use Rust for implementation
Date: 2024-01-15
## Status
Accepted
"#;
assert_eq!(detect_format(content), AdrFormat::Nygard);
}
#[test]
fn test_detect_format_with_leading_whitespace() {
let content = " \n\n---\nstatus: accepted\n---\n";
assert_eq!(detect_format(content), AdrFormat::Madr4);
}
#[test]
fn test_extract_nygard_number() {
assert_eq!(extract_nygard_number("# 1. Use Rust"), Some(1));
assert_eq!(extract_nygard_number("# 42. Some Decision"), Some(42));
assert_eq!(extract_nygard_number("# 1 - Use Rust"), Some(1));
assert_eq!(extract_nygard_number("# Use Rust"), None);
assert_eq!(extract_nygard_number("## 1. Section"), None);
}
#[test]
fn test_extract_nygard_title() {
assert_eq!(extract_nygard_title("# 1. Use Rust"), Some("Use Rust"));
assert_eq!(
extract_nygard_title("# 42. Some Decision"),
Some("Some Decision")
);
assert_eq!(extract_nygard_title("# 1 - Use Rust"), Some("Use Rust"));
assert_eq!(extract_nygard_title("# Use Rust"), None);
}
#[test]
fn test_is_nygard_title() {
assert!(is_nygard_title("# 1. Use Rust"));
assert!(is_nygard_title("# 42. Some Decision"));
assert!(is_nygard_title("# 1 - Use Rust"));
assert!(!is_nygard_title("# Use Rust"));
assert!(!is_nygard_title("## 1. Section"));
}
#[test]
fn test_format_from_str() {
assert_eq!("nygard".parse::<AdrFormat>().unwrap(), AdrFormat::Nygard);
assert_eq!("madr".parse::<AdrFormat>().unwrap(), AdrFormat::Madr4);
assert_eq!("madr4".parse::<AdrFormat>().unwrap(), AdrFormat::Madr4);
assert_eq!("auto".parse::<AdrFormat>().unwrap(), AdrFormat::Auto);
assert_eq!("NYGARD".parse::<AdrFormat>().unwrap(), AdrFormat::Nygard);
assert!("unknown".parse::<AdrFormat>().is_err());
}
#[test]
fn test_format_display() {
assert_eq!(format!("{}", AdrFormat::Nygard), "nygard");
assert_eq!(format!("{}", AdrFormat::Madr4), "madr");
assert_eq!(format!("{}", AdrFormat::Auto), "auto");
}
}