1use std::fmt;
11
12use crate::identity::{NameKey, Quoted};
13use crate::model::DaxExpressionKind;
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
21pub enum Provenance {
22 Dax {
27 kind: DaxExpressionKind,
29 },
30 Binding(
33 Box<BindingEdge>,
35 ),
36 M,
38 Structural {
40 role: StructuralEdge,
42 },
43}
44
45#[derive(Debug, Clone, PartialEq, Eq, Hash)]
48pub struct BindingEdge {
49 pub kind: BindingSite,
51 pub report: Option<NameKey>,
53 pub page: Option<NameKey>,
55 pub visual: Option<NameKey>,
57 pub bookmark: Option<NameKey>,
60}
61
62impl Provenance {
63 pub(super) fn is_strong_pass_edge(&self) -> bool {
67 !matches!(
68 self,
69 Provenance::Structural {
70 role: StructuralEdge::RelationshipEndpoint
71 }
72 )
73 }
74
75 pub(super) fn is_weak_pass_edge(&self) -> bool {
79 !matches!(
80 self,
81 Provenance::Structural {
82 role: StructuralEdge::TableMember
83 }
84 )
85 }
86}
87
88impl fmt::Display for Provenance {
89 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
94 match self {
95 Provenance::Dax { kind } => f.write_str(dax_site(*kind)),
96 Provenance::Binding(edge) => {
97 let BindingEdge {
98 kind,
99 report,
100 page,
101 visual,
102 bookmark,
103 } = edge.as_ref();
104 write_site(f, kind)?;
105 if let Some(visual) = visual {
106 write!(f, " — visual {}", Quoted(visual.as_str()))?;
107 }
108 if let Some(page) = page {
109 write!(f, " on page {}", Quoted(page.as_str()))?;
110 }
111 if let Some(bookmark) = bookmark {
112 write!(f, " in bookmark {}", Quoted(bookmark.as_str()))?;
113 }
114 if let Some(report) = report {
115 write!(f, " in report {}", Quoted(report.as_str()))?;
116 }
117 Ok(())
118 }
119 Provenance::M => f.write_str("M expression"),
120 Provenance::Structural { role } => write!(f, "{role}"),
121 }
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Eq, Hash)]
129pub enum BindingSite {
130 FieldWell {
132 role: String,
134 },
135 Filter,
137 Sort,
139 Drillthrough,
141 ConditionalFormatting,
143 AltText,
145}
146
147fn write_site(f: &mut fmt::Formatter<'_>, site: &BindingSite) -> fmt::Result {
148 match site {
149 BindingSite::FieldWell { role } => {
150 write!(f, "field well {}", Quoted(role.as_str()))
151 }
152 BindingSite::Filter => f.write_str("filter"),
153 BindingSite::Sort => f.write_str("sort definition"),
154 BindingSite::Drillthrough => f.write_str("drillthrough parameter"),
155 BindingSite::ConditionalFormatting => f.write_str("conditional formatting"),
156 BindingSite::AltText => f.write_str("alt text"),
157 }
158}
159
160#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
162pub enum StructuralEdge {
163 TableMember,
166 TablePartition,
168 Relationship,
170 RelationshipEndpoint,
172 SortByColumn,
174 GroupByColumn,
176 HierarchyLevel,
178 EngineManaged,
182 RolePermission,
184}
185
186impl fmt::Display for StructuralEdge {
187 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
188 f.write_str(match self {
189 StructuralEdge::TableMember => "table member",
190 StructuralEdge::TablePartition => "table partition",
191 StructuralEdge::Relationship => "relationship",
192 StructuralEdge::RelationshipEndpoint => "relationship endpoint",
193 StructuralEdge::SortByColumn => "sort-by column",
194 StructuralEdge::GroupByColumn => "group-by column",
195 StructuralEdge::HierarchyLevel => "hierarchy level",
196 StructuralEdge::EngineManaged => "engine-managed column",
197 StructuralEdge::RolePermission => "role permission",
198 })
199 }
200}
201
202fn dax_site(kind: DaxExpressionKind) -> &'static str {
205 match kind {
206 DaxExpressionKind::Measure => "measure expression",
207 DaxExpressionKind::MeasureFormatString => "measure format string",
208 DaxExpressionKind::MeasureDetailRows => "measure detail rows",
209 DaxExpressionKind::KpiTarget => "KPI target",
210 DaxExpressionKind::KpiStatus => "KPI status",
211 DaxExpressionKind::KpiTrend => "KPI trend",
212 DaxExpressionKind::CalculatedColumn => "calculated column expression",
213 DaxExpressionKind::CalculatedTable => "calculated table expression",
214 DaxExpressionKind::TableDetailRows => "table detail rows",
215 DaxExpressionKind::RlsFilter => "RLS filter",
216 DaxExpressionKind::CalculationItem => "calculation item expression",
217 DaxExpressionKind::CalculationItemFormatString => "calculation item format string",
218 DaxExpressionKind::CalculationGroupNoSelection => "no-selection expression",
219 DaxExpressionKind::CalculationGroupNoSelectionFormatString => "no-selection format string",
220 DaxExpressionKind::CalculationGroupMultipleOrEmptySelection => {
221 "multiple-or-empty-selection expression"
222 }
223 DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString => {
224 "multiple-or-empty-selection format string"
225 }
226 DaxExpressionKind::Function => "function body",
227 DaxExpressionKind::ReportMeasure => "report measure expression",
228 DaxExpressionKind::ReportMeasureFormatString => "report measure format string",
229 }
230}
231
232#[cfg(test)]
233mod tests {
234 use super::*;
235
236 fn binding(kind: BindingSite) -> Provenance {
237 Provenance::Binding(Box::new(BindingEdge {
238 kind,
239 report: Some(NameKey::new("Mini")),
240 page: Some(NameKey::new("P1")),
241 visual: Some(NameKey::new("V1")),
242 bookmark: None,
243 }))
244 }
245
246 mod display {
247 use super::*;
248
249 #[test]
250 fn a_field_well_renders_its_site_chain() {
251 assert_eq!(
252 binding(BindingSite::FieldWell {
253 role: "Y".to_string(),
254 })
255 .to_string(),
256 "field well 'Y' — visual 'V1' on page 'P1' in report 'Mini'"
257 );
258 }
259
260 #[test]
261 fn a_bookmark_follows_the_visual_it_saved() {
262 let provenance = Provenance::Binding(Box::new(BindingEdge {
263 kind: BindingSite::Filter,
264 report: None,
265 page: Some(NameKey::new("P1")),
266 visual: Some(NameKey::new("V1")),
267 bookmark: Some(NameKey::new("B1")),
268 }));
269
270 assert_eq!(
271 provenance.to_string(),
272 "filter — visual 'V1' on page 'P1' in bookmark 'B1'"
273 );
274 }
275
276 #[test]
277 fn a_report_level_filter_names_no_site() {
278 let provenance = Provenance::Binding(Box::new(BindingEdge {
279 kind: BindingSite::Filter,
280 report: None,
281 page: None,
282 visual: None,
283 bookmark: None,
284 }));
285
286 assert_eq!(provenance.to_string(), "filter");
287 }
288
289 #[test]
290 fn structural_and_m_sites_render_as_phrases() {
291 assert_eq!(
292 Provenance::Structural {
293 role: StructuralEdge::RelationshipEndpoint
294 }
295 .to_string(),
296 "relationship endpoint"
297 );
298 assert_eq!(Provenance::M.to_string(), "M expression");
299 }
300
301 #[test]
302 fn dax_sites_render_as_phrases() {
303 assert_eq!(
304 Provenance::Dax {
305 kind: DaxExpressionKind::RlsFilter
306 }
307 .to_string(),
308 "RLS filter"
309 );
310 assert_eq!(
311 Provenance::Dax {
312 kind: DaxExpressionKind::Measure
313 }
314 .to_string(),
315 "measure expression"
316 );
317 }
318 }
319
320 mod classification {
321 use super::*;
322
323 #[test]
324 fn the_strong_pass_excludes_only_relationship_endpoints() {
325 let endpoint = Provenance::Structural {
326 role: StructuralEdge::RelationshipEndpoint,
327 };
328 let member = Provenance::Structural {
329 role: StructuralEdge::TableMember,
330 };
331
332 assert!(!endpoint.is_strong_pass_edge());
333 assert!(member.is_strong_pass_edge());
334 assert!(Provenance::M.is_strong_pass_edge());
335 assert!(
336 Provenance::Dax {
337 kind: DaxExpressionKind::Measure
338 }
339 .is_strong_pass_edge()
340 );
341 }
342
343 #[test]
344 fn the_weak_pass_excludes_only_containment() {
345 let endpoint = Provenance::Structural {
346 role: StructuralEdge::RelationshipEndpoint,
347 };
348 let member = Provenance::Structural {
349 role: StructuralEdge::TableMember,
350 };
351
352 assert!(!member.is_weak_pass_edge());
353 assert!(endpoint.is_weak_pass_edge());
354 assert!(Provenance::M.is_weak_pass_edge());
355 }
356 }
357}