use serde::{Deserialize, Serialize};
fn default_version() -> String {
"1.1".to_string()
}
fn default_api_version() -> String {
"v1.0.0".to_string()
}
fn default_kind() -> String {
"MetricViews".to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct DBMVDocument {
#[serde(default = "default_api_version")]
pub api_version: String,
#[serde(default = "default_kind")]
pub kind: String,
pub system: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default)]
pub metric_views: Vec<DBMVMetricView>,
}
impl Default for DBMVDocument {
fn default() -> Self {
Self {
api_version: default_api_version(),
kind: default_kind(),
system: String::new(),
description: None,
metric_views: Vec::new(),
}
}
}
impl DBMVDocument {
pub fn new(system: impl Into<String>) -> Self {
Self {
system: system.into(),
..Default::default()
}
}
pub fn add_metric_view(&mut self, view: DBMVMetricView) {
self.metric_views.push(view);
}
pub fn get_metric_view(&self, name: &str) -> Option<&DBMVMetricView> {
self.metric_views.iter().find(|v| v.name == name)
}
pub fn from_yaml(yaml_content: &str) -> Result<Self, serde_yaml::Error> {
serde_yaml::from_str(yaml_content)
}
pub fn to_yaml(&self) -> Result<String, serde_yaml::Error> {
serde_yaml::to_string(self)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVMetricView {
pub name: String,
#[serde(default = "default_version")]
pub version: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub filter: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dimensions: Vec<DBMVDimension>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub measures: Vec<DBMVMeasure>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub joins: Vec<DBMVJoin>,
#[serde(skip_serializing_if = "Option::is_none")]
pub materialization: Option<DBMVMaterialization>,
}
impl Default for DBMVMetricView {
fn default() -> Self {
Self {
name: String::new(),
version: default_version(),
source: String::new(),
filter: None,
comment: None,
dimensions: Vec::new(),
measures: Vec::new(),
joins: Vec::new(),
materialization: None,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVDimension {
pub name: String,
pub expr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVMeasure {
pub name: String,
pub expr: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub display_name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub comment: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub format: Option<DBMVMeasureFormat>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub window: Vec<DBMVWindow>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVMeasureFormat {
#[serde(rename = "type")]
pub format_type: String,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVWindow {
pub order: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub range: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub semiadditive: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVJoin {
pub name: String,
pub source: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub on: Option<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub using: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub joins: Vec<DBMVJoin>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVMaterialization {
pub schedule: String,
pub mode: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub materialized_views: Vec<DBMVMaterializedView>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct DBMVMaterializedView {
pub name: String,
#[serde(rename = "type")]
pub view_type: String,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub dimensions: Vec<String>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub measures: Vec<String>,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_document_new() {
let doc = DBMVDocument::new("my-system");
assert_eq!(doc.system, "my-system");
assert_eq!(doc.api_version, "v1.0.0");
assert_eq!(doc.kind, "MetricViews");
assert!(doc.metric_views.is_empty());
}
#[test]
fn test_document_add_metric_view() {
let mut doc = DBMVDocument::new("test-system");
doc.add_metric_view(DBMVMetricView {
name: "orders".to_string(),
source: "catalog.schema.orders".to_string(),
..Default::default()
});
assert_eq!(doc.metric_views.len(), 1);
assert_eq!(doc.get_metric_view("orders").unwrap().name, "orders");
assert!(doc.get_metric_view("nonexistent").is_none());
}
#[test]
fn test_default_version() {
let view = DBMVMetricView::default();
assert_eq!(view.version, "1.1");
}
#[test]
fn test_measure_format_type_rename() {
let format = DBMVMeasureFormat {
format_type: "currency".to_string(),
};
let yaml = serde_yaml::to_string(&format).unwrap();
assert!(yaml.contains("type: currency"));
}
#[test]
fn test_materialized_view_type_rename() {
let mv = DBMVMaterializedView {
name: "test".to_string(),
view_type: "aggregated".to_string(),
dimensions: vec![],
measures: vec![],
};
let yaml = serde_yaml::to_string(&mv).unwrap();
assert!(yaml.contains("type: aggregated"));
}
#[test]
fn test_document_yaml_roundtrip() {
let mut doc = DBMVDocument::new("test-system");
doc.description = Some("Test metrics".to_string());
doc.add_metric_view(DBMVMetricView {
name: "orders_metrics".to_string(),
source: "catalog.schema.orders".to_string(),
dimensions: vec![DBMVDimension {
name: "order_date".to_string(),
expr: "order_date".to_string(),
display_name: Some("Order Date".to_string()),
comment: None,
}],
measures: vec![DBMVMeasure {
name: "total_revenue".to_string(),
expr: "SUM(revenue)".to_string(),
display_name: Some("Total Revenue".to_string()),
comment: None,
format: Some(DBMVMeasureFormat {
format_type: "currency".to_string(),
}),
window: vec![],
}],
..Default::default()
});
let yaml = doc.to_yaml().unwrap();
let parsed = DBMVDocument::from_yaml(&yaml).unwrap();
assert_eq!(doc, parsed);
}
#[test]
fn test_camel_case_envelope_snake_case_inner() {
let doc = DBMVDocument::new("test");
let yaml = doc.to_yaml().unwrap();
assert!(yaml.contains("apiVersion:"));
assert!(yaml.contains("metricViews:"));
assert!(!yaml.contains("api_version:"));
assert!(!yaml.contains("metric_views:"));
}
#[test]
fn test_inner_fields_snake_case() {
let mut doc = DBMVDocument::new("test");
doc.add_metric_view(DBMVMetricView {
name: "test_view".to_string(),
source: "catalog.schema.table".to_string(),
dimensions: vec![DBMVDimension {
name: "dim1".to_string(),
expr: "col1".to_string(),
display_name: Some("Dimension 1".to_string()),
comment: None,
}],
measures: vec![DBMVMeasure {
name: "measure1".to_string(),
expr: "SUM(col2)".to_string(),
display_name: None,
comment: None,
format: None,
window: vec![],
}],
..Default::default()
});
let yaml = doc.to_yaml().unwrap();
assert!(yaml.contains("display_name:"));
}
#[test]
fn test_nested_joins() {
let join = DBMVJoin {
name: "customers".to_string(),
source: "catalog.schema.customers".to_string(),
on: Some("source.customer_id = customers.id".to_string()),
using: vec![],
joins: vec![DBMVJoin {
name: "nation".to_string(),
source: "catalog.schema.nations".to_string(),
on: Some("customers.nation_id = nation.id".to_string()),
using: vec![],
joins: vec![],
}],
};
let yaml = serde_yaml::to_string(&join).unwrap();
assert!(yaml.contains("nation"));
assert!(yaml.contains("customers.nation_id"));
let parsed: DBMVJoin = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(join, parsed);
}
#[test]
fn test_window_measure() {
let measure = DBMVMeasure {
name: "ytd_revenue".to_string(),
expr: "SUM(revenue)".to_string(),
display_name: None,
comment: None,
format: None,
window: vec![DBMVWindow {
order: "order_date".to_string(),
range: Some("cumulative".to_string()),
semiadditive: Some("last".to_string()),
}],
};
let yaml = serde_yaml::to_string(&measure).unwrap();
let parsed: DBMVMeasure = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(measure, parsed);
}
#[test]
fn test_materialization() {
let mat = DBMVMaterialization {
schedule: "every 6 hours".to_string(),
mode: "relaxed".to_string(),
materialized_views: vec![
DBMVMaterializedView {
name: "baseline".to_string(),
view_type: "unaggregated".to_string(),
dimensions: vec![],
measures: vec![],
},
DBMVMaterializedView {
name: "revenue_by_date".to_string(),
view_type: "aggregated".to_string(),
dimensions: vec!["order_date".to_string()],
measures: vec!["total_revenue".to_string()],
},
],
};
let yaml = serde_yaml::to_string(&mat).unwrap();
assert!(yaml.contains("materialized_views:"));
let parsed: DBMVMaterialization = serde_yaml::from_str(&yaml).unwrap();
assert_eq!(mat, parsed);
}
#[test]
fn test_optional_fields_omitted() {
let view = DBMVMetricView {
name: "simple".to_string(),
source: "catalog.schema.table".to_string(),
..Default::default()
};
let yaml = serde_yaml::to_string(&view).unwrap();
assert!(!yaml.contains("filter:"));
assert!(!yaml.contains("comment:"));
assert!(!yaml.contains("dimensions:"));
assert!(!yaml.contains("measures:"));
assert!(!yaml.contains("joins:"));
assert!(!yaml.contains("materialization:"));
}
}