elasticube_core/cube/
hierarchy.rs

1//! Hierarchy types for drill-down/roll-up operations
2
3use serde::{Deserialize, Serialize};
4
5/// Represents a hierarchy in the cube
6///
7/// A hierarchy defines a drill-down path through related dimensions
8/// (e.g., Year → Quarter → Month → Day for time-based analysis).
9#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
10pub struct Hierarchy {
11    /// Name of the hierarchy
12    name: String,
13
14    /// Ordered list of dimension names from coarsest to finest granularity
15    /// Example: ["year", "quarter", "month", "day"]
16    levels: Vec<String>,
17
18    /// User-provided description
19    description: Option<String>,
20}
21
22impl Hierarchy {
23    /// Create a new hierarchy
24    ///
25    /// # Arguments
26    ///
27    /// * `name` - The name of the hierarchy
28    /// * `levels` - Ordered list of dimension names from coarse to fine granularity
29    ///
30    /// # Example
31    ///
32    /// ```rust,ignore
33    /// let time_hierarchy = Hierarchy::new(
34    ///     "time",
35    ///     vec!["year", "quarter", "month", "day"]
36    /// );
37    /// ```
38    pub fn new(name: impl Into<String>, levels: Vec<String>) -> Self {
39        Self {
40            name: name.into(),
41            levels,
42            description: None,
43        }
44    }
45
46    /// Create a new hierarchy with description
47    pub fn with_config(
48        name: impl Into<String>,
49        levels: Vec<String>,
50        description: Option<String>,
51    ) -> Self {
52        Self {
53            name: name.into(),
54            levels,
55            description,
56        }
57    }
58
59    /// Get the hierarchy name
60    pub fn name(&self) -> &str {
61        &self.name
62    }
63
64    /// Get the levels
65    pub fn levels(&self) -> &[String] {
66        &self.levels
67    }
68
69    /// Get the description
70    pub fn description(&self) -> Option<&str> {
71        self.description.as_deref()
72    }
73
74    /// Get the number of levels
75    pub fn depth(&self) -> usize {
76        self.levels.len()
77    }
78
79    /// Get a level by index (0 = coarsest, depth-1 = finest)
80    pub fn level_at(&self, index: usize) -> Option<&str> {
81        self.levels.get(index).map(|s| s.as_str())
82    }
83
84    /// Get the coarsest (top) level
85    pub fn top_level(&self) -> Option<&str> {
86        self.levels.first().map(|s| s.as_str())
87    }
88
89    /// Get the finest (bottom) level
90    pub fn bottom_level(&self) -> Option<&str> {
91        self.levels.last().map(|s| s.as_str())
92    }
93
94    /// Get the parent level of a given level
95    pub fn parent_of(&self, level: &str) -> Option<&str> {
96        self.levels
97            .iter()
98            .position(|l| l == level)
99            .and_then(|idx| {
100                if idx > 0 {
101                    self.levels.get(idx - 1).map(|s| s.as_str())
102                } else {
103                    None
104                }
105            })
106    }
107
108    /// Get the child level of a given level
109    pub fn child_of(&self, level: &str) -> Option<&str> {
110        self.levels
111            .iter()
112            .position(|l| l == level)
113            .and_then(|idx| self.levels.get(idx + 1).map(|s| s.as_str()))
114    }
115
116    /// Check if a level exists in this hierarchy
117    pub fn contains_level(&self, level: &str) -> bool {
118        self.levels.iter().any(|l| l == level)
119    }
120
121    /// Get all ancestor levels of a given level (from top to parent)
122    pub fn ancestors_of(&self, level: &str) -> Vec<&str> {
123        if let Some(idx) = self.levels.iter().position(|l| l == level) {
124            self.levels[..idx].iter().map(|s| s.as_str()).collect()
125        } else {
126            vec![]
127        }
128    }
129
130    /// Get all descendant levels of a given level (from child to bottom)
131    pub fn descendants_of(&self, level: &str) -> Vec<&str> {
132        if let Some(idx) = self.levels.iter().position(|l| l == level) {
133            self.levels[idx + 1..]
134                .iter()
135                .map(|s| s.as_str())
136                .collect()
137        } else {
138            vec![]
139        }
140    }
141
142    /// Set the description
143    pub fn set_description(&mut self, description: impl Into<String>) {
144        self.description = Some(description.into());
145    }
146
147    /// Builder-style: set description
148    pub fn with_description(mut self, description: impl Into<String>) -> Self {
149        self.description = Some(description.into());
150        self
151    }
152
153    /// Validate the hierarchy
154    pub fn validate(&self) -> Result<(), String> {
155        if self.levels.is_empty() {
156            return Err("Hierarchy must have at least one level".to_string());
157        }
158
159        // Check for duplicate levels
160        let mut seen = std::collections::HashSet::new();
161        for level in &self.levels {
162            if !seen.insert(level) {
163                return Err(format!("Duplicate level '{}' in hierarchy", level));
164            }
165        }
166
167        Ok(())
168    }
169}
170
171#[cfg(test)]
172mod tests {
173    use super::*;
174
175    #[test]
176    fn test_hierarchy_creation() {
177        let hierarchy = Hierarchy::new(
178            "time",
179            vec![
180                "year".to_string(),
181                "quarter".to_string(),
182                "month".to_string(),
183                "day".to_string(),
184            ],
185        );
186
187        assert_eq!(hierarchy.name(), "time");
188        assert_eq!(hierarchy.depth(), 4);
189        assert_eq!(hierarchy.top_level(), Some("year"));
190        assert_eq!(hierarchy.bottom_level(), Some("day"));
191    }
192
193    #[test]
194    fn test_hierarchy_navigation() {
195        let hierarchy = Hierarchy::new(
196            "geography",
197            vec![
198                "country".to_string(),
199                "state".to_string(),
200                "city".to_string(),
201            ],
202        );
203
204        assert_eq!(hierarchy.parent_of("state"), Some("country"));
205        assert_eq!(hierarchy.parent_of("country"), None);
206        assert_eq!(hierarchy.child_of("country"), Some("state"));
207        assert_eq!(hierarchy.child_of("city"), None);
208    }
209
210    #[test]
211    fn test_hierarchy_ancestors_descendants() {
212        let hierarchy = Hierarchy::new(
213            "time",
214            vec![
215                "year".to_string(),
216                "quarter".to_string(),
217                "month".to_string(),
218                "day".to_string(),
219            ],
220        );
221
222        assert_eq!(hierarchy.ancestors_of("month"), vec!["year", "quarter"]);
223        assert_eq!(hierarchy.descendants_of("quarter"), vec!["month", "day"]);
224        assert_eq!(hierarchy.ancestors_of("year"), Vec::<&str>::new());
225        assert_eq!(hierarchy.descendants_of("day"), Vec::<&str>::new());
226    }
227
228    #[test]
229    fn test_hierarchy_validation() {
230        let valid = Hierarchy::new(
231            "test",
232            vec!["level1".to_string(), "level2".to_string()],
233        );
234        assert!(valid.validate().is_ok());
235
236        let empty = Hierarchy::new("test", vec![]);
237        assert!(empty.validate().is_err());
238
239        let duplicate = Hierarchy::new(
240            "test",
241            vec!["level1".to_string(), "level1".to_string()],
242        );
243        assert!(duplicate.validate().is_err());
244    }
245}