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 pub mobile: bool,
65}
66
67impl fmt::Display for BindingEdge {
68 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
74 let BindingEdge {
75 kind,
76 report,
77 page,
78 visual,
79 bookmark,
80 mobile,
81 } = self;
82 if *mobile {
83 f.write_str("mobile layout ")?;
84 }
85 write_site(f, kind)?;
86 if let Some(visual) = visual {
87 write!(f, " — visual {}", Quoted(visual.as_str()))?;
88 }
89 if let Some(page) = page {
90 write!(f, " on page {}", Quoted(page.as_str()))?;
91 }
92 if let Some(bookmark) = bookmark {
93 write!(f, " in bookmark {}", Quoted(bookmark.as_str()))?;
94 }
95 if let Some(report) = report {
96 write!(f, " in report {}", Quoted(report.as_str()))?;
97 }
98 Ok(())
99 }
100}
101
102impl Provenance {
103 pub(super) fn is_strong_pass_edge(&self) -> bool {
108 !matches!(
109 self,
110 Provenance::Structural {
111 role: StructuralEdge::RelationshipEndpoint
112 } | Provenance::Structural {
113 role: StructuralEdge::InactiveRelationship
114 } | Provenance::Structural {
115 role: StructuralEdge::InactiveRelationshipEndpoint
116 }
117 )
118 }
119
120 pub(super) fn is_weak_pass_edge(&self) -> bool {
126 !matches!(
127 self,
128 Provenance::Structural {
129 role: StructuralEdge::TableMember
130 } | Provenance::Structural {
131 role: StructuralEdge::InactiveRelationship
132 } | Provenance::Structural {
133 role: StructuralEdge::InactiveRelationshipEndpoint
134 }
135 )
136 }
137}
138
139impl fmt::Display for Provenance {
140 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
145 match self {
146 Provenance::Dax { kind } => f.write_str(dax_site(*kind)),
147 Provenance::Binding(edge) => edge.fmt(f),
148 Provenance::M => f.write_str("Power Query expression"),
149 Provenance::Structural { role } => write!(f, "{role}"),
150 }
151 }
152}
153
154#[derive(Debug, Clone, PartialEq, Eq, Hash)]
158pub enum BindingSite {
159 FieldWell {
161 role: String,
163 },
164 Filter,
166 Sort,
168 Drillthrough,
170 ConditionalFormatting,
172 AltText,
174}
175
176fn write_site(f: &mut fmt::Formatter<'_>, site: &BindingSite) -> fmt::Result {
177 match site {
178 BindingSite::FieldWell { role } => {
179 write!(f, "field well {}", Quoted(role.as_str()))
180 }
181 BindingSite::Filter => f.write_str("filter"),
182 BindingSite::Sort => f.write_str("sort definition"),
183 BindingSite::Drillthrough => f.write_str("drillthrough parameter"),
184 BindingSite::ConditionalFormatting => f.write_str("conditional formatting"),
185 BindingSite::AltText => f.write_str("alt text"),
186 }
187}
188
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
191pub enum StructuralEdge {
192 TableMember,
195 TablePartition,
197 Relationship,
199 InactiveRelationship,
204 RelationshipEndpoint,
206 InactiveRelationshipEndpoint,
212 SortByColumn,
214 GroupByColumn,
216 HierarchyLevel,
218 EngineManaged,
222 MParameterBinding,
227 RolePermission,
229}
230
231impl fmt::Display for StructuralEdge {
232 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
233 f.write_str(match self {
234 StructuralEdge::TableMember => "table member",
235 StructuralEdge::TablePartition => "table partition",
236 StructuralEdge::Relationship => "relationship",
237 StructuralEdge::InactiveRelationship => "inactive relationship",
238 StructuralEdge::RelationshipEndpoint => "relationship endpoint",
239 StructuralEdge::InactiveRelationshipEndpoint => "inactive relationship endpoint",
240 StructuralEdge::SortByColumn => "sort-by column",
241 StructuralEdge::GroupByColumn => "group-by column",
242 StructuralEdge::HierarchyLevel => "hierarchy level",
243 StructuralEdge::EngineManaged => "engine-managed column",
244 StructuralEdge::MParameterBinding => "dynamic M parameter binding",
245 StructuralEdge::RolePermission => "role permission",
246 })
247 }
248}
249
250fn dax_site(kind: DaxExpressionKind) -> &'static str {
253 match kind {
254 DaxExpressionKind::Measure => "measure expression",
255 DaxExpressionKind::MeasureFormatString => "measure format string",
256 DaxExpressionKind::MeasureDetailRows => "measure detail rows",
257 DaxExpressionKind::KpiTarget => "KPI target",
258 DaxExpressionKind::KpiStatus => "KPI status",
259 DaxExpressionKind::KpiTrend => "KPI trend",
260 DaxExpressionKind::CalculatedColumn => "calculated column expression",
261 DaxExpressionKind::CalculatedTable => "calculated table expression",
262 DaxExpressionKind::ChangeDetection => "change detection expression",
263 DaxExpressionKind::TableDetailRows => "table detail rows",
264 DaxExpressionKind::RlsFilter => "RLS filter",
265 DaxExpressionKind::CalculationItem => "calculation item expression",
266 DaxExpressionKind::CalculationItemFormatString => "calculation item format string",
267 DaxExpressionKind::CalculationGroupNoSelection => "no-selection expression",
268 DaxExpressionKind::CalculationGroupNoSelectionFormatString => "no-selection format string",
269 DaxExpressionKind::CalculationGroupMultipleOrEmptySelection => {
270 "multiple-or-empty-selection expression"
271 }
272 DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString => {
273 "multiple-or-empty-selection format string"
274 }
275 DaxExpressionKind::Function => "function body",
276 DaxExpressionKind::ReportMeasure => "report measure expression",
277 DaxExpressionKind::ReportMeasureFormatString => "report measure format string",
278 }
279}
280
281#[cfg(test)]
282mod tests {
283 use super::*;
284
285 fn binding(kind: BindingSite) -> Provenance {
286 Provenance::Binding(Box::new(BindingEdge {
287 kind,
288 report: Some(NameKey::new("Mini")),
289 page: Some(NameKey::new("P1")),
290 visual: Some(NameKey::new("V1")),
291 bookmark: None,
292 mobile: false,
293 }))
294 }
295
296 mod display {
297 use super::*;
298
299 #[test]
300 fn a_field_well_renders_its_site_chain() {
301 assert_eq!(
302 binding(BindingSite::FieldWell {
303 role: "Y".to_string(),
304 })
305 .to_string(),
306 "field well 'Y' — visual 'V1' on page 'P1' in report 'Mini'"
307 );
308 }
309
310 #[test]
311 fn a_bookmark_follows_the_visual_it_saved() {
312 let provenance = Provenance::Binding(Box::new(BindingEdge {
313 kind: BindingSite::Filter,
314 report: None,
315 page: Some(NameKey::new("P1")),
316 visual: Some(NameKey::new("V1")),
317 bookmark: Some(NameKey::new("B1")),
318 mobile: false,
319 }));
320
321 assert_eq!(
322 provenance.to_string(),
323 "filter — visual 'V1' on page 'P1' in bookmark 'B1'"
324 );
325 }
326
327 #[test]
330 fn a_mobile_layout_binding_says_so() {
331 let provenance = Provenance::Binding(Box::new(BindingEdge {
332 kind: BindingSite::FieldWell {
333 role: "Values".to_string(),
334 },
335 report: Some(NameKey::new("Mini")),
336 page: Some(NameKey::new("P1")),
337 visual: Some(NameKey::new("V1")),
338 bookmark: None,
339 mobile: true,
340 }));
341
342 assert_eq!(
343 provenance.to_string(),
344 "mobile layout field well 'Values' — visual 'V1' on page 'P1' in report 'Mini'"
345 );
346 }
347
348 #[test]
349 fn a_report_level_filter_names_no_site() {
350 let provenance = Provenance::Binding(Box::new(BindingEdge {
351 kind: BindingSite::Filter,
352 report: None,
353 page: None,
354 visual: None,
355 bookmark: None,
356 mobile: false,
357 }));
358
359 assert_eq!(provenance.to_string(), "filter");
360 }
361
362 #[test]
363 fn structural_and_m_sites_render_as_phrases() {
364 assert_eq!(
365 Provenance::Structural {
366 role: StructuralEdge::RelationshipEndpoint
367 }
368 .to_string(),
369 "relationship endpoint"
370 );
371 assert_eq!(
372 Provenance::Structural {
373 role: StructuralEdge::InactiveRelationship
374 }
375 .to_string(),
376 "inactive relationship"
377 );
378 assert_eq!(Provenance::M.to_string(), "Power Query expression");
379 }
380
381 #[test]
382 fn dax_sites_render_as_phrases() {
383 assert_eq!(
384 Provenance::Dax {
385 kind: DaxExpressionKind::RlsFilter
386 }
387 .to_string(),
388 "RLS filter"
389 );
390 assert_eq!(
391 Provenance::Dax {
392 kind: DaxExpressionKind::Measure
393 }
394 .to_string(),
395 "measure expression"
396 );
397 }
398 }
399
400 mod classification {
401 use super::*;
402
403 #[test]
404 fn the_strong_pass_excludes_relationship_endpoints_and_inactive_relationships() {
405 let endpoint = Provenance::Structural {
406 role: StructuralEdge::RelationshipEndpoint,
407 };
408 let inactive = Provenance::Structural {
409 role: StructuralEdge::InactiveRelationship,
410 };
411 let inactive_key = Provenance::Structural {
412 role: StructuralEdge::InactiveRelationshipEndpoint,
413 };
414 let member = Provenance::Structural {
415 role: StructuralEdge::TableMember,
416 };
417
418 assert!(!endpoint.is_strong_pass_edge());
419 assert!(!inactive.is_strong_pass_edge());
420 assert!(!inactive_key.is_strong_pass_edge());
421 assert!(member.is_strong_pass_edge());
422 assert!(Provenance::M.is_strong_pass_edge());
423 assert!(
424 Provenance::Dax {
425 kind: DaxExpressionKind::Measure
426 }
427 .is_strong_pass_edge()
428 );
429 }
430
431 #[test]
432 fn the_weak_pass_excludes_containment_and_the_inactive_relationship_edges() {
433 let endpoint = Provenance::Structural {
434 role: StructuralEdge::RelationshipEndpoint,
435 };
436 let inactive = Provenance::Structural {
437 role: StructuralEdge::InactiveRelationship,
438 };
439 let inactive_key = Provenance::Structural {
440 role: StructuralEdge::InactiveRelationshipEndpoint,
441 };
442 let member = Provenance::Structural {
443 role: StructuralEdge::TableMember,
444 };
445
446 assert!(!member.is_weak_pass_edge());
447 assert!(endpoint.is_weak_pass_edge());
448 assert!(!inactive.is_weak_pass_edge());
453 assert!(!inactive_key.is_weak_pass_edge());
454 assert!(Provenance::M.is_weak_pass_edge());
455 }
456 }
457}