mod pbir;
mod tmdl;
use std::fs;
use std::path::{Path, PathBuf};
use crate::model::TabularDatabase;
use crate::report::{DatasetReference, ReportModel};
use crate::{Error, Result};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SkipNotice {
pub path: PathBuf,
pub location: Option<String>,
pub kind: SkipKind,
pub detail: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SkipKind {
UnknownObject,
UnknownProperty,
MalformedValue,
UnresolvedAlias,
StaleState,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Ingested<T> {
pub value: T,
pub skips: Vec<SkipNotice>,
}
pub fn semantic_model(path: &Path) -> Result<Ingested<TabularDatabase>> {
let definition = locate_definition(path)?;
let item_root = definition.parent().unwrap_or(path);
let name = platform_display_name(item_root);
let mut skips = Vec::new();
let value = tmdl::load_database(&definition, name, &mut skips)?;
Ok(Ingested { value, skips })
}
pub fn report(path: &Path) -> Result<Ingested<ReportModel>> {
let definition = locate_report_definition(path)?;
let item_root = definition.parent().unwrap_or(path);
let name = platform_display_name(item_root);
let mut skips = Vec::new();
let value = pbir::load_report(&definition, name, &mut skips)?;
Ok(Ingested { value, skips })
}
#[must_use]
pub fn dataset_reference(item_root: &Path) -> (DatasetReference, Vec<SkipNotice>) {
let mut skips = Vec::new();
let reference = pbir::dataset_reference(item_root, &mut skips);
(reference, skips)
}
pub fn locate_definition(path: &Path) -> Result<PathBuf> {
if !path.is_dir() {
return Err(Error::UnsupportedFormat(format!(
"not a semantic model: {} is not a directory",
path.display()
)));
}
let nested = path.join("definition");
if nested.is_dir() {
return Ok(nested);
}
let looks_like_definition = path.join("model.tmdl").is_file()
|| path
.file_name()
.is_some_and(|name| name.eq_ignore_ascii_case("definition"));
if looks_like_definition {
return Ok(path.to_path_buf());
}
Err(Error::UnsupportedFormat(format!(
"not a semantic model: no definition/ or model.tmdl under {}",
path.display()
)))
}
fn locate_report_definition(path: &Path) -> Result<PathBuf> {
if !path.is_dir() {
return Err(Error::UnsupportedFormat(format!(
"not a report: {} is not a directory",
path.display()
)));
}
let nested = path.join("definition");
if nested.join("report.json").is_file() {
return Ok(nested);
}
if path.join("report.json").is_file() {
return Ok(path.to_path_buf());
}
Err(Error::UnsupportedFormat(format!(
"not a report: no definition/report.json or report.json under {}",
path.display()
)))
}
#[must_use]
pub fn platform_display_name(item_root: &Path) -> Option<String> {
let text = fs::read_to_string(item_root.join(".platform")).ok()?;
let platform: serde_json::Value = serde_json::from_str(&text).ok()?;
platform
.get("metadata")?
.get("displayName")?
.as_str()
.map(str::to_string)
}