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 {
68 !matches!(
69 self,
70 Provenance::Structural {
71 role: StructuralEdge::RelationshipEndpoint
72 } | Provenance::Structural {
73 role: StructuralEdge::InactiveRelationship
74 } | Provenance::Structural {
75 role: StructuralEdge::InactiveRelationshipEndpoint
76 }
77 )
78 }
79
80 pub(super) fn is_weak_pass_edge(&self) -> bool {
86 !matches!(
87 self,
88 Provenance::Structural {
89 role: StructuralEdge::TableMember
90 } | Provenance::Structural {
91 role: StructuralEdge::InactiveRelationship
92 } | Provenance::Structural {
93 role: StructuralEdge::InactiveRelationshipEndpoint
94 }
95 )
96 }
97}
98
99impl fmt::Display for Provenance {
100 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
105 match self {
106 Provenance::Dax { kind } => f.write_str(dax_site(*kind)),
107 Provenance::Binding(edge) => {
108 let BindingEdge {
109 kind,
110 report,
111 page,
112 visual,
113 bookmark,
114 } = edge.as_ref();
115 write_site(f, kind)?;
116 if let Some(visual) = visual {
117 write!(f, " — visual {}", Quoted(visual.as_str()))?;
118 }
119 if let Some(page) = page {
120 write!(f, " on page {}", Quoted(page.as_str()))?;
121 }
122 if let Some(bookmark) = bookmark {
123 write!(f, " in bookmark {}", Quoted(bookmark.as_str()))?;
124 }
125 if let Some(report) = report {
126 write!(f, " in report {}", Quoted(report.as_str()))?;
127 }
128 Ok(())
129 }
130 Provenance::M => f.write_str("Power Query expression"),
131 Provenance::Structural { role } => write!(f, "{role}"),
132 }
133 }
134}
135
136#[derive(Debug, Clone, PartialEq, Eq, Hash)]
140pub enum BindingSite {
141 FieldWell {
143 role: String,
145 },
146 Filter,
148 Sort,
150 Drillthrough,
152 ConditionalFormatting,
154 AltText,
156}
157
158fn write_site(f: &mut fmt::Formatter<'_>, site: &BindingSite) -> fmt::Result {
159 match site {
160 BindingSite::FieldWell { role } => {
161 write!(f, "field well {}", Quoted(role.as_str()))
162 }
163 BindingSite::Filter => f.write_str("filter"),
164 BindingSite::Sort => f.write_str("sort definition"),
165 BindingSite::Drillthrough => f.write_str("drillthrough parameter"),
166 BindingSite::ConditionalFormatting => f.write_str("conditional formatting"),
167 BindingSite::AltText => f.write_str("alt text"),
168 }
169}
170
171#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
173pub enum StructuralEdge {
174 TableMember,
177 TablePartition,
179 Relationship,
181 InactiveRelationship,
186 RelationshipEndpoint,
188 InactiveRelationshipEndpoint,
194 SortByColumn,
196 GroupByColumn,
198 HierarchyLevel,
200 EngineManaged,
204 RolePermission,
206}
207
208impl fmt::Display for StructuralEdge {
209 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
210 f.write_str(match self {
211 StructuralEdge::TableMember => "table member",
212 StructuralEdge::TablePartition => "table partition",
213 StructuralEdge::Relationship => "relationship",
214 StructuralEdge::InactiveRelationship => "inactive relationship",
215 StructuralEdge::RelationshipEndpoint => "relationship endpoint",
216 StructuralEdge::InactiveRelationshipEndpoint => "inactive relationship endpoint",
217 StructuralEdge::SortByColumn => "sort-by column",
218 StructuralEdge::GroupByColumn => "group-by column",
219 StructuralEdge::HierarchyLevel => "hierarchy level",
220 StructuralEdge::EngineManaged => "engine-managed column",
221 StructuralEdge::RolePermission => "role permission",
222 })
223 }
224}
225
226fn dax_site(kind: DaxExpressionKind) -> &'static str {
229 match kind {
230 DaxExpressionKind::Measure => "measure expression",
231 DaxExpressionKind::MeasureFormatString => "measure format string",
232 DaxExpressionKind::MeasureDetailRows => "measure detail rows",
233 DaxExpressionKind::KpiTarget => "KPI target",
234 DaxExpressionKind::KpiStatus => "KPI status",
235 DaxExpressionKind::KpiTrend => "KPI trend",
236 DaxExpressionKind::CalculatedColumn => "calculated column expression",
237 DaxExpressionKind::CalculatedTable => "calculated table expression",
238 DaxExpressionKind::TableDetailRows => "table detail rows",
239 DaxExpressionKind::RlsFilter => "RLS filter",
240 DaxExpressionKind::CalculationItem => "calculation item expression",
241 DaxExpressionKind::CalculationItemFormatString => "calculation item format string",
242 DaxExpressionKind::CalculationGroupNoSelection => "no-selection expression",
243 DaxExpressionKind::CalculationGroupNoSelectionFormatString => "no-selection format string",
244 DaxExpressionKind::CalculationGroupMultipleOrEmptySelection => {
245 "multiple-or-empty-selection expression"
246 }
247 DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString => {
248 "multiple-or-empty-selection format string"
249 }
250 DaxExpressionKind::Function => "function body",
251 DaxExpressionKind::ReportMeasure => "report measure expression",
252 DaxExpressionKind::ReportMeasureFormatString => "report measure format string",
253 }
254}
255
256#[cfg(test)]
257mod tests {
258 use super::*;
259
260 fn binding(kind: BindingSite) -> Provenance {
261 Provenance::Binding(Box::new(BindingEdge {
262 kind,
263 report: Some(NameKey::new("Mini")),
264 page: Some(NameKey::new("P1")),
265 visual: Some(NameKey::new("V1")),
266 bookmark: None,
267 }))
268 }
269
270 mod display {
271 use super::*;
272
273 #[test]
274 fn a_field_well_renders_its_site_chain() {
275 assert_eq!(
276 binding(BindingSite::FieldWell {
277 role: "Y".to_string(),
278 })
279 .to_string(),
280 "field well 'Y' — visual 'V1' on page 'P1' in report 'Mini'"
281 );
282 }
283
284 #[test]
285 fn a_bookmark_follows_the_visual_it_saved() {
286 let provenance = Provenance::Binding(Box::new(BindingEdge {
287 kind: BindingSite::Filter,
288 report: None,
289 page: Some(NameKey::new("P1")),
290 visual: Some(NameKey::new("V1")),
291 bookmark: Some(NameKey::new("B1")),
292 }));
293
294 assert_eq!(
295 provenance.to_string(),
296 "filter — visual 'V1' on page 'P1' in bookmark 'B1'"
297 );
298 }
299
300 #[test]
301 fn a_report_level_filter_names_no_site() {
302 let provenance = Provenance::Binding(Box::new(BindingEdge {
303 kind: BindingSite::Filter,
304 report: None,
305 page: None,
306 visual: None,
307 bookmark: None,
308 }));
309
310 assert_eq!(provenance.to_string(), "filter");
311 }
312
313 #[test]
314 fn structural_and_m_sites_render_as_phrases() {
315 assert_eq!(
316 Provenance::Structural {
317 role: StructuralEdge::RelationshipEndpoint
318 }
319 .to_string(),
320 "relationship endpoint"
321 );
322 assert_eq!(
323 Provenance::Structural {
324 role: StructuralEdge::InactiveRelationship
325 }
326 .to_string(),
327 "inactive relationship"
328 );
329 assert_eq!(Provenance::M.to_string(), "Power Query expression");
330 }
331
332 #[test]
333 fn dax_sites_render_as_phrases() {
334 assert_eq!(
335 Provenance::Dax {
336 kind: DaxExpressionKind::RlsFilter
337 }
338 .to_string(),
339 "RLS filter"
340 );
341 assert_eq!(
342 Provenance::Dax {
343 kind: DaxExpressionKind::Measure
344 }
345 .to_string(),
346 "measure expression"
347 );
348 }
349 }
350
351 mod classification {
352 use super::*;
353
354 #[test]
355 fn the_strong_pass_excludes_relationship_endpoints_and_inactive_relationships() {
356 let endpoint = Provenance::Structural {
357 role: StructuralEdge::RelationshipEndpoint,
358 };
359 let inactive = Provenance::Structural {
360 role: StructuralEdge::InactiveRelationship,
361 };
362 let inactive_key = Provenance::Structural {
363 role: StructuralEdge::InactiveRelationshipEndpoint,
364 };
365 let member = Provenance::Structural {
366 role: StructuralEdge::TableMember,
367 };
368
369 assert!(!endpoint.is_strong_pass_edge());
370 assert!(!inactive.is_strong_pass_edge());
371 assert!(!inactive_key.is_strong_pass_edge());
372 assert!(member.is_strong_pass_edge());
373 assert!(Provenance::M.is_strong_pass_edge());
374 assert!(
375 Provenance::Dax {
376 kind: DaxExpressionKind::Measure
377 }
378 .is_strong_pass_edge()
379 );
380 }
381
382 #[test]
383 fn the_weak_pass_excludes_containment_and_the_inactive_relationship_edges() {
384 let endpoint = Provenance::Structural {
385 role: StructuralEdge::RelationshipEndpoint,
386 };
387 let inactive = Provenance::Structural {
388 role: StructuralEdge::InactiveRelationship,
389 };
390 let inactive_key = Provenance::Structural {
391 role: StructuralEdge::InactiveRelationshipEndpoint,
392 };
393 let member = Provenance::Structural {
394 role: StructuralEdge::TableMember,
395 };
396
397 assert!(!member.is_weak_pass_edge());
398 assert!(endpoint.is_weak_pass_edge());
399 assert!(!inactive.is_weak_pass_edge());
404 assert!(!inactive_key.is_weak_pass_edge());
405 assert!(Provenance::M.is_weak_pass_edge());
406 }
407 }
408}