use std::path::PathBuf;
use serde::Serialize;
use super::{Frontmatter, Status};
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct AdrId(String);
impl AdrId {
#[must_use]
pub fn new(id: impl Into<String>) -> Self {
Self(id.into())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
#[must_use]
pub fn from_path(path: &std::path::Path) -> Self {
let id = path
.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("unknown");
Self::new(id)
}
}
impl std::fmt::Display for AdrId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl AsRef<str> for AdrId {
fn as_ref(&self) -> &str {
&self.0
}
}
#[derive(Debug, Clone, Serialize)]
pub struct Adr {
id: AdrId,
filename: String,
#[serde(skip)]
source_path: PathBuf,
frontmatter: Frontmatter,
#[serde(skip)]
body_markdown: String,
body_html: String,
body_text: String,
}
impl Adr {
#[must_use]
pub fn new(
id: AdrId,
filename: String,
source_path: PathBuf,
frontmatter: Frontmatter,
body_markdown: String,
body_html: String,
body_text: String,
) -> Self {
Self {
id,
filename,
source_path,
frontmatter,
body_markdown,
body_html,
body_text,
}
}
#[must_use]
pub fn id(&self) -> &AdrId {
&self.id
}
#[must_use]
pub fn filename(&self) -> &str {
&self.filename
}
#[must_use]
pub fn source_path(&self) -> &PathBuf {
&self.source_path
}
#[must_use]
pub fn frontmatter(&self) -> &Frontmatter {
&self.frontmatter
}
#[must_use]
pub fn body_markdown(&self) -> &str {
&self.body_markdown
}
#[must_use]
pub fn body_html(&self) -> &str {
&self.body_html
}
#[must_use]
pub fn body_text(&self) -> &str {
&self.body_text
}
#[must_use]
pub fn title(&self) -> &str {
&self.frontmatter.title
}
#[must_use]
pub fn description(&self) -> &str {
&self.frontmatter.description
}
#[must_use]
pub fn status(&self) -> Status {
self.frontmatter.status
}
#[must_use]
pub fn category(&self) -> &str {
&self.frontmatter.category
}
#[must_use]
pub fn tags(&self) -> &[String] {
&self.frontmatter.tags
}
#[must_use]
pub fn author(&self) -> &str {
&self.frontmatter.author
}
#[must_use]
pub fn project(&self) -> &str {
&self.frontmatter.project
}
#[must_use]
pub fn technologies(&self) -> &[String] {
&self.frontmatter.technologies
}
#[must_use]
pub fn related(&self) -> &[String] {
&self.frontmatter.related
}
#[must_use]
pub fn created(&self) -> Option<time::Date> {
self.frontmatter.created
}
#[must_use]
pub fn updated(&self) -> Option<time::Date> {
self.frontmatter.updated
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_adr_id_from_path() {
let path = PathBuf::from("docs/decisions/adr_0001.md");
let id = AdrId::from_path(&path);
assert_eq!(id.as_str(), "adr_0001");
}
#[test]
fn test_adr_id_display() {
let id = AdrId::new("adr_0001");
assert_eq!(format!("{id}"), "adr_0001");
}
#[test]
fn test_adr_creation() {
let frontmatter = Frontmatter::new("Test ADR").with_status(Status::Accepted);
let adr = Adr::new(
AdrId::new("adr_0001"),
"adr_0001.md".to_string(),
PathBuf::from("docs/decisions/adr_0001.md"),
frontmatter,
"# Context\n\nSome context.".to_string(),
"<h1>Context</h1><p>Some context.</p>".to_string(),
"Context Some context.".to_string(),
);
assert_eq!(adr.id().as_str(), "adr_0001");
assert_eq!(adr.title(), "Test ADR");
assert_eq!(adr.status(), Status::Accepted);
assert!(adr.body_html().contains("<h1>Context</h1>"));
}
#[test]
fn test_adr_serialization() {
let frontmatter = Frontmatter::new("Test").with_category("architecture");
let adr = Adr::new(
AdrId::new("test"),
"test.md".to_string(),
PathBuf::from("test.md"),
frontmatter,
"body".to_string(),
"<p>body</p>".to_string(),
"body".to_string(),
);
let json = serde_json::to_string(&adr).expect("should serialize");
assert!(json.contains("\"id\":\"test\""));
assert!(json.contains("\"filename\":\"test.md\""));
assert!(!json.contains("source_path"));
assert!(!json.contains("body_markdown"));
}
#[test]
fn test_adr_all_accessors() {
use time::macros::date;
let frontmatter = Frontmatter::new("Complete ADR")
.with_description("Full description")
.with_status(Status::Deprecated)
.with_category("security")
.with_author("Security Team")
.with_project("test-project")
.with_created(date!(2025 - 01 - 10))
.with_updated(date!(2025 - 01 - 15))
.with_tags(vec!["security".to_string()])
.with_technologies(vec!["rust".to_string()])
.with_related(vec!["adr-001.md".to_string()]);
let adr = Adr::new(
AdrId::new("adr_0002"),
"adr_0002.md".to_string(),
PathBuf::from("docs/decisions/adr_0002.md"),
frontmatter,
"# Body\n\nMarkdown content.".to_string(),
"<h1>Body</h1><p>Markdown content.</p>".to_string(),
"Body Markdown content.".to_string(),
);
assert_eq!(adr.id().as_str(), "adr_0002");
assert_eq!(adr.filename(), "adr_0002.md");
assert_eq!(
adr.source_path(),
&PathBuf::from("docs/decisions/adr_0002.md")
);
assert_eq!(adr.frontmatter().title, "Complete ADR");
assert_eq!(adr.body_markdown(), "# Body\n\nMarkdown content.");
assert_eq!(adr.body_html(), "<h1>Body</h1><p>Markdown content.</p>");
assert_eq!(adr.body_text(), "Body Markdown content.");
assert_eq!(adr.title(), "Complete ADR");
assert_eq!(adr.description(), "Full description");
assert_eq!(adr.status(), Status::Deprecated);
assert_eq!(adr.category(), "security");
assert_eq!(adr.tags(), &["security"]);
assert_eq!(adr.author(), "Security Team");
assert_eq!(adr.project(), "test-project");
assert_eq!(adr.technologies(), &["rust"]);
assert_eq!(adr.related(), &["adr-001.md"]);
assert_eq!(adr.created(), Some(date!(2025 - 01 - 10)));
assert_eq!(adr.updated(), Some(date!(2025 - 01 - 15)));
}
}