models created

This commit is contained in:
dal 2025-05-05 18:47:49 -06:00
parent c3c8100f3c
commit 4791a62531
No known key found for this signature in database
GPG Key ID: 16F4B0E1E9F61122
3 changed files with 231 additions and 0 deletions

View File

@ -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

View File

@ -1 +1,2 @@
pub mod models;
// Placeholder for semantic_layer library code

View File

@ -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);
}
}