mirror of https://github.com/buster-so/buster.git
models created
This commit is contained in:
parent
c3c8100f3c
commit
4791a62531
|
@ -6,4 +6,6 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
serde = { workspace = true }
|
||||
serde_yaml = { workspace = true }
|
||||
# Dependencies will be inherited from the workspace
|
||||
|
|
|
@ -1 +1,2 @@
|
|||
pub mod models;
|
||||
// Placeholder for semantic_layer library code
|
||||
|
|
|
@ -0,0 +1,228 @@
|
|||
use serde::Deserialize;
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct SemanticLayerSpec {
|
||||
pub models: Vec<Model>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Model {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
#[serde(default)] // Use default empty vec if missing
|
||||
pub dimensions: Vec<Dimension>,
|
||||
#[serde(default)]
|
||||
pub measures: Vec<Measure>,
|
||||
#[serde(default)]
|
||||
pub metrics: Vec<Metric>,
|
||||
#[serde(default)]
|
||||
pub filters: Vec<Filter>,
|
||||
#[serde(default)]
|
||||
pub entities: Vec<Entity>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Dimension {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "type")] // Rename field 'type_' to avoid Rust keyword collision
|
||||
pub type_: Option<String>, // 'type' is optional according to spec
|
||||
#[serde(default)] // Default to false if 'searchable' is missing
|
||||
pub searchable: bool,
|
||||
pub options: Option<Vec<String>>, // Default to None if 'options' is missing
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Measure {
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>, // 'type' is optional according to spec
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Metric {
|
||||
pub name: String,
|
||||
pub expr: String,
|
||||
pub description: Option<String>,
|
||||
pub args: Option<Vec<Argument>>, // 'args' is optional
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Filter {
|
||||
pub name: String,
|
||||
pub expr: String,
|
||||
pub description: Option<String>,
|
||||
pub args: Option<Vec<Argument>>, // 'args' is optional
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Argument {
|
||||
pub name: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: String, // 'type' is required for arguments
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize, PartialEq)]
|
||||
pub struct Entity {
|
||||
pub name: String,
|
||||
pub primary_key: String,
|
||||
pub foreign_key: String,
|
||||
#[serde(rename = "type")]
|
||||
pub type_: Option<String>, // 'type' is optional according to spec
|
||||
pub cardinality: Option<String>, // 'cardinality' is optional
|
||||
pub description: Option<String>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use serde_yaml;
|
||||
|
||||
#[test]
|
||||
fn test_deserialize_model_file() {
|
||||
let yaml_content = r#"
|
||||
models:
|
||||
- name: culture
|
||||
description: Core model for cultural groups
|
||||
dimensions:
|
||||
- name: cultureid
|
||||
description: Unique identifier for the culture
|
||||
- name: name
|
||||
description: Culture name
|
||||
options: ["Western", "Eastern"]
|
||||
measures:
|
||||
- name: revenue
|
||||
description: Revenue generated by the culture
|
||||
filters:
|
||||
- name: active_subscribed_customer
|
||||
expr: logins.login_count > {threshold} AND subscriptions.subscription_status = 'active'
|
||||
args:
|
||||
- name: threshold
|
||||
type: integer
|
||||
description: Minimum number of logins
|
||||
description: Customers with logins above threshold and active subscription
|
||||
metrics:
|
||||
- name: popular_product_revenue
|
||||
expr: SUM(revenue) WHERE culture_products.product_count > 5
|
||||
description: Revenue from cultures with popular products
|
||||
entities:
|
||||
- name: logins
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
type: LEFT
|
||||
cardinality: one-to-many
|
||||
description: Links to login activity
|
||||
- name: subscriptions
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
cardinality: one-to-one
|
||||
description: Links to subscription data (no type, LLM decides)
|
||||
- name: culture_products
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
cardinality: many-to-many
|
||||
description: Links to product associations (many-to-many via junction)
|
||||
- name: logins
|
||||
description: Tracks user logins by culture
|
||||
dimensions:
|
||||
- name: cultureid
|
||||
description: Foreign key to culture
|
||||
measures:
|
||||
- name: login_count
|
||||
description: Number of logins
|
||||
entities:
|
||||
- name: culture
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
cardinality: many-to-one
|
||||
- name: subscriptions
|
||||
description: Subscription status for cultures
|
||||
dimensions:
|
||||
- name: cultureid
|
||||
description: Foreign key to culture
|
||||
- name: subscription_status
|
||||
description: Current subscription status
|
||||
options: ["active", "inactive"]
|
||||
entities:
|
||||
- name: culture
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
cardinality: one-to-one
|
||||
- name: culture_products
|
||||
description: Junction table linking cultures to products
|
||||
dimensions:
|
||||
- name: cultureid
|
||||
description: Foreign key to culture
|
||||
- name: productid
|
||||
description: Foreign key to products
|
||||
measures:
|
||||
- name: product_count
|
||||
description: Number of products in this association
|
||||
entities:
|
||||
- name: culture
|
||||
primary_key: cultureid
|
||||
foreign_key: cultureid
|
||||
cardinality: many-to-many
|
||||
- name: products
|
||||
primary_key: productid
|
||||
foreign_key: productid
|
||||
cardinality: many-to-many
|
||||
"#;
|
||||
|
||||
let spec: Result<SemanticLayerSpec, _> = serde_yaml::from_str(yaml_content);
|
||||
assert!(spec.is_ok(), "Failed to deserialize YAML: {:?}", spec.err());
|
||||
let spec = spec.unwrap();
|
||||
|
||||
assert_eq!(spec.models.len(), 4);
|
||||
|
||||
// Basic checks on the first model ('culture')
|
||||
let culture_model = &spec.models[0];
|
||||
assert_eq!(culture_model.name, "culture");
|
||||
assert_eq!(culture_model.description, Some("Core model for cultural groups".to_string()));
|
||||
assert_eq!(culture_model.dimensions.len(), 2);
|
||||
assert_eq!(culture_model.measures.len(), 1);
|
||||
assert_eq!(culture_model.filters.len(), 1);
|
||||
assert_eq!(culture_model.metrics.len(), 1);
|
||||
assert_eq!(culture_model.entities.len(), 3);
|
||||
|
||||
// Check dimension 'name' options
|
||||
let name_dim = &culture_model.dimensions[1];
|
||||
assert_eq!(name_dim.name, "name");
|
||||
assert_eq!(name_dim.options, Some(vec!["Western".to_string(), "Eastern".to_string()]));
|
||||
assert!(!name_dim.searchable); // Default false
|
||||
|
||||
// Check filter 'active_subscribed_customer' args
|
||||
let filter = &culture_model.filters[0];
|
||||
assert_eq!(filter.name, "active_subscribed_customer");
|
||||
assert!(filter.args.is_some());
|
||||
let filter_args = filter.args.as_ref().unwrap();
|
||||
assert_eq!(filter_args.len(), 1);
|
||||
assert_eq!(filter_args[0].name, "threshold");
|
||||
assert_eq!(filter_args[0].type_, "integer");
|
||||
|
||||
|
||||
// Check entity 'logins' type and cardinality
|
||||
let logins_entity = &culture_model.entities[0];
|
||||
assert_eq!(logins_entity.name, "logins");
|
||||
assert_eq!(logins_entity.type_, Some("LEFT".to_string()));
|
||||
assert_eq!(logins_entity.cardinality, Some("one-to-many".to_string()));
|
||||
|
||||
// Check entity 'subscriptions' type and cardinality (optional)
|
||||
let subs_entity = &culture_model.entities[1];
|
||||
assert_eq!(subs_entity.name, "subscriptions");
|
||||
assert_eq!(subs_entity.type_, None);
|
||||
assert_eq!(subs_entity.cardinality, Some("one-to-one".to_string()));
|
||||
|
||||
// Check second model ('logins')
|
||||
let logins_model = &spec.models[1];
|
||||
assert_eq!(logins_model.name, "logins");
|
||||
assert_eq!(logins_model.dimensions.len(), 1);
|
||||
assert_eq!(logins_model.measures.len(), 1);
|
||||
assert_eq!(logins_model.filters.len(), 0); // Default empty vec
|
||||
assert_eq!(logins_model.metrics.len(), 0); // Default empty vec
|
||||
assert_eq!(logins_model.entities.len(), 1);
|
||||
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue