use std::fmt;
use crate::identity::{FieldRef, NameKey, Quoted};
use crate::model::{DaxExpressionKind, DaxExpressionRef, ExpressionOwner};
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct ReportModel {
pub name: Option<String>,
pub dataset: DatasetReference,
pub filters: Vec<Filter>,
pub pages: Vec<Page>,
pub mobile_pages: Vec<Page>,
pub bookmarks: Vec<Bookmark>,
pub measures: Vec<ReportMeasure>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DatasetReference {
ByPath {
path: String,
},
ByConnection {
connection_string: String,
},
Unresolved,
}
impl Default for DatasetReference {
fn default() -> Self {
Self::Unresolved
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Page {
pub name: NameKey,
pub display_name: Option<String>,
pub is_hidden: bool,
pub filters: Vec<Filter>,
pub binding: Option<PageBinding>,
pub visuals: Vec<Visual>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum PageBindingKind {
#[default]
Default,
Drillthrough,
Tooltip,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PageBinding {
pub kind: PageBindingKind,
pub parameters: Vec<DrillthroughParameter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DrillthroughParameter {
pub name: Option<NameKey>,
pub target: FieldTarget,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Visual {
pub name: NameKey,
pub visual_type: String,
pub wells: Vec<FieldWell>,
pub filters: Vec<Filter>,
pub sorts: Vec<FieldTarget>,
pub conditional_formatting: Vec<FieldTarget>,
pub alt_text: Vec<FieldTarget>,
pub tooltip_page: Option<NameKey>,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct FieldWell {
pub role: String,
pub projections: Vec<Projection>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Projection {
pub target: FieldTarget,
pub query_ref: Option<String>,
pub active: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct Filter {
pub name: Option<NameKey>,
pub target: Option<FieldTarget>,
pub references: Vec<FieldTarget>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Bookmark {
pub name: NameKey,
pub display_name: Option<String>,
pub filters: Vec<Filter>,
pub sections: Vec<BookmarkSection>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BookmarkSection {
pub page: NameKey,
pub filters: Vec<Filter>,
pub visuals: Vec<BookmarkVisual>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct BookmarkVisual {
pub visual: NameKey,
pub wells: Vec<FieldWell>,
pub filters: Vec<Filter>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ReportMeasure {
pub name: NameKey,
pub expression: String,
pub format_string: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum FieldTarget {
Column {
table: NameKey,
column: NameKey,
},
Measure {
home_table: Option<NameKey>,
measure: NameKey,
},
HierarchyLevel {
table: NameKey,
hierarchy: NameKey,
level: NameKey,
via_column: Option<NameKey>,
via_variation: Option<NameKey>,
},
Aggregation {
function: Option<String>,
inner: Box<FieldTarget>,
},
Written(FieldRef),
}
impl fmt::Display for FieldTarget {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
FieldTarget::Column { table, column } => {
write!(f, "{}[{}]", Quoted(table.as_str()), column.as_str())
}
FieldTarget::Measure {
home_table,
measure,
} => match home_table {
Some(table) => write!(f, "{}[{}]", Quoted(table.as_str()), measure.as_str()),
None => write!(f, "[{}]", measure.as_str()),
},
FieldTarget::HierarchyLevel {
table,
hierarchy,
level,
..
} => write!(
f,
"hierarchy {}[{}] level {}",
Quoted(table.as_str()),
hierarchy.as_str(),
Quoted(level.as_str())
),
FieldTarget::Aggregation { function, inner } => match function {
Some(function) => write!(f, "{function}({inner})"),
None => write!(f, "Aggregation({inner})"),
},
FieldTarget::Written(reference) => write!(f, "{reference}"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum BindingKind<'a> {
FieldWell {
role: &'a str,
},
Filter,
Sort,
Drillthrough,
ConditionalFormatting,
AltText,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct BindingRef<'a> {
pub page: Option<&'a NameKey>,
pub visual: Option<&'a NameKey>,
pub bookmark: Option<&'a NameKey>,
pub mobile: bool,
pub kind: BindingKind<'a>,
pub target: &'a FieldTarget,
}
impl ReportModel {
#[must_use]
pub fn bindings(&self) -> Vec<BindingRef<'_>> {
let mut out = Vec::new();
for filter in &self.filters {
extend_with_filter(&mut out, None, None, None, false, filter);
}
for page in &self.pages {
extend_with_page(&mut out, page, false);
}
for page in &self.mobile_pages {
extend_with_page(&mut out, page, true);
}
for bookmark in &self.bookmarks {
let bookmark_id = Some(&bookmark.name);
for filter in &bookmark.filters {
extend_with_filter(&mut out, None, None, bookmark_id, false, filter);
}
for section in &bookmark.sections {
let page_id = Some(§ion.page);
for filter in §ion.filters {
extend_with_filter(&mut out, page_id, None, bookmark_id, false, filter);
}
for visual in §ion.visuals {
let visual_id = Some(&visual.visual);
extend_with_wells(
&mut out,
page_id,
visual_id,
bookmark_id,
false,
&visual.wells,
);
for filter in &visual.filters {
extend_with_filter(
&mut out,
page_id,
visual_id,
bookmark_id,
false,
filter,
);
}
}
}
}
out
}
#[must_use]
pub fn dax_expressions(&self) -> Vec<DaxExpressionRef<'_>> {
let mut out = Vec::new();
for measure in &self.measures {
let owner = ExpressionOwner::ReportMeasure {
measure: measure.name.as_str(),
};
out.push(DaxExpressionRef {
owner,
kind: DaxExpressionKind::ReportMeasure,
home_table: None,
text: &measure.expression,
});
if let Some(text) = &measure.format_string {
out.push(DaxExpressionRef {
owner,
kind: DaxExpressionKind::ReportMeasureFormatString,
home_table: None,
text,
});
}
}
out
}
}
fn extend_with_page<'a>(out: &mut Vec<BindingRef<'a>>, page: &'a Page, mobile: bool) {
let page_id = Some(&page.name);
if let Some(binding) = &page.binding {
for parameter in &binding.parameters {
out.push(BindingRef {
page: page_id,
visual: None,
bookmark: None,
mobile,
kind: BindingKind::Drillthrough,
target: ¶meter.target,
});
}
}
for filter in &page.filters {
extend_with_filter(out, page_id, None, None, mobile, filter);
}
for visual in &page.visuals {
let visual_id = Some(&visual.name);
extend_with_wells(out, page_id, visual_id, None, mobile, &visual.wells);
for filter in &visual.filters {
extend_with_filter(out, page_id, visual_id, None, mobile, filter);
}
for target in &visual.sorts {
out.push(BindingRef {
page: page_id,
visual: visual_id,
bookmark: None,
mobile,
kind: BindingKind::Sort,
target,
});
}
for target in &visual.conditional_formatting {
out.push(BindingRef {
page: page_id,
visual: visual_id,
bookmark: None,
mobile,
kind: BindingKind::ConditionalFormatting,
target,
});
}
for target in &visual.alt_text {
out.push(BindingRef {
page: page_id,
visual: visual_id,
bookmark: None,
mobile,
kind: BindingKind::AltText,
target,
});
}
}
}
fn extend_with_filter<'a>(
out: &mut Vec<BindingRef<'a>>,
page: Option<&'a NameKey>,
visual: Option<&'a NameKey>,
bookmark: Option<&'a NameKey>,
mobile: bool,
filter: &'a Filter,
) {
for target in filter.target.iter().chain(&filter.references) {
out.push(BindingRef {
page,
visual,
bookmark,
mobile,
kind: BindingKind::Filter,
target,
});
}
}
fn extend_with_wells<'a>(
out: &mut Vec<BindingRef<'a>>,
page: Option<&'a NameKey>,
visual: Option<&'a NameKey>,
bookmark: Option<&'a NameKey>,
mobile: bool,
wells: &'a [FieldWell],
) {
for well in wells {
for projection in &well.projections {
out.push(BindingRef {
page,
visual,
bookmark,
mobile,
kind: BindingKind::FieldWell {
role: well.role.as_str(),
},
target: &projection.target,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use rstest::rstest;
fn column_target(table: &str, column: &str) -> FieldTarget {
FieldTarget::Column {
table: NameKey::new(table),
column: NameKey::new(column),
}
}
fn measure_target(home_table: Option<&str>, measure: &str) -> FieldTarget {
FieldTarget::Measure {
home_table: home_table.map(NameKey::new),
measure: NameKey::new(measure),
}
}
fn filter_on(target: FieldTarget) -> Filter {
Filter {
target: Some(target),
..Default::default()
}
}
fn page(name: &str) -> Page {
Page {
name: NameKey::new(name),
display_name: None,
is_hidden: false,
filters: Vec::new(),
binding: None,
visuals: Vec::new(),
}
}
fn visual(name: &str, visual_type: &str) -> Visual {
Visual {
name: NameKey::new(name),
visual_type: visual_type.to_string(),
wells: Vec::new(),
filters: Vec::new(),
sorts: Vec::new(),
conditional_formatting: Vec::new(),
alt_text: Vec::new(),
tooltip_page: None,
}
}
fn well(role: &str, targets: &[FieldTarget]) -> FieldWell {
FieldWell {
role: role.to_string(),
projections: targets
.iter()
.cloned()
.map(|target| Projection {
target,
query_ref: None,
active: true,
})
.collect(),
}
}
mod field_target {
use super::*;
#[rstest]
#[case::column(column_target("Product", "Category"), "'Product'[Category]")]
#[case::measure_with_home_table(measure_target(Some("Sales"), "Cost"), "'Sales'[Cost]")]
#[case::measure_without_home_table(measure_target(None, "Cost"), "[Cost]")]
#[case::hierarchy_level(
FieldTarget::HierarchyLevel {
table: NameKey::new("Accounts"),
hierarchy: NameKey::new("Street Hierarchy"),
level: NameKey::new("State or Province"),
via_column: None,
via_variation: None,
},
"hierarchy 'Accounts'[Street Hierarchy] level 'State or Province'"
)]
#[case::aggregation(
FieldTarget::Aggregation {
function: Some("Sum".to_string()),
inner: Box::new(column_target("Sales", "Units")),
},
"Sum('Sales'[Units])"
)]
#[case::aggregation_without_function(
FieldTarget::Aggregation {
function: None,
inner: Box::new(column_target("Sales", "Units")),
},
"Aggregation('Sales'[Units])"
)]
#[case::written(
FieldTarget::Written(FieldRef {
table: Some(NameKey::new("Sales")),
name: NameKey::new("Amount"),
}),
"'Sales'[Amount]"
)]
fn displays_for_diagnostics(#[case] target: FieldTarget, #[case] expected: &str) {
assert_eq!(target.to_string(), expected);
}
#[test]
fn compares_equal_ignoring_case() {
assert_eq!(
column_target("Product", "Category"),
column_target("PRODUCT", "CATEGORY")
);
assert_eq!(
measure_target(Some("Sales"), "Cost"),
measure_target(Some("sales"), "COST")
);
}
#[test]
fn distinguishes_a_column_from_a_measure_with_the_same_names() {
assert_ne!(
column_target("Sales", "Cost"),
measure_target(Some("Sales"), "Cost")
);
}
}
mod bindings {
use super::*;
fn sample() -> ReportModel {
ReportModel {
name: Some("Sales overview".to_string()),
dataset: DatasetReference::ByPath {
path: "../Sales.SemanticModel".to_string(),
},
..Default::default()
}
}
fn sample_with_page(page: Page) -> ReportModel {
ReportModel {
pages: vec![page],
..sample()
}
}
fn sample_with_mobile_page(page: Page) -> ReportModel {
ReportModel {
mobile_pages: vec![page],
..sample()
}
}
type Provenance<'a> = (
Option<&'a str>,
Option<&'a str>,
Option<&'a str>,
bool,
BindingKind<'a>,
String,
);
fn provenance(report: &ReportModel) -> Vec<Provenance<'_>> {
report
.bindings()
.into_iter()
.map(|binding| {
(
binding.page.map(NameKey::as_str),
binding.visual.map(NameKey::as_str),
binding.bookmark.map(NameKey::as_str),
binding.mobile,
binding.kind,
binding.target.to_string(),
)
})
.collect()
}
#[test]
fn a_report_filter_has_no_page_visual_or_bookmark() {
let report = ReportModel {
filters: vec![filter_on(column_target("Product", "Category"))],
..sample()
};
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page, None);
assert_eq!(bindings[0].visual, None);
assert_eq!(bindings[0].bookmark, None);
assert_eq!(bindings[0].kind, BindingKind::Filter);
}
#[test]
fn a_drillthrough_parameter_is_tagged_on_its_page() {
let report = sample_with_page(Page {
binding: Some(PageBinding {
kind: PageBindingKind::Drillthrough,
parameters: vec![DrillthroughParameter {
name: Some(NameKey::new("Param_Filter5")),
target: column_target("Industries", "Industry"),
}],
}),
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual, None);
assert_eq!(bindings[0].kind, BindingKind::Drillthrough);
}
#[test]
fn a_page_filter_carries_its_page_but_no_visual() {
let report = sample_with_page(Page {
filters: vec![filter_on(column_target("Owners", "Sales owner"))],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual, None);
assert_eq!(bindings[0].kind, BindingKind::Filter);
}
#[test]
fn a_visual_well_carries_role_page_and_visual() {
let report = sample_with_page(Page {
visuals: vec![Visual {
wells: vec![well("Category", &[column_target("Product", "Category")])],
..visual("visual1", "donutChart")
}],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual.unwrap().as_str(), "visual1");
assert_eq!(bindings[0].bookmark, None);
assert_eq!(
bindings[0].kind,
BindingKind::FieldWell { role: "Category" }
);
}
#[test]
fn sorts_and_conditional_formatting_are_tagged_as_such() {
let report = sample_with_page(Page {
visuals: vec![Visual {
sorts: vec![column_target("Product", "Category")],
conditional_formatting: vec![measure_target(Some("Sales"), "Margin")],
..visual("visual1", "tableEx")
}],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(
bindings.iter().map(|b| b.kind).collect::<Vec<_>>(),
vec![BindingKind::Sort, BindingKind::ConditionalFormatting]
);
for binding in &bindings {
assert_eq!(binding.page.unwrap().as_str(), "ReportSection1");
assert_eq!(binding.visual.unwrap().as_str(), "visual1");
}
}
#[test]
fn a_bookmark_filter_carries_bookmark_and_page() {
let report = ReportModel {
bookmarks: vec![Bookmark {
name: NameKey::new("Bookmark1"),
display_name: Some("FY24".to_string()),
filters: Vec::new(),
sections: vec![BookmarkSection {
page: NameKey::new("ReportSection1"),
filters: vec![filter_on(column_target("Products", "Product category"))],
visuals: Vec::new(),
}],
}],
..sample()
};
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual, None);
assert_eq!(bindings[0].bookmark.unwrap().as_str(), "Bookmark1");
assert_eq!(bindings[0].kind, BindingKind::Filter);
}
#[test]
fn bookmark_wells_carry_bookmark_page_and_visual() {
let report = ReportModel {
bookmarks: vec![Bookmark {
name: NameKey::new("Bookmark1"),
display_name: None,
filters: Vec::new(),
sections: vec![BookmarkSection {
page: NameKey::new("ReportSection1"),
filters: Vec::new(),
visuals: vec![BookmarkVisual {
visual: NameKey::new("visual1"),
wells: vec![well("Rows", &[column_target("Product", "Subcategory")])],
filters: Vec::new(),
}],
}],
}],
..sample()
};
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual.unwrap().as_str(), "visual1");
assert_eq!(bindings[0].bookmark.unwrap().as_str(), "Bookmark1");
assert_eq!(bindings[0].kind, BindingKind::FieldWell { role: "Rows" });
}
#[test]
fn order_follows_report_structure() {
let report = ReportModel {
filters: vec![filter_on(column_target("Product", "Category"))],
pages: vec![
Page {
binding: Some(PageBinding {
kind: PageBindingKind::Drillthrough,
parameters: vec![DrillthroughParameter {
name: None,
target: column_target("Industries", "Industry"),
}],
}),
filters: vec![filter_on(column_target("Owners", "Sales owner"))],
visuals: vec![Visual {
wells: vec![well("Category", &[column_target("Product", "Category")])],
filters: vec![filter_on(column_target("Region", "Country"))],
sorts: vec![measure_target(Some("Sales"), "Sales")],
conditional_formatting: vec![measure_target(Some("Sales"), "Margin")],
..visual("visual1", "donutChart")
}],
..page("ReportSection1")
},
Page {
visuals: vec![Visual {
wells: vec![well(
"Tooltips",
&[measure_target(Some("Sales"), "Customers %")],
)],
..visual("visual2", "slicer")
}],
..page("ReportSection2")
},
],
mobile_pages: vec![Page {
visuals: vec![Visual {
wells: vec![well("Values", &[measure_target(Some("Sales"), "Total")])],
..visual("visual1", "card")
}],
..page("ReportSection1")
}],
bookmarks: vec![Bookmark {
name: NameKey::new("Bookmark1"),
display_name: None,
filters: vec![filter_on(measure_target(None, "Total Units"))],
sections: vec![BookmarkSection {
page: NameKey::new("ReportSection1"),
filters: vec![filter_on(column_target("Products", "Product category"))],
visuals: vec![BookmarkVisual {
visual: NameKey::new("visual1"),
wells: vec![well("Rows", &[column_target("Product", "Subcategory")])],
filters: vec![filter_on(column_target("Product", "Color"))],
}],
}],
}],
measures: Vec::new(),
..sample()
};
assert_eq!(
provenance(&report),
vec![
(
None,
None,
None,
false,
BindingKind::Filter,
"'Product'[Category]".to_string(),
),
(
Some("ReportSection1"),
None,
None,
false,
BindingKind::Drillthrough,
"'Industries'[Industry]".to_string(),
),
(
Some("ReportSection1"),
None,
None,
false,
BindingKind::Filter,
"'Owners'[Sales owner]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
None,
false,
BindingKind::FieldWell { role: "Category" },
"'Product'[Category]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
None,
false,
BindingKind::Filter,
"'Region'[Country]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
None,
false,
BindingKind::Sort,
"'Sales'[Sales]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
None,
false,
BindingKind::ConditionalFormatting,
"'Sales'[Margin]".to_string(),
),
(
Some("ReportSection2"),
Some("visual2"),
None,
false,
BindingKind::FieldWell { role: "Tooltips" },
"'Sales'[Customers %]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
None,
true,
BindingKind::FieldWell { role: "Values" },
"'Sales'[Total]".to_string(),
),
(
None,
None,
Some("Bookmark1"),
false,
BindingKind::Filter,
"[Total Units]".to_string(),
),
(
Some("ReportSection1"),
None,
Some("Bookmark1"),
false,
BindingKind::Filter,
"'Products'[Product category]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
Some("Bookmark1"),
false,
BindingKind::FieldWell { role: "Rows" },
"'Product'[Subcategory]".to_string(),
),
(
Some("ReportSection1"),
Some("visual1"),
Some("Bookmark1"),
false,
BindingKind::Filter,
"'Product'[Color]".to_string(),
),
]
);
}
#[test]
fn a_filter_yields_target_then_references_in_order() {
let report = sample_with_page(Page {
visuals: vec![Visual {
filters: vec![Filter {
name: Some(NameKey::new("Filter5")),
target: Some(column_target("Product", "Category")),
references: vec![
column_target("Product", "Subcategory"),
measure_target(None, "Units"),
],
}],
..visual("visual1", "donutChart")
}],
..page("ReportSection1")
});
let targets: Vec<&FieldTarget> =
report.bindings().into_iter().map(|b| b.target).collect();
assert_eq!(
targets,
vec![
&column_target("Product", "Category"),
&column_target("Product", "Subcategory"),
&measure_target(None, "Units"),
]
);
}
#[test]
fn a_filter_without_a_target_still_binds_its_references() {
let report = sample_with_page(Page {
visuals: vec![Visual {
filters: vec![Filter {
name: Some(NameKey::new("Filter5")),
target: None,
references: vec![
column_target("Product", "Subcategory"),
measure_target(None, "Units"),
],
}],
..visual("visual1", "donutChart")
}],
..page("ReportSection1")
});
let targets: Vec<&FieldTarget> =
report.bindings().into_iter().map(|b| b.target).collect();
assert_eq!(
targets,
vec![
&column_target("Product", "Subcategory"),
&measure_target(None, "Units"),
]
);
}
#[test]
fn an_inactive_projection_still_binds() {
let report = sample_with_page(Page {
visuals: vec![Visual {
wells: vec![FieldWell {
role: "Y".to_string(),
projections: vec![Projection {
target: column_target("Sales", "Units"),
query_ref: None,
active: false,
}],
}],
..visual("visual1", "lineChart")
}],
..page("ReportSection1")
});
assert_eq!(report.bindings().len(), 1);
}
#[test]
fn a_hidden_pages_visuals_still_bind() {
let report = sample_with_page(Page {
is_hidden: true,
visuals: vec![Visual {
wells: vec![well("Values", &[column_target("Sales", "Units")])],
..visual("visual1", "card")
}],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].kind, BindingKind::FieldWell { role: "Values" });
}
#[test]
fn a_mobile_page_visual_binds_and_is_tagged_mobile() {
let report = sample_with_mobile_page(Page {
visuals: vec![Visual {
wells: vec![well("Values", &[column_target("Sales", "Units")])],
..visual("visual1", "card")
}],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert_eq!(bindings[0].page.unwrap().as_str(), "ReportSection1");
assert_eq!(bindings[0].visual.unwrap().as_str(), "visual1");
assert_eq!(bindings[0].bookmark, None);
assert!(bindings[0].mobile);
assert_eq!(bindings[0].kind, BindingKind::FieldWell { role: "Values" });
}
#[test]
fn a_hidden_mobile_pages_visuals_still_bind() {
let report = sample_with_mobile_page(Page {
is_hidden: true,
visuals: vec![Visual {
wells: vec![well("Values", &[column_target("Sales", "Units")])],
..visual("visual1", "card")
}],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert!(bindings[0].mobile);
}
#[test]
fn a_mobile_page_filter_is_tagged_mobile() {
let report = sample_with_mobile_page(Page {
filters: vec![filter_on(column_target("Sales", "Units"))],
..page("ReportSection1")
});
let bindings = report.bindings();
assert_eq!(bindings.len(), 1);
assert!(bindings[0].mobile);
assert_eq!(bindings[0].kind, BindingKind::Filter);
}
}
mod format_agnostic {
use super::*;
fn report_with(well_target: FieldTarget) -> ReportModel {
ReportModel {
pages: vec![Page {
visuals: vec![Visual {
wells: vec![well("Y", &[well_target])],
..visual("visual1", "clusteredColumnChart")
}],
..page("ReportSection1")
}],
..Default::default()
}
}
#[test]
fn structured_and_written_targets_bind_alike() {
let pbir = report_with(FieldTarget::Measure {
home_table: Some(NameKey::new("Sales")),
measure: NameKey::new("Cost"),
});
let legacy = report_with(FieldTarget::Written(FieldRef {
table: Some(NameKey::new("Sales")),
name: NameKey::new("Cost"),
}));
let pbir_bindings = pbir.bindings();
let legacy_bindings = legacy.bindings();
assert_eq!(pbir_bindings.len(), 1);
assert_eq!(legacy_bindings.len(), 1);
assert_eq!(pbir_bindings[0].page, legacy_bindings[0].page);
assert_eq!(pbir_bindings[0].visual, legacy_bindings[0].visual);
assert_eq!(pbir_bindings[0].bookmark, legacy_bindings[0].bookmark);
assert_eq!(pbir_bindings[0].kind, legacy_bindings[0].kind);
}
}
mod dax_expressions {
use super::*;
#[test]
fn enumerates_a_report_measures_body_and_format_string() {
let report = ReportModel {
measures: vec![ReportMeasure {
name: NameKey::new("Growth %"),
expression: "DIVIDE([Sales] - [Prior Sales], [Prior Sales])".to_string(),
format_string: Some("0.0%;-0.0%;0.0%".to_string()),
}],
..Default::default()
};
let expressions = report.dax_expressions();
assert_eq!(expressions.len(), 2);
assert_eq!(
expressions[0],
DaxExpressionRef {
owner: ExpressionOwner::ReportMeasure {
measure: "Growth %"
},
kind: DaxExpressionKind::ReportMeasure,
home_table: None,
text: "DIVIDE([Sales] - [Prior Sales], [Prior Sales])",
}
);
assert_eq!(
expressions[1],
DaxExpressionRef {
owner: ExpressionOwner::ReportMeasure {
measure: "Growth %"
},
kind: DaxExpressionKind::ReportMeasureFormatString,
home_table: None,
text: "0.0%;-0.0%;0.0%",
}
);
}
#[test]
fn a_measure_without_a_format_string_enumerates_only_its_body() {
let report = ReportModel {
measures: vec![ReportMeasure {
name: NameKey::new("Total Units"),
expression: "SUM('Sales'[Units])".to_string(),
format_string: None,
}],
..Default::default()
};
let expressions = report.dax_expressions();
assert_eq!(expressions.len(), 1);
assert_eq!(expressions[0].kind, DaxExpressionKind::ReportMeasure);
}
#[test]
fn a_report_without_measures_has_none() {
assert!(ReportModel::default().dax_expressions().is_empty());
}
#[test]
fn owner_materializes_a_report_measure_object_id() {
let owner = ExpressionOwner::ReportMeasure {
measure: "Growth %",
};
assert_eq!(
owner.to_object_id().to_string(),
"report measure 'Growth %'"
);
}
}
mod expression_views {
use super::*;
#[test]
fn are_copy_so_enumeration_borrows_everything() {
fn assert_copy<T: Copy>() {}
assert_copy::<BindingRef<'_>>();
assert_copy::<BindingKind<'_>>();
}
}
mod defaults {
use super::*;
#[test]
fn a_dataset_reference_is_unresolved() {
assert_eq!(DatasetReference::default(), DatasetReference::Unresolved);
assert_eq!(ReportModel::default().dataset, DatasetReference::Unresolved);
}
#[test]
fn a_page_binding_kind_is_default() {
assert_eq!(PageBindingKind::default(), PageBindingKind::Default);
assert_eq!(PageBinding::default().kind, PageBindingKind::Default);
}
}
}