metis_core/domain/
configuration.rs

1use crate::domain::documents::types::DocumentType;
2use serde::{Deserialize, Serialize};
3use std::fmt;
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(
17        strategies_enabled: bool,
18        initiatives_enabled: bool,
19    ) -> Result<Self, ConfigurationError> {
20        // Validation: If initiatives are disabled, strategies must also be disabled
21        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    /// Full flight levels: Vision → Strategy → Initiative → Task
34    pub fn full() -> Self {
35        Self {
36            strategies_enabled: true,
37            initiatives_enabled: true,
38        }
39    }
40
41    /// Streamlined flight levels: Vision → Initiative → Task
42    pub fn streamlined() -> Self {
43        Self {
44            strategies_enabled: false,
45            initiatives_enabled: true,
46        }
47    }
48
49    /// Direct flight levels: Vision → Task
50    pub fn direct() -> Self {
51        Self {
52            strategies_enabled: false,
53            initiatives_enabled: false,
54        }
55    }
56
57    /// Check if a document type is allowed in this configuration
58    pub fn is_document_type_allowed(&self, doc_type: DocumentType) -> bool {
59        match doc_type {
60            DocumentType::Vision | DocumentType::Adr => true, // Always allowed
61            DocumentType::Task => true,                       // Always allowed
62            DocumentType::Strategy => self.strategies_enabled,
63            DocumentType::Initiative => self.initiatives_enabled,
64        }
65    }
66
67    /// Get the parent document type for a given document type in this configuration
68    pub fn get_parent_type(&self, doc_type: DocumentType) -> Option<DocumentType> {
69        match doc_type {
70            DocumentType::Vision | DocumentType::Adr => None, // Top level documents
71            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    /// Get the configuration name/preset
90    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", // This shouldn't happen due to validation
96        }
97    }
98
99    /// Get enabled document types in hierarchical order
100    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); // ADRs are always available
113
114        types
115    }
116
117    /// Get the hierarchy display string
118    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/// Configuration validation errors
148#[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#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_preset_configurations() {
181        let full = FlightLevelConfig::full();
182        assert!(full.strategies_enabled);
183        assert!(full.initiatives_enabled);
184        assert_eq!(full.preset_name(), "full");
185
186        let streamlined = FlightLevelConfig::streamlined();
187        assert!(!streamlined.strategies_enabled);
188        assert!(streamlined.initiatives_enabled);
189        assert_eq!(streamlined.preset_name(), "streamlined");
190
191        let direct = FlightLevelConfig::direct();
192        assert!(!direct.strategies_enabled);
193        assert!(!direct.initiatives_enabled);
194        assert_eq!(direct.preset_name(), "direct");
195    }
196
197    #[test]
198    fn test_configuration_validation() {
199        // Valid configurations
200        assert!(FlightLevelConfig::new(true, true).is_ok());
201        assert!(FlightLevelConfig::new(false, true).is_ok());
202        assert!(FlightLevelConfig::new(false, false).is_ok());
203
204        // Invalid configuration: strategies enabled but initiatives disabled
205        assert!(FlightLevelConfig::new(true, false).is_err());
206    }
207
208    #[test]
209    fn test_document_type_allowed() {
210        let full = FlightLevelConfig::full();
211        assert!(full.is_document_type_allowed(DocumentType::Vision));
212        assert!(full.is_document_type_allowed(DocumentType::Strategy));
213        assert!(full.is_document_type_allowed(DocumentType::Initiative));
214        assert!(full.is_document_type_allowed(DocumentType::Task));
215        assert!(full.is_document_type_allowed(DocumentType::Adr));
216
217        let streamlined = FlightLevelConfig::streamlined();
218        assert!(streamlined.is_document_type_allowed(DocumentType::Vision));
219        assert!(!streamlined.is_document_type_allowed(DocumentType::Strategy));
220        assert!(streamlined.is_document_type_allowed(DocumentType::Initiative));
221        assert!(streamlined.is_document_type_allowed(DocumentType::Task));
222        assert!(streamlined.is_document_type_allowed(DocumentType::Adr));
223
224        let direct = FlightLevelConfig::direct();
225        assert!(direct.is_document_type_allowed(DocumentType::Vision));
226        assert!(!direct.is_document_type_allowed(DocumentType::Strategy));
227        assert!(!direct.is_document_type_allowed(DocumentType::Initiative));
228        assert!(direct.is_document_type_allowed(DocumentType::Task));
229        assert!(direct.is_document_type_allowed(DocumentType::Adr));
230    }
231
232    #[test]
233    fn test_parent_type_resolution() {
234        let full = FlightLevelConfig::full();
235        assert_eq!(full.get_parent_type(DocumentType::Vision), None);
236        assert_eq!(
237            full.get_parent_type(DocumentType::Strategy),
238            Some(DocumentType::Vision)
239        );
240        assert_eq!(
241            full.get_parent_type(DocumentType::Initiative),
242            Some(DocumentType::Strategy)
243        );
244        assert_eq!(
245            full.get_parent_type(DocumentType::Task),
246            Some(DocumentType::Initiative)
247        );
248        assert_eq!(full.get_parent_type(DocumentType::Adr), None);
249
250        let streamlined = FlightLevelConfig::streamlined();
251        assert_eq!(
252            streamlined.get_parent_type(DocumentType::Initiative),
253            Some(DocumentType::Vision)
254        );
255        assert_eq!(
256            streamlined.get_parent_type(DocumentType::Task),
257            Some(DocumentType::Initiative)
258        );
259
260        let direct = FlightLevelConfig::direct();
261        assert_eq!(
262            direct.get_parent_type(DocumentType::Task),
263            Some(DocumentType::Vision)
264        );
265    }
266
267    #[test]
268    fn test_enabled_document_types() {
269        let full = FlightLevelConfig::full();
270        let full_types = full.enabled_document_types();
271        assert_eq!(
272            full_types,
273            vec![
274                DocumentType::Vision,
275                DocumentType::Strategy,
276                DocumentType::Initiative,
277                DocumentType::Task,
278                DocumentType::Adr
279            ]
280        );
281
282        let streamlined = FlightLevelConfig::streamlined();
283        let streamlined_types = streamlined.enabled_document_types();
284        assert_eq!(
285            streamlined_types,
286            vec![
287                DocumentType::Vision,
288                DocumentType::Initiative,
289                DocumentType::Task,
290                DocumentType::Adr
291            ]
292        );
293
294        let direct = FlightLevelConfig::direct();
295        let direct_types = direct.enabled_document_types();
296        assert_eq!(
297            direct_types,
298            vec![DocumentType::Vision, DocumentType::Task, DocumentType::Adr]
299        );
300    }
301
302    #[test]
303    fn test_hierarchy_display() {
304        assert_eq!(
305            FlightLevelConfig::full().hierarchy_display(),
306            "Vision → Strategy → Initiative → Task"
307        );
308        assert_eq!(
309            FlightLevelConfig::streamlined().hierarchy_display(),
310            "Vision → Initiative → Task"
311        );
312        assert_eq!(
313            FlightLevelConfig::direct().hierarchy_display(),
314            "Vision → Task"
315        );
316    }
317
318    #[test]
319    fn test_serialization() {
320        let config = FlightLevelConfig::streamlined();
321        let json = serde_json::to_string(&config).unwrap();
322        let deserialized: FlightLevelConfig = serde_json::from_str(&json).unwrap();
323        assert_eq!(config, deserialized);
324    }
325}