1use crate::{DocumentType, MetisError, Result};
4use chrono::{DateTime, Utc};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Clone, Serialize)]
9pub struct DocumentContext {
10 pub title: String,
12 pub slug: String,
13 pub created_at: DateTime<Utc>,
14 pub updated_at: DateTime<Utc>,
15 pub parent_title: Option<String>,
16 pub blocked_by: Vec<String>,
17
18 pub risk_level: Option<RiskLevel>, pub stakeholders: Vec<String>, pub technical_lead: Option<String>, pub complexity: Option<Complexity>, pub decision_maker: Option<String>, pub decision_date: Option<DateTime<Utc>>, pub number: Option<u32>, }
27
28#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
30#[serde(rename_all = "lowercase")]
31pub enum RiskLevel {
32 Low,
33 Medium,
34 High,
35 Critical,
36}
37
38#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
40#[serde(rename_all = "lowercase")]
41pub enum Complexity {
42 S, M, L, XL, }
47
48impl DocumentContext {
49 pub fn new(title: String) -> Self {
51 let slug = Self::title_to_slug(&title);
52 let now = Utc::now();
53
54 Self {
55 title,
56 slug,
57 created_at: now,
58 updated_at: now,
59 parent_title: None,
60 blocked_by: Vec::new(),
61 risk_level: None,
62 stakeholders: Vec::new(),
63 technical_lead: None,
64 complexity: None,
65 decision_maker: None,
66 decision_date: None,
67 number: None,
68 }
69 }
70
71 pub fn validate_for_type(&self, doc_type: &DocumentType) -> Result<()> {
73 match doc_type {
74 DocumentType::Strategy => {
75 if self.risk_level.is_none() {
76 return Err(MetisError::MissingRequiredField {
77 field: "risk_level".to_string(),
78 });
79 }
80 }
81 DocumentType::Initiative => {
82 if self.complexity.is_none() {
83 return Err(MetisError::MissingRequiredField {
84 field: "complexity".to_string(),
85 });
86 }
87 }
88 DocumentType::Adr => {
89 if self.decision_maker.is_none() {
90 return Err(MetisError::MissingRequiredField {
91 field: "decision_maker".to_string(),
92 });
93 }
94 if self.number.is_none() {
95 return Err(MetisError::MissingRequiredField {
96 field: "number".to_string(),
97 });
98 }
99 }
100 DocumentType::Vision | DocumentType::Task => {
101 }
103 }
104 Ok(())
105 }
106
107 pub fn title_to_slug(title: &str) -> String {
109 title
110 .to_lowercase()
111 .chars()
112 .map(|c| if c.is_alphanumeric() { c } else { '-' })
113 .collect::<String>()
114 .split('-')
115 .filter(|s| !s.is_empty())
116 .collect::<Vec<_>>()
117 .join("-")
118 }
119
120 pub fn with_parent(mut self, parent_title: String) -> Self {
122 self.parent_title = Some(parent_title);
123 self
124 }
125
126 pub fn with_blocked_by(mut self, blocked_by: Vec<String>) -> Self {
127 self.blocked_by = blocked_by;
128 self
129 }
130
131 pub fn with_risk_level(mut self, risk_level: RiskLevel) -> Self {
132 self.risk_level = Some(risk_level);
133 self
134 }
135
136 pub fn with_stakeholders(mut self, stakeholders: Vec<String>) -> Self {
137 self.stakeholders = stakeholders;
138 self
139 }
140
141 pub fn with_technical_lead(mut self, technical_lead: String) -> Self {
142 self.technical_lead = Some(technical_lead);
143 self
144 }
145
146 pub fn with_complexity(mut self, complexity: Complexity) -> Self {
147 self.complexity = Some(complexity);
148 self
149 }
150
151 pub fn with_decision_maker(mut self, decision_maker: String) -> Self {
152 self.decision_maker = Some(decision_maker);
153 self
154 }
155
156 pub fn with_decision_date(mut self, decision_date: DateTime<Utc>) -> Self {
157 self.decision_date = Some(decision_date);
158 self
159 }
160
161 pub fn with_number(mut self, number: u32) -> Self {
162 self.number = Some(number);
163 self
164 }
165}
166
167#[cfg(test)]
168mod tests {
169 use super::*;
170 use chrono::Utc;
171
172 #[test]
173 fn test_title_to_slug() {
174 assert_eq!(
175 DocumentContext::title_to_slug("Core Document Management Library"),
176 "core-document-management-library"
177 );
178 assert_eq!(
179 DocumentContext::title_to_slug("ADR-001: Document Format"),
180 "adr-001-document-format"
181 );
182 assert_eq!(
183 DocumentContext::title_to_slug("Storage & Indexing System"),
184 "storage-indexing-system"
185 );
186 }
187
188 #[test]
189 fn test_new_context() {
190 let ctx = DocumentContext::new("Test Document".to_string());
191 assert_eq!(ctx.title, "Test Document");
192 assert_eq!(ctx.slug, "test-document");
193 assert!(ctx.created_at <= Utc::now());
194 assert!(ctx.updated_at <= Utc::now());
195 }
196
197 #[test]
198 fn test_strategy_validation() {
199 let ctx = DocumentContext::new("Test Strategy".to_string());
200
201 assert!(ctx.validate_for_type(&DocumentType::Strategy).is_err());
203
204 let ctx_with_risk = ctx.with_risk_level(RiskLevel::Medium);
206 assert!(ctx_with_risk
207 .validate_for_type(&DocumentType::Strategy)
208 .is_ok());
209 }
210
211 #[test]
212 fn test_initiative_validation() {
213 let ctx = DocumentContext::new("Test Initiative".to_string());
214
215 assert!(ctx.validate_for_type(&DocumentType::Initiative).is_err());
217
218 let ctx_with_complexity = ctx.with_complexity(Complexity::M);
220 assert!(ctx_with_complexity
221 .validate_for_type(&DocumentType::Initiative)
222 .is_ok());
223 }
224
225 #[test]
226 fn test_adr_validation() {
227 let ctx = DocumentContext::new("Test ADR".to_string());
228
229 assert!(ctx.validate_for_type(&DocumentType::Adr).is_err());
231
232 let ctx_complete = ctx
234 .with_decision_maker("Engineering Team".to_string())
235 .with_number(1);
236 assert!(ctx_complete.validate_for_type(&DocumentType::Adr).is_ok());
237 }
238
239 #[test]
240 fn test_vision_and_task_validation() {
241 let ctx = DocumentContext::new("Test Document".to_string());
242
243 assert!(ctx.validate_for_type(&DocumentType::Vision).is_ok());
245 assert!(ctx.validate_for_type(&DocumentType::Task).is_ok());
246 }
247
248 #[test]
249 fn test_builder_pattern() {
250 let ctx = DocumentContext::new("Test Document".to_string())
251 .with_parent("Parent Document".to_string())
252 .with_blocked_by(vec!["Blocker 1".to_string(), "Blocker 2".to_string()])
253 .with_stakeholders(vec!["Alice".to_string(), "Bob".to_string()]);
254
255 assert_eq!(ctx.parent_title, Some("Parent Document".to_string()));
256 assert_eq!(ctx.blocked_by.len(), 2);
257 assert_eq!(ctx.stakeholders.len(), 2);
258 }
259}