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
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
|
serde = { workspace = true }
|
||||||
|
serde_yaml = { workspace = true }
|
||||||
# Dependencies will be inherited from the workspace
|
# Dependencies will be inherited from the workspace
|
||||||
|
|
|
@ -1 +1,2 @@
|
||||||
|
pub mod models;
|
||||||
// Placeholder for semantic_layer library code
|
// 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