1use std::path::PathBuf;
7
8use serde::Serialize;
9
10use super::{Frontmatter, Status};
11
12#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
23pub struct AdrId(String);
24
25impl AdrId {
26 #[must_use]
28 pub fn new(id: impl Into<String>) -> Self {
29 Self(id.into())
30 }
31
32 #[must_use]
34 pub fn as_str(&self) -> &str {
35 &self.0
36 }
37
38 #[must_use]
42 pub fn from_path(path: &std::path::Path) -> Self {
43 let id = path
44 .file_stem()
45 .and_then(|s| s.to_str())
46 .unwrap_or("unknown");
47 Self::new(id)
48 }
49}
50
51impl std::fmt::Display for AdrId {
52 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
53 write!(f, "{}", self.0)
54 }
55}
56
57impl AsRef<str> for AdrId {
58 fn as_ref(&self) -> &str {
59 &self.0
60 }
61}
62
63#[derive(Debug, Clone, Serialize)]
68pub struct Adr {
69 id: AdrId,
71
72 filename: String,
74
75 #[serde(skip)]
77 source_path: PathBuf,
78
79 frontmatter: Frontmatter,
81
82 #[serde(skip)]
84 body_markdown: String,
85
86 body_html: String,
88
89 body_text: String,
91}
92
93impl Adr {
94 #[must_use]
96 pub fn new(
97 id: AdrId,
98 filename: String,
99 source_path: PathBuf,
100 frontmatter: Frontmatter,
101 body_markdown: String,
102 body_html: String,
103 body_text: String,
104 ) -> Self {
105 Self {
106 id,
107 filename,
108 source_path,
109 frontmatter,
110 body_markdown,
111 body_html,
112 body_text,
113 }
114 }
115
116 #[must_use]
118 pub fn id(&self) -> &AdrId {
119 &self.id
120 }
121
122 #[must_use]
124 pub fn filename(&self) -> &str {
125 &self.filename
126 }
127
128 #[must_use]
130 pub fn source_path(&self) -> &PathBuf {
131 &self.source_path
132 }
133
134 #[must_use]
136 pub fn frontmatter(&self) -> &Frontmatter {
137 &self.frontmatter
138 }
139
140 #[must_use]
142 pub fn body_markdown(&self) -> &str {
143 &self.body_markdown
144 }
145
146 #[must_use]
148 pub fn body_html(&self) -> &str {
149 &self.body_html
150 }
151
152 #[must_use]
154 pub fn body_text(&self) -> &str {
155 &self.body_text
156 }
157
158 #[must_use]
162 pub fn title(&self) -> &str {
163 &self.frontmatter.title
164 }
165
166 #[must_use]
168 pub fn description(&self) -> &str {
169 &self.frontmatter.description
170 }
171
172 #[must_use]
174 pub fn status(&self) -> Status {
175 self.frontmatter.status
176 }
177
178 #[must_use]
180 pub fn category(&self) -> &str {
181 &self.frontmatter.category
182 }
183
184 #[must_use]
186 pub fn tags(&self) -> &[String] {
187 &self.frontmatter.tags
188 }
189
190 #[must_use]
192 pub fn author(&self) -> &str {
193 &self.frontmatter.author
194 }
195
196 #[must_use]
198 pub fn project(&self) -> &str {
199 &self.frontmatter.project
200 }
201
202 #[must_use]
204 pub fn technologies(&self) -> &[String] {
205 &self.frontmatter.technologies
206 }
207
208 #[must_use]
210 pub fn related(&self) -> &[String] {
211 &self.frontmatter.related
212 }
213
214 #[must_use]
216 pub fn created(&self) -> Option<time::Date> {
217 self.frontmatter.created
218 }
219
220 #[must_use]
222 pub fn updated(&self) -> Option<time::Date> {
223 self.frontmatter.updated
224 }
225}
226
227#[cfg(test)]
228mod tests {
229 use super::*;
230
231 #[test]
232 fn test_adr_id_from_path() {
233 let path = PathBuf::from("docs/decisions/adr_0001.md");
234 let id = AdrId::from_path(&path);
235 assert_eq!(id.as_str(), "adr_0001");
236 }
237
238 #[test]
239 fn test_adr_id_display() {
240 let id = AdrId::new("adr_0001");
241 assert_eq!(format!("{id}"), "adr_0001");
242 }
243
244 #[test]
245 fn test_adr_creation() {
246 let frontmatter = Frontmatter::new("Test ADR").with_status(Status::Accepted);
247
248 let adr = Adr::new(
249 AdrId::new("adr_0001"),
250 "adr_0001.md".to_string(),
251 PathBuf::from("docs/decisions/adr_0001.md"),
252 frontmatter,
253 "# Context\n\nSome context.".to_string(),
254 "<h1>Context</h1><p>Some context.</p>".to_string(),
255 "Context Some context.".to_string(),
256 );
257
258 assert_eq!(adr.id().as_str(), "adr_0001");
259 assert_eq!(adr.title(), "Test ADR");
260 assert_eq!(adr.status(), Status::Accepted);
261 assert!(adr.body_html().contains("<h1>Context</h1>"));
262 }
263
264 #[test]
265 fn test_adr_serialization() {
266 let frontmatter = Frontmatter::new("Test").with_category("architecture");
267
268 let adr = Adr::new(
269 AdrId::new("test"),
270 "test.md".to_string(),
271 PathBuf::from("test.md"),
272 frontmatter,
273 "body".to_string(),
274 "<p>body</p>".to_string(),
275 "body".to_string(),
276 );
277
278 let json = serde_json::to_string(&adr).expect("should serialize");
279 assert!(json.contains("\"id\":\"test\""));
280 assert!(json.contains("\"filename\":\"test.md\""));
281 assert!(!json.contains("source_path"));
283 assert!(!json.contains("body_markdown"));
284 }
285
286 #[test]
287 fn test_adr_all_accessors() {
288 use time::macros::date;
289
290 let frontmatter = Frontmatter::new("Complete ADR")
291 .with_description("Full description")
292 .with_status(Status::Deprecated)
293 .with_category("security")
294 .with_author("Security Team")
295 .with_project("test-project")
296 .with_created(date!(2025 - 01 - 10))
297 .with_updated(date!(2025 - 01 - 15))
298 .with_tags(vec!["security".to_string()])
299 .with_technologies(vec!["rust".to_string()])
300 .with_related(vec!["adr-001.md".to_string()]);
301
302 let adr = Adr::new(
303 AdrId::new("adr_0002"),
304 "adr_0002.md".to_string(),
305 PathBuf::from("docs/decisions/adr_0002.md"),
306 frontmatter,
307 "# Body\n\nMarkdown content.".to_string(),
308 "<h1>Body</h1><p>Markdown content.</p>".to_string(),
309 "Body Markdown content.".to_string(),
310 );
311
312 assert_eq!(adr.id().as_str(), "adr_0002");
314 assert_eq!(adr.filename(), "adr_0002.md");
315 assert_eq!(
316 adr.source_path(),
317 &PathBuf::from("docs/decisions/adr_0002.md")
318 );
319 assert_eq!(adr.frontmatter().title, "Complete ADR");
320 assert_eq!(adr.body_markdown(), "# Body\n\nMarkdown content.");
321 assert_eq!(adr.body_html(), "<h1>Body</h1><p>Markdown content.</p>");
322 assert_eq!(adr.body_text(), "Body Markdown content.");
323 assert_eq!(adr.title(), "Complete ADR");
324 assert_eq!(adr.description(), "Full description");
325 assert_eq!(adr.status(), Status::Deprecated);
326 assert_eq!(adr.category(), "security");
327 assert_eq!(adr.tags(), &["security"]);
328 assert_eq!(adr.author(), "Security Team");
329 assert_eq!(adr.project(), "test-project");
330 assert_eq!(adr.technologies(), &["rust"]);
331 assert_eq!(adr.related(), &["adr-001.md"]);
332 assert_eq!(adr.created(), Some(date!(2025 - 01 - 10)));
333 assert_eq!(adr.updated(), Some(date!(2025 - 01 - 15)));
334 }
335}