kmp_domain/value_objects/
dimension_selection.rs1use 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}