1use crate::domain::documents::types::DocumentType;
2use serde::{Deserialize, Serialize};
3use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct FlightLevelConfig {
8 pub strategies_enabled: bool,
10 pub initiatives_enabled: bool,
12}
13
14impl FlightLevelConfig {
15 pub fn new(
17 strategies_enabled: bool,
18 initiatives_enabled: bool,
19 ) -> Result<Self, ConfigurationError> {
20 if !initiatives_enabled && strategies_enabled {
22 return Err(ConfigurationError::InvalidConfiguration(
23 "Cannot enable strategies without initiatives - this would create a gap in the hierarchy".to_string()
24 ));
25 }
26
27 Ok(Self {
28 strategies_enabled,
29 initiatives_enabled,
30 })
31 }
32
33 pub fn full() -> Self {
35 Self {
36 strategies_enabled: true,
37 initiatives_enabled: true,
38 }
39 }
40
41 pub fn streamlined() -> Self {
43 Self {
44 strategies_enabled: false,
45 initiatives_enabled: true,
46 }
47 }
48
49 pub fn direct() -> Self {
51 Self {
52 strategies_enabled: false,
53 initiatives_enabled: false,
54 }
55 }
56
57 pub fn is_document_type_allowed(&self, doc_type: DocumentType) -> bool {
59 match doc_type {
60 DocumentType::Vision | DocumentType::Adr => true, DocumentType::Task => true, DocumentType::Strategy => self.strategies_enabled,
63 DocumentType::Initiative => self.initiatives_enabled,
64 }
65 }
66
67 pub fn get_parent_type(&self, doc_type: DocumentType) -> Option<DocumentType> {
69 match doc_type {
70 DocumentType::Vision | DocumentType::Adr => None, DocumentType::Strategy => Some(DocumentType::Vision),
72 DocumentType::Initiative => {
73 if self.strategies_enabled {
74 Some(DocumentType::Strategy)
75 } else {
76 Some(DocumentType::Vision)
77 }
78 }
79 DocumentType::Task => {
80 if self.initiatives_enabled {
81 Some(DocumentType::Initiative)
82 } else {
83 Some(DocumentType::Vision)
84 }
85 }
86 }
87 }
88
89 pub fn preset_name(&self) -> &'static str {
91 match (self.strategies_enabled, self.initiatives_enabled) {
92 (true, true) => "full",
93 (false, true) => "streamlined",
94 (false, false) => "direct",
95 (true, false) => "invalid", }
97 }
98
99 pub fn enabled_document_types(&self) -> Vec<DocumentType> {
101 let mut types = vec![DocumentType::Vision];
102
103 if self.strategies_enabled {
104 types.push(DocumentType::Strategy);
105 }
106
107 if self.initiatives_enabled {
108 types.push(DocumentType::Initiative);
109 }
110
111 types.push(DocumentType::Task);
112 types.push(DocumentType::Adr); types
115 }
116
117 pub fn hierarchy_display(&self) -> String {
119 let mut hierarchy = vec!["Vision"];
120
121 if self.strategies_enabled {
122 hierarchy.push("Strategy");
123 }
124
125 if self.initiatives_enabled {
126 hierarchy.push("Initiative");
127 }
128
129 hierarchy.push("Task");
130
131 hierarchy.join(" → ")
132 }
133}
134
135impl Default for FlightLevelConfig {
136 fn default() -> Self {
137 Self::full()
138 }
139}
140
141impl fmt::Display for FlightLevelConfig {
142 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
143 write!(f, "{}", self.preset_name())
144 }
145}
146
147#[derive(Debug, Clone, PartialEq, Eq)]
149pub enum ConfigurationError {
150 InvalidConfiguration(String),
151 SerializationError(String),
152 InvalidValue(String),
153 MissingConfiguration(String),
154}
155
156impl fmt::Display for ConfigurationError {
157 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
158 match self {
159 ConfigurationError::InvalidConfiguration(msg) => {
160 write!(f, "Invalid configuration: {}", msg)
161 }
162 ConfigurationError::SerializationError(msg) => {
163 write!(f, "Serialization error: {}", msg)
164 }
165 ConfigurationError::InvalidValue(msg) => write!(f, "Invalid value: {}", msg),
166 ConfigurationError::MissingConfiguration(msg) => {
167 write!(f, "Missing configuration: {}", msg)
168 }
169 }
170 }
171}
172
173impl std::error::Error for ConfigurationError {}
174
175#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
177pub struct ConfigFile {
178 pub project: ProjectConfig,
179 pub flight_levels: FlightLevelConfig,
180}
181
182#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
183pub struct ProjectConfig {
184 pub prefix: String,
185}
186
187impl ConfigFile {
188 pub fn new(prefix: String, flight_levels: FlightLevelConfig) -> Result<Self, ConfigurationError> {
190 if !prefix.chars().all(|c| c.is_ascii_uppercase()) || prefix.len() < 2 || prefix.len() > 8 {
192 return Err(ConfigurationError::InvalidValue(
193 "Project prefix must be 2-8 uppercase letters".to_string(),
194 ));
195 }
196
197 Ok(Self {
198 project: ProjectConfig { prefix },
199 flight_levels,
200 })
201 }
202
203 pub fn load<P: AsRef<std::path::Path>>(path: P) -> Result<Self, ConfigurationError> {
205 let content = std::fs::read_to_string(path.as_ref()).map_err(|e| {
206 ConfigurationError::SerializationError(format!("Failed to read config file: {}", e))
207 })?;
208
209 toml::from_str(&content).map_err(|e| {
210 ConfigurationError::SerializationError(format!("Failed to parse TOML: {}", e))
211 })
212 }
213
214 pub fn save<P: AsRef<std::path::Path>>(&self, path: P) -> Result<(), ConfigurationError> {
216 let content = toml::to_string_pretty(self).map_err(|e| {
217 ConfigurationError::SerializationError(format!("Failed to serialize config: {}", e))
218 })?;
219
220 std::fs::write(path.as_ref(), content).map_err(|e| {
221 ConfigurationError::SerializationError(format!("Failed to write config file: {}", e))
222 })?;
223
224 Ok(())
225 }
226
227 pub fn default_with_prefix(prefix: String) -> Result<Self, ConfigurationError> {
229 Self::new(prefix, FlightLevelConfig::streamlined())
230 }
231
232 pub fn prefix(&self) -> &str {
234 &self.project.prefix
235 }
236
237 pub fn flight_levels(&self) -> &FlightLevelConfig {
239 &self.flight_levels
240 }
241}
242
243impl Default for ConfigFile {
244 fn default() -> Self {
245 Self {
246 project: ProjectConfig {
247 prefix: "PROJ".to_string(),
248 },
249 flight_levels: FlightLevelConfig::streamlined(),
250 }
251 }
252}
253
254#[cfg(test)]
255mod tests {
256 use super::*;
257
258 #[test]
259 fn test_preset_configurations() {
260 let full = FlightLevelConfig::full();
261 assert!(full.strategies_enabled);
262 assert!(full.initiatives_enabled);
263 assert_eq!(full.preset_name(), "full");
264
265 let streamlined = FlightLevelConfig::streamlined();
266 assert!(!streamlined.strategies_enabled);
267 assert!(streamlined.initiatives_enabled);
268 assert_eq!(streamlined.preset_name(), "streamlined");
269
270 let direct = FlightLevelConfig::direct();
271 assert!(!direct.strategies_enabled);
272 assert!(!direct.initiatives_enabled);
273 assert_eq!(direct.preset_name(), "direct");
274 }
275
276 #[test]
277 fn test_configuration_validation() {
278 assert!(FlightLevelConfig::new(true, true).is_ok());
280 assert!(FlightLevelConfig::new(false, true).is_ok());
281 assert!(FlightLevelConfig::new(false, false).is_ok());
282
283 assert!(FlightLevelConfig::new(true, false).is_err());
285 }
286
287 #[test]
288 fn test_document_type_allowed() {
289 let full = FlightLevelConfig::full();
290 assert!(full.is_document_type_allowed(DocumentType::Vision));
291 assert!(full.is_document_type_allowed(DocumentType::Strategy));
292 assert!(full.is_document_type_allowed(DocumentType::Initiative));
293 assert!(full.is_document_type_allowed(DocumentType::Task));
294 assert!(full.is_document_type_allowed(DocumentType::Adr));
295
296 let streamlined = FlightLevelConfig::streamlined();
297 assert!(streamlined.is_document_type_allowed(DocumentType::Vision));
298 assert!(!streamlined.is_document_type_allowed(DocumentType::Strategy));
299 assert!(streamlined.is_document_type_allowed(DocumentType::Initiative));
300 assert!(streamlined.is_document_type_allowed(DocumentType::Task));
301 assert!(streamlined.is_document_type_allowed(DocumentType::Adr));
302
303 let direct = FlightLevelConfig::direct();
304 assert!(direct.is_document_type_allowed(DocumentType::Vision));
305 assert!(!direct.is_document_type_allowed(DocumentType::Strategy));
306 assert!(!direct.is_document_type_allowed(DocumentType::Initiative));
307 assert!(direct.is_document_type_allowed(DocumentType::Task));
308 assert!(direct.is_document_type_allowed(DocumentType::Adr));
309 }
310
311 #[test]
312 fn test_parent_type_resolution() {
313 let full = FlightLevelConfig::full();
314 assert_eq!(full.get_parent_type(DocumentType::Vision), None);
315 assert_eq!(
316 full.get_parent_type(DocumentType::Strategy),
317 Some(DocumentType::Vision)
318 );
319 assert_eq!(
320 full.get_parent_type(DocumentType::Initiative),
321 Some(DocumentType::Strategy)
322 );
323 assert_eq!(
324 full.get_parent_type(DocumentType::Task),
325 Some(DocumentType::Initiative)
326 );
327 assert_eq!(full.get_parent_type(DocumentType::Adr), None);
328
329 let streamlined = FlightLevelConfig::streamlined();
330 assert_eq!(
331 streamlined.get_parent_type(DocumentType::Initiative),
332 Some(DocumentType::Vision)
333 );
334 assert_eq!(
335 streamlined.get_parent_type(DocumentType::Task),
336 Some(DocumentType::Initiative)
337 );
338
339 let direct = FlightLevelConfig::direct();
340 assert_eq!(
341 direct.get_parent_type(DocumentType::Task),
342 Some(DocumentType::Vision)
343 );
344 }
345
346 #[test]
347 fn test_enabled_document_types() {
348 let full = FlightLevelConfig::full();
349 let full_types = full.enabled_document_types();
350 assert_eq!(
351 full_types,
352 vec![
353 DocumentType::Vision,
354 DocumentType::Strategy,
355 DocumentType::Initiative,
356 DocumentType::Task,
357 DocumentType::Adr
358 ]
359 );
360
361 let streamlined = FlightLevelConfig::streamlined();
362 let streamlined_types = streamlined.enabled_document_types();
363 assert_eq!(
364 streamlined_types,
365 vec![
366 DocumentType::Vision,
367 DocumentType::Initiative,
368 DocumentType::Task,
369 DocumentType::Adr
370 ]
371 );
372
373 let direct = FlightLevelConfig::direct();
374 let direct_types = direct.enabled_document_types();
375 assert_eq!(
376 direct_types,
377 vec![DocumentType::Vision, DocumentType::Task, DocumentType::Adr]
378 );
379 }
380
381 #[test]
382 fn test_hierarchy_display() {
383 assert_eq!(
384 FlightLevelConfig::full().hierarchy_display(),
385 "Vision → Strategy → Initiative → Task"
386 );
387 assert_eq!(
388 FlightLevelConfig::streamlined().hierarchy_display(),
389 "Vision → Initiative → Task"
390 );
391 assert_eq!(
392 FlightLevelConfig::direct().hierarchy_display(),
393 "Vision → Task"
394 );
395 }
396
397 #[test]
398 fn test_serialization() {
399 let config = FlightLevelConfig::streamlined();
400 let json = serde_json::to_string(&config).unwrap();
401 let deserialized: FlightLevelConfig = serde_json::from_str(&json).unwrap();
402 assert_eq!(config, deserialized);
403 }
404
405 #[test]
406 fn test_config_file_creation() {
407 let config = ConfigFile::new("TEST".to_string(), FlightLevelConfig::streamlined()).unwrap();
408 assert_eq!(config.prefix(), "TEST");
409 assert_eq!(config.flight_levels(), &FlightLevelConfig::streamlined());
410 }
411
412 #[test]
413 fn test_config_file_validation() {
414 assert!(ConfigFile::new("AB".to_string(), FlightLevelConfig::streamlined()).is_ok());
416 assert!(ConfigFile::new("ABCDEFGH".to_string(), FlightLevelConfig::streamlined()).is_ok());
417 assert!(ConfigFile::new("METIS".to_string(), FlightLevelConfig::streamlined()).is_ok());
418
419 assert!(ConfigFile::new("A".to_string(), FlightLevelConfig::streamlined()).is_err()); assert!(ConfigFile::new("ABCDEFGHI".to_string(), FlightLevelConfig::streamlined()).is_err()); assert!(ConfigFile::new("ab".to_string(), FlightLevelConfig::streamlined()).is_err()); assert!(ConfigFile::new("A1".to_string(), FlightLevelConfig::streamlined()).is_err()); assert!(ConfigFile::new("A-B".to_string(), FlightLevelConfig::streamlined()).is_err()); }
426
427 #[test]
428 fn test_config_file_save_and_load() {
429 use tempfile::NamedTempFile;
430
431 let original_config = ConfigFile::new("METIS".to_string(), FlightLevelConfig::full()).unwrap();
432
433 let temp_file = NamedTempFile::new().unwrap();
435 let temp_path = temp_file.path().to_path_buf();
436
437 original_config.save(&temp_path).unwrap();
439
440 let loaded_config = ConfigFile::load(&temp_path).unwrap();
442
443 assert_eq!(original_config, loaded_config);
445 assert_eq!(loaded_config.prefix(), "METIS");
446 assert_eq!(loaded_config.flight_levels(), &FlightLevelConfig::full());
447 }
448
449 #[test]
450 fn test_config_file_toml_format() {
451 use tempfile::NamedTempFile;
452
453 let config = ConfigFile::new("METIS".to_string(), FlightLevelConfig::streamlined()).unwrap();
454
455 let temp_file = NamedTempFile::new().unwrap();
457 let temp_path = temp_file.path();
458
459 config.save(temp_path).unwrap();
461
462 let content = std::fs::read_to_string(temp_path).unwrap();
464
465 assert!(content.contains("[project]"));
467 assert!(content.contains("prefix = \"METIS\""));
468 assert!(content.contains("[flight_levels]"));
469 assert!(content.contains("strategies_enabled = false"));
470 assert!(content.contains("initiatives_enabled = true"));
471 }
472
473 #[test]
474 fn test_config_file_default() {
475 let config = ConfigFile::default();
476 assert_eq!(config.prefix(), "PROJ");
477 assert_eq!(config.flight_levels(), &FlightLevelConfig::streamlined());
478 }
479
480 #[test]
481 fn test_config_file_default_with_prefix() {
482 let config = ConfigFile::default_with_prefix("CUSTOM".to_string()).unwrap();
483 assert_eq!(config.prefix(), "CUSTOM");
484 assert_eq!(config.flight_levels(), &FlightLevelConfig::streamlined());
485 }
486}