use std::fmt;
use crate::identity::{NameKey, Quoted};
use crate::model::DaxExpressionKind;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Provenance {
Dax {
kind: DaxExpressionKind,
},
Binding(
Box<BindingEdge>,
),
M,
Structural {
role: StructuralEdge,
},
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct BindingEdge {
pub kind: BindingSite,
pub report: Option<NameKey>,
pub page: Option<NameKey>,
pub visual: Option<NameKey>,
pub bookmark: Option<NameKey>,
}
impl Provenance {
pub(super) fn is_strong_pass_edge(&self) -> bool {
!matches!(
self,
Provenance::Structural {
role: StructuralEdge::RelationshipEndpoint
} | Provenance::Structural {
role: StructuralEdge::InactiveRelationship
} | Provenance::Structural {
role: StructuralEdge::InactiveRelationshipEndpoint
}
)
}
pub(super) fn is_weak_pass_edge(&self) -> bool {
!matches!(
self,
Provenance::Structural {
role: StructuralEdge::TableMember
} | Provenance::Structural {
role: StructuralEdge::InactiveRelationship
} | Provenance::Structural {
role: StructuralEdge::InactiveRelationshipEndpoint
}
)
}
}
impl fmt::Display for Provenance {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Provenance::Dax { kind } => f.write_str(dax_site(*kind)),
Provenance::Binding(edge) => {
let BindingEdge {
kind,
report,
page,
visual,
bookmark,
} = edge.as_ref();
write_site(f, kind)?;
if let Some(visual) = visual {
write!(f, " — visual {}", Quoted(visual.as_str()))?;
}
if let Some(page) = page {
write!(f, " on page {}", Quoted(page.as_str()))?;
}
if let Some(bookmark) = bookmark {
write!(f, " in bookmark {}", Quoted(bookmark.as_str()))?;
}
if let Some(report) = report {
write!(f, " in report {}", Quoted(report.as_str()))?;
}
Ok(())
}
Provenance::M => f.write_str("Power Query expression"),
Provenance::Structural { role } => write!(f, "{role}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum BindingSite {
FieldWell {
role: String,
},
Filter,
Sort,
Drillthrough,
ConditionalFormatting,
AltText,
}
fn write_site(f: &mut fmt::Formatter<'_>, site: &BindingSite) -> fmt::Result {
match site {
BindingSite::FieldWell { role } => {
write!(f, "field well {}", Quoted(role.as_str()))
}
BindingSite::Filter => f.write_str("filter"),
BindingSite::Sort => f.write_str("sort definition"),
BindingSite::Drillthrough => f.write_str("drillthrough parameter"),
BindingSite::ConditionalFormatting => f.write_str("conditional formatting"),
BindingSite::AltText => f.write_str("alt text"),
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum StructuralEdge {
TableMember,
TablePartition,
Relationship,
InactiveRelationship,
RelationshipEndpoint,
InactiveRelationshipEndpoint,
SortByColumn,
GroupByColumn,
HierarchyLevel,
EngineManaged,
RolePermission,
}
impl fmt::Display for StructuralEdge {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(match self {
StructuralEdge::TableMember => "table member",
StructuralEdge::TablePartition => "table partition",
StructuralEdge::Relationship => "relationship",
StructuralEdge::InactiveRelationship => "inactive relationship",
StructuralEdge::RelationshipEndpoint => "relationship endpoint",
StructuralEdge::InactiveRelationshipEndpoint => "inactive relationship endpoint",
StructuralEdge::SortByColumn => "sort-by column",
StructuralEdge::GroupByColumn => "group-by column",
StructuralEdge::HierarchyLevel => "hierarchy level",
StructuralEdge::EngineManaged => "engine-managed column",
StructuralEdge::RolePermission => "role permission",
})
}
}
fn dax_site(kind: DaxExpressionKind) -> &'static str {
match kind {
DaxExpressionKind::Measure => "measure expression",
DaxExpressionKind::MeasureFormatString => "measure format string",
DaxExpressionKind::MeasureDetailRows => "measure detail rows",
DaxExpressionKind::KpiTarget => "KPI target",
DaxExpressionKind::KpiStatus => "KPI status",
DaxExpressionKind::KpiTrend => "KPI trend",
DaxExpressionKind::CalculatedColumn => "calculated column expression",
DaxExpressionKind::CalculatedTable => "calculated table expression",
DaxExpressionKind::TableDetailRows => "table detail rows",
DaxExpressionKind::RlsFilter => "RLS filter",
DaxExpressionKind::CalculationItem => "calculation item expression",
DaxExpressionKind::CalculationItemFormatString => "calculation item format string",
DaxExpressionKind::CalculationGroupNoSelection => "no-selection expression",
DaxExpressionKind::CalculationGroupNoSelectionFormatString => "no-selection format string",
DaxExpressionKind::CalculationGroupMultipleOrEmptySelection => {
"multiple-or-empty-selection expression"
}
DaxExpressionKind::CalculationGroupMultipleOrEmptySelectionFormatString => {
"multiple-or-empty-selection format string"
}
DaxExpressionKind::Function => "function body",
DaxExpressionKind::ReportMeasure => "report measure expression",
DaxExpressionKind::ReportMeasureFormatString => "report measure format string",
}
}
#[cfg(test)]
mod tests {
use super::*;
fn binding(kind: BindingSite) -> Provenance {
Provenance::Binding(Box::new(BindingEdge {
kind,
report: Some(NameKey::new("Mini")),
page: Some(NameKey::new("P1")),
visual: Some(NameKey::new("V1")),
bookmark: None,
}))
}
mod display {
use super::*;
#[test]
fn a_field_well_renders_its_site_chain() {
assert_eq!(
binding(BindingSite::FieldWell {
role: "Y".to_string(),
})
.to_string(),
"field well 'Y' — visual 'V1' on page 'P1' in report 'Mini'"
);
}
#[test]
fn a_bookmark_follows_the_visual_it_saved() {
let provenance = Provenance::Binding(Box::new(BindingEdge {
kind: BindingSite::Filter,
report: None,
page: Some(NameKey::new("P1")),
visual: Some(NameKey::new("V1")),
bookmark: Some(NameKey::new("B1")),
}));
assert_eq!(
provenance.to_string(),
"filter — visual 'V1' on page 'P1' in bookmark 'B1'"
);
}
#[test]
fn a_report_level_filter_names_no_site() {
let provenance = Provenance::Binding(Box::new(BindingEdge {
kind: BindingSite::Filter,
report: None,
page: None,
visual: None,
bookmark: None,
}));
assert_eq!(provenance.to_string(), "filter");
}
#[test]
fn structural_and_m_sites_render_as_phrases() {
assert_eq!(
Provenance::Structural {
role: StructuralEdge::RelationshipEndpoint
}
.to_string(),
"relationship endpoint"
);
assert_eq!(
Provenance::Structural {
role: StructuralEdge::InactiveRelationship
}
.to_string(),
"inactive relationship"
);
assert_eq!(Provenance::M.to_string(), "Power Query expression");
}
#[test]
fn dax_sites_render_as_phrases() {
assert_eq!(
Provenance::Dax {
kind: DaxExpressionKind::RlsFilter
}
.to_string(),
"RLS filter"
);
assert_eq!(
Provenance::Dax {
kind: DaxExpressionKind::Measure
}
.to_string(),
"measure expression"
);
}
}
mod classification {
use super::*;
#[test]
fn the_strong_pass_excludes_relationship_endpoints_and_inactive_relationships() {
let endpoint = Provenance::Structural {
role: StructuralEdge::RelationshipEndpoint,
};
let inactive = Provenance::Structural {
role: StructuralEdge::InactiveRelationship,
};
let inactive_key = Provenance::Structural {
role: StructuralEdge::InactiveRelationshipEndpoint,
};
let member = Provenance::Structural {
role: StructuralEdge::TableMember,
};
assert!(!endpoint.is_strong_pass_edge());
assert!(!inactive.is_strong_pass_edge());
assert!(!inactive_key.is_strong_pass_edge());
assert!(member.is_strong_pass_edge());
assert!(Provenance::M.is_strong_pass_edge());
assert!(
Provenance::Dax {
kind: DaxExpressionKind::Measure
}
.is_strong_pass_edge()
);
}
#[test]
fn the_weak_pass_excludes_containment_and_the_inactive_relationship_edges() {
let endpoint = Provenance::Structural {
role: StructuralEdge::RelationshipEndpoint,
};
let inactive = Provenance::Structural {
role: StructuralEdge::InactiveRelationship,
};
let inactive_key = Provenance::Structural {
role: StructuralEdge::InactiveRelationshipEndpoint,
};
let member = Provenance::Structural {
role: StructuralEdge::TableMember,
};
assert!(!member.is_weak_pass_edge());
assert!(endpoint.is_weak_pass_edge());
assert!(!inactive.is_weak_pass_edge());
assert!(!inactive_key.is_weak_pass_edge());
assert!(Provenance::M.is_weak_pass_edge());
}
}
}