metis_core/domain/
configuration.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use crate::domain::documents::types::DocumentType;
4
5/// Flight level configuration defining which levels are enabled
6#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
7pub struct FlightLevelConfig {
8    /// Whether strategy level is enabled
9    pub strategies_enabled: bool,
10    /// Whether initiative level is enabled
11    pub initiatives_enabled: bool,
12}
13
14impl FlightLevelConfig {
15    /// Create a new configuration
16    pub fn new(strategies_enabled: bool, initiatives_enabled: bool) -> Result<Self, ConfigurationError> {
17        // Validation: If initiatives are disabled, strategies must also be disabled
18        if !initiatives_enabled && strategies_enabled {
19            return Err(ConfigurationError::InvalidConfiguration(
20                "Cannot enable strategies without initiatives - this would create a gap in the hierarchy".to_string()
21            ));
22        }
23        
24        Ok(Self {
25            strategies_enabled,
26            initiatives_enabled,
27        })
28    }
29
30    /// Full flight levels: Vision → Strategy → Initiative → Task
31    pub fn full() -> Self {
32        Self {
33            strategies_enabled: true,
34            initiatives_enabled: true,
35        }
36    }
37
38    /// Streamlined flight levels: Vision → Initiative → Task
39    pub fn streamlined() -> Self {
40        Self {
41            strategies_enabled: false,
42            initiatives_enabled: true,
43        }
44    }
45
46    /// Direct flight levels: Vision → Task
47    pub fn direct() -> Self {
48        Self {
49            strategies_enabled: false,
50            initiatives_enabled: false,
51        }
52    }
53
54    /// Check if a document type is allowed in this configuration
55    pub fn is_document_type_allowed(&self, doc_type: DocumentType) -> bool {
56        match doc_type {
57            DocumentType::Vision | DocumentType::Adr => true, // Always allowed
58            DocumentType::Task => true, // Always allowed
59            DocumentType::Strategy => self.strategies_enabled,
60            DocumentType::Initiative => self.initiatives_enabled,
61        }
62    }
63
64    /// Get the parent document type for a given document type in this configuration
65    pub fn get_parent_type(&self, doc_type: DocumentType) -> Option<DocumentType> {
66        match doc_type {
67            DocumentType::Vision | DocumentType::Adr => None, // Top level documents
68            DocumentType::Strategy => Some(DocumentType::Vision),
69            DocumentType::Initiative => {
70                if self.strategies_enabled {
71                    Some(DocumentType::Strategy)
72                } else {
73                    Some(DocumentType::Vision)
74                }
75            }
76            DocumentType::Task => {
77                if self.initiatives_enabled {
78                    Some(DocumentType::Initiative)
79                } else {
80                    Some(DocumentType::Vision)
81                }
82            }
83        }
84    }
85
86    /// Get the configuration name/preset
87    pub fn preset_name(&self) -> &'static str {
88        match (self.strategies_enabled, self.initiatives_enabled) {
89            (true, true) => "full",
90            (false, true) => "streamlined", 
91            (false, false) => "direct",
92            (true, false) => "invalid", // This shouldn't happen due to validation
93        }
94    }
95
96    /// Get enabled document types in hierarchical order
97    pub fn enabled_document_types(&self) -> Vec<DocumentType> {
98        let mut types = vec![DocumentType::Vision];
99        
100        if self.strategies_enabled {
101            types.push(DocumentType::Strategy);
102        }
103        
104        if self.initiatives_enabled {
105            types.push(DocumentType::Initiative);
106        }
107        
108        types.push(DocumentType::Task);
109        types.push(DocumentType::Adr); // ADRs are always available
110        
111        types
112    }
113
114    /// Get the hierarchy display string
115    pub fn hierarchy_display(&self) -> String {
116        let mut hierarchy = vec!["Vision"];
117        
118        if self.strategies_enabled {
119            hierarchy.push("Strategy");
120        }
121        
122        if self.initiatives_enabled {
123            hierarchy.push("Initiative");
124        }
125        
126        hierarchy.push("Task");
127        
128        hierarchy.join(" → ")
129    }
130}
131
132impl Default for FlightLevelConfig {
133    fn default() -> Self {
134        Self::full()
135    }
136}
137
138impl fmt::Display for FlightLevelConfig {
139    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
140        write!(f, "{}", self.preset_name())
141    }
142}
143
144/// Configuration validation errors
145#[derive(Debug, Clone, PartialEq, Eq)]
146pub enum ConfigurationError {
147    InvalidConfiguration(String),
148    SerializationError(String),
149}
150
151impl fmt::Display for ConfigurationError {
152    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
153        match self {
154            ConfigurationError::InvalidConfiguration(msg) => write!(f, "Invalid configuration: {}", msg),
155            ConfigurationError::SerializationError(msg) => write!(f, "Serialization error: {}", msg),
156        }
157    }
158}
159
160impl std::error::Error for ConfigurationError {}
161
162#[cfg(test)]
163mod tests {
164    use super::*;
165
166    #[test]
167    fn test_preset_configurations() {
168        let full = FlightLevelConfig::full();
169        assert!(full.strategies_enabled);
170        assert!(full.initiatives_enabled);
171        assert_eq!(full.preset_name(), "full");
172
173        let streamlined = FlightLevelConfig::streamlined();
174        assert!(!streamlined.strategies_enabled);
175        assert!(streamlined.initiatives_enabled);
176        assert_eq!(streamlined.preset_name(), "streamlined");
177
178        let direct = FlightLevelConfig::direct();
179        assert!(!direct.strategies_enabled);
180        assert!(!direct.initiatives_enabled);
181        assert_eq!(direct.preset_name(), "direct");
182    }
183
184    #[test]
185    fn test_configuration_validation() {
186        // Valid configurations
187        assert!(FlightLevelConfig::new(true, true).is_ok());
188        assert!(FlightLevelConfig::new(false, true).is_ok());
189        assert!(FlightLevelConfig::new(false, false).is_ok());
190
191        // Invalid configuration: strategies enabled but initiatives disabled
192        assert!(FlightLevelConfig::new(true, false).is_err());
193    }
194
195    #[test]
196    fn test_document_type_allowed() {
197        let full = FlightLevelConfig::full();
198        assert!(full.is_document_type_allowed(DocumentType::Vision));
199        assert!(full.is_document_type_allowed(DocumentType::Strategy));
200        assert!(full.is_document_type_allowed(DocumentType::Initiative));
201        assert!(full.is_document_type_allowed(DocumentType::Task));
202        assert!(full.is_document_type_allowed(DocumentType::Adr));
203
204        let streamlined = FlightLevelConfig::streamlined();
205        assert!(streamlined.is_document_type_allowed(DocumentType::Vision));
206        assert!(!streamlined.is_document_type_allowed(DocumentType::Strategy));
207        assert!(streamlined.is_document_type_allowed(DocumentType::Initiative));
208        assert!(streamlined.is_document_type_allowed(DocumentType::Task));
209        assert!(streamlined.is_document_type_allowed(DocumentType::Adr));
210
211        let direct = FlightLevelConfig::direct();
212        assert!(direct.is_document_type_allowed(DocumentType::Vision));
213        assert!(!direct.is_document_type_allowed(DocumentType::Strategy));
214        assert!(!direct.is_document_type_allowed(DocumentType::Initiative));
215        assert!(direct.is_document_type_allowed(DocumentType::Task));
216        assert!(direct.is_document_type_allowed(DocumentType::Adr));
217    }
218
219    #[test]
220    fn test_parent_type_resolution() {
221        let full = FlightLevelConfig::full();
222        assert_eq!(full.get_parent_type(DocumentType::Vision), None);
223        assert_eq!(full.get_parent_type(DocumentType::Strategy), Some(DocumentType::Vision));
224        assert_eq!(full.get_parent_type(DocumentType::Initiative), Some(DocumentType::Strategy));
225        assert_eq!(full.get_parent_type(DocumentType::Task), Some(DocumentType::Initiative));
226        assert_eq!(full.get_parent_type(DocumentType::Adr), None);
227
228        let streamlined = FlightLevelConfig::streamlined();
229        assert_eq!(streamlined.get_parent_type(DocumentType::Initiative), Some(DocumentType::Vision));
230        assert_eq!(streamlined.get_parent_type(DocumentType::Task), Some(DocumentType::Initiative));
231
232        let direct = FlightLevelConfig::direct();
233        assert_eq!(direct.get_parent_type(DocumentType::Task), Some(DocumentType::Vision));
234    }
235
236    #[test]
237    fn test_enabled_document_types() {
238        let full = FlightLevelConfig::full();
239        let full_types = full.enabled_document_types();
240        assert_eq!(full_types, vec![
241            DocumentType::Vision,
242            DocumentType::Strategy,
243            DocumentType::Initiative,
244            DocumentType::Task,
245            DocumentType::Adr
246        ]);
247
248        let streamlined = FlightLevelConfig::streamlined();
249        let streamlined_types = streamlined.enabled_document_types();
250        assert_eq!(streamlined_types, vec![
251            DocumentType::Vision,
252            DocumentType::Initiative,
253            DocumentType::Task,
254            DocumentType::Adr
255        ]);
256
257        let direct = FlightLevelConfig::direct();
258        let direct_types = direct.enabled_document_types();
259        assert_eq!(direct_types, vec![
260            DocumentType::Vision,
261            DocumentType::Task,
262            DocumentType::Adr
263        ]);
264    }
265
266    #[test]
267    fn test_hierarchy_display() {
268        assert_eq!(FlightLevelConfig::full().hierarchy_display(), "Vision → Strategy → Initiative → Task");
269        assert_eq!(FlightLevelConfig::streamlined().hierarchy_display(), "Vision → Initiative → Task");
270        assert_eq!(FlightLevelConfig::direct().hierarchy_display(), "Vision → Task");
271    }
272
273    #[test]
274    fn test_serialization() {
275        let config = FlightLevelConfig::streamlined();
276        let json = serde_json::to_string(&config).unwrap();
277        let deserialized: FlightLevelConfig = serde_json::from_str(&json).unwrap();
278        assert_eq!(config, deserialized);
279    }
280}