Skip to main content

kmp_domain/value_objects/
dimension_selection.rs

1use std::collections::BTreeSet;
2
3use crate::MemoryDimensionIdentity;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum DimensionSelectionMode {
7    All,
8    Only,
9    Except,
10}
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum DimensionScopeMode {
14    CurrentAbout,
15    Abouts,
16    AllAbouts,
17}
18
19#[derive(Debug, Clone, PartialEq, Eq)]
20pub struct DimensionSelection {
21    mode: DimensionSelectionMode,
22    dimensions: BTreeSet<String>,
23    scope_mode: DimensionScopeMode,
24    abouts: BTreeSet<String>,
25    scope_ids: BTreeSet<String>,
26}
27
28impl DimensionSelection {
29    pub fn all() -> Self {
30        Self {
31            mode: DimensionSelectionMode::All,
32            dimensions: BTreeSet::new(),
33            scope_mode: DimensionScopeMode::CurrentAbout,
34            abouts: BTreeSet::new(),
35            scope_ids: BTreeSet::new(),
36        }
37    }
38
39    pub fn only(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
40        Self {
41            mode: DimensionSelectionMode::Only,
42            dimensions: normalize_dimensions(values),
43            scope_mode: DimensionScopeMode::CurrentAbout,
44            abouts: BTreeSet::new(),
45            scope_ids: BTreeSet::new(),
46        }
47    }
48
49    pub fn except(values: impl IntoIterator<Item = impl Into<String>>) -> Self {
50        Self {
51            mode: DimensionSelectionMode::Except,
52            dimensions: normalize_dimensions(values),
53            scope_mode: DimensionScopeMode::CurrentAbout,
54            abouts: BTreeSet::new(),
55            scope_ids: BTreeSet::new(),
56        }
57    }
58
59    pub fn mode(&self) -> DimensionSelectionMode {
60        self.mode
61    }
62
63    pub fn dimensions(&self) -> &BTreeSet<String> {
64        &self.dimensions
65    }
66
67    pub fn scope_mode(&self) -> DimensionScopeMode {
68        self.scope_mode
69    }
70
71    pub fn abouts(&self) -> &BTreeSet<String> {
72        &self.abouts
73    }
74
75    pub fn scope_ids(&self) -> &BTreeSet<String> {
76        &self.scope_ids
77    }
78
79    pub fn with_scope_ids(
80        mut self,
81        scope_ids: impl IntoIterator<Item = impl Into<String>>,
82    ) -> Self {
83        self.scope_ids = normalize_dimensions(scope_ids);
84        self
85    }
86
87    pub fn with_current_about_scope(mut self) -> Self {
88        self.scope_mode = DimensionScopeMode::CurrentAbout;
89        self.abouts.clear();
90        self
91    }
92
93    pub fn with_about_scope(mut self, abouts: impl IntoIterator<Item = impl Into<String>>) -> Self {
94        self.scope_mode = DimensionScopeMode::Abouts;
95        self.abouts = normalize_dimensions(abouts);
96        self
97    }
98
99    pub fn with_all_about_scope(mut self) -> Self {
100        self.scope_mode = DimensionScopeMode::AllAbouts;
101        self.abouts.clear();
102        self
103    }
104
105    pub fn resolve_current_about(&self, current_about: &str) -> Self {
106        if self.scope_mode != DimensionScopeMode::CurrentAbout {
107            return self.clone();
108        }
109        self.clone().with_about_scope([current_about.to_string()])
110    }
111
112    pub fn includes(&self, dimension: &str) -> bool {
113        self.includes_dimension(dimension)
114    }
115
116    pub fn includes_dimension(&self, dimension: &str) -> bool {
117        match self.mode {
118            DimensionSelectionMode::All => true,
119            DimensionSelectionMode::Only => self.dimensions.contains(dimension),
120            DimensionSelectionMode::Except => !self.dimensions.contains(dimension),
121        }
122    }
123
124    pub fn includes_coordinate(&self, dimension: &str, scope_id: &str) -> bool {
125        self.includes_dimension(dimension)
126            && self.includes_scope(scope_id)
127            && self.includes_dimension_scope(scope_id)
128    }
129
130    pub fn includes_scope(&self, scope_id: &str) -> bool {
131        match self.scope_mode {
132            DimensionScopeMode::AllAbouts => true,
133            DimensionScopeMode::CurrentAbout => true,
134            DimensionScopeMode::Abouts => MemoryDimensionIdentity::parse(scope_id)
135                .map(|identity| self.abouts.contains(identity.about()))
136                .unwrap_or(false),
137        }
138    }
139
140    pub fn includes_dimension_scope(&self, scope_id: &str) -> bool {
141        if self.scope_ids.is_empty() {
142            return true;
143        }
144        let scope_id = scope_id.trim();
145        self.scope_ids.contains(scope_id)
146            || MemoryDimensionIdentity::parse(scope_id)
147                .map(|identity| self.scope_ids.contains(identity.dimension_id()))
148                .unwrap_or(false)
149    }
150}
151
152impl Default for DimensionSelection {
153    fn default() -> Self {
154        Self::all()
155    }
156}
157
158fn normalize_dimensions(values: impl IntoIterator<Item = impl Into<String>>) -> BTreeSet<String> {
159    values
160        .into_iter()
161        .map(Into::into)
162        .map(|value| value.trim().to_string())
163        .filter(|value| !value.is_empty())
164        .collect()
165}
166
167#[cfg(test)]
168mod tests {
169    use super::DimensionSelection;
170
171    #[test]
172    fn selection_filters_dimensions() {
173        let only = DimensionSelection::only(["conversation", "entity", " "]);
174        assert!(only.includes("conversation"));
175        assert!(!only.includes("benchmark_record"));
176
177        let except = DimensionSelection::except(["entity"]);
178        assert!(except.includes("conversation"));
179        assert!(!except.includes("entity"));
180    }
181
182    #[test]
183    fn selection_filters_about_scope_when_resolved() {
184        let current = DimensionSelection::only(["timeline"]).resolve_current_about("question:a");
185        assert!(current.includes_coordinate("timeline", "about:question:a:dimension:timeline"));
186        assert!(!current.includes_coordinate("timeline", "about:question:b:dimension:timeline"));
187
188        let all = DimensionSelection::only(["timeline"]).with_all_about_scope();
189        assert!(all.includes_coordinate("timeline", "about:question:b:dimension:timeline"));
190    }
191
192    #[test]
193    fn selection_filters_exact_dimension_scope_ids() {
194        let local = DimensionSelection::only(["conversation"])
195            .resolve_current_about("question:a")
196            .with_scope_ids(["conversation:alpha"]);
197        assert!(local.includes_coordinate(
198            "conversation",
199            "about:question:a:dimension:conversation:alpha"
200        ));
201        assert!(!local.includes_coordinate(
202            "conversation",
203            "about:question:a:dimension:conversation:beta"
204        ));
205        assert!(
206            !local.includes_coordinate("topic", "about:question:a:dimension:conversation:alpha")
207        );
208
209        let namespaced = DimensionSelection::all()
210            .with_scope_ids(["about:question:a:dimension:conversation:alpha"]);
211        assert!(namespaced.includes_coordinate(
212            "conversation",
213            "about:question:a:dimension:conversation:alpha"
214        ));
215        assert!(!namespaced.includes_coordinate(
216            "conversation",
217            "about:question:b:dimension:conversation:alpha"
218        ));
219    }
220}