1use serde::{Deserialize, Serialize};
2
3#[derive(Debug, Clone, Deserialize, Serialize)]
4#[serde(rename_all = "camelCase")]
5pub struct TransformSpec {
6 pub sql: Option<SqlSpec>,
7 pub aggregate: Option<AggregateSpec>,
8 pub forecast: Option<ForecastSpec>,
9}
10
11#[derive(Debug, Clone, Deserialize, Serialize)]
12#[serde(untagged)]
13pub enum SqlSpec {
14 Single(String),
15 Multiple(Vec<String>),
16}
17
18#[derive(Debug, Clone, Deserialize, Serialize)]
19#[serde(rename_all = "camelCase")]
20pub struct ForecastSpec {
21 pub timestamp: String,
22 pub value: String,
23 pub horizon: Option<u64>,
24 pub confidence_level: Option<f64>,
25 pub model: Option<String>,
26 #[serde(default)]
27 pub group_by: Option<Vec<String>>,
28}
29
30#[derive(Debug, Clone, Deserialize, Serialize)]
31#[serde(rename_all = "camelCase")]
32pub struct AggregateSpec {
33 #[serde(default)]
34 pub dimensions: Vec<Dimension>,
35 #[serde(default)]
36 pub measures: Vec<Measure>,
37 pub filters: Option<FilterGroup>,
38 pub sort: Option<Vec<SortSpec>>,
39 pub limit: Option<u64>,
40}
41
42#[derive(Debug, Clone, Deserialize, Serialize)]
43#[serde(untagged)]
44pub enum Dimension {
45 Simple(String),
46 Detailed(DimensionSpec),
47}
48
49#[derive(Debug, Clone, Deserialize, Serialize)]
50#[serde(rename_all = "camelCase")]
51pub struct DimensionSpec {
52 pub column: String,
53 pub name: Option<String>,
54 #[serde(rename = "type")]
55 pub dim_type: Option<String>,
56}
57
58#[derive(Debug, Clone, Deserialize, Serialize)]
59#[serde(rename_all = "camelCase")]
60pub struct Measure {
61 pub column: Option<String>,
62 pub aggregation: Option<String>,
63 pub name: String,
64 pub expression: Option<String>,
65}
66
67#[derive(Debug, Clone, Deserialize, Serialize)]
68#[serde(rename_all = "camelCase")]
69pub struct FilterGroup {
70 pub combinator: Option<String>,
71 pub rules: Vec<FilterRule>,
72}
73
74#[derive(Debug, Clone, Deserialize, Serialize)]
75#[serde(rename_all = "camelCase")]
76pub struct FilterRule {
77 pub field: String,
78 pub operator: String,
79 pub value: Option<serde_json::Value>,
80}
81
82#[derive(Debug, Clone, Deserialize, Serialize)]
83#[serde(rename_all = "camelCase")]
84pub struct SortSpec {
85 pub field: String,
86 pub direction: Option<String>,
87}
88
89#[cfg(test)]
90mod tests {
91 use super::*;
92
93 #[test]
94 fn test_backward_compat_aggregate_only() {
95 let yaml = r#"
96aggregate:
97 dimensions:
98 - region
99 measures:
100 - column: amount
101 aggregation: sum
102 name: totalAmount
103 limit: 10
104"#;
105 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
106 assert!(spec.aggregate.is_some());
107 assert!(spec.sql.is_none());
108 assert!(spec.forecast.is_none());
109 let agg = spec.aggregate.unwrap();
110 assert_eq!(agg.dimensions.len(), 1);
111 assert_eq!(agg.measures.len(), 1);
112 assert_eq!(agg.limit, Some(10));
113 }
114
115 #[test]
116 fn test_sql_single_string() {
117 let yaml = r#"
118sql: "SELECT * FROM sales WHERE year = 2024"
119"#;
120 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
121 assert!(spec.aggregate.is_none());
122 assert!(spec.forecast.is_none());
123 match spec.sql.unwrap() {
124 SqlSpec::Single(s) => assert_eq!(s, "SELECT * FROM sales WHERE year = 2024"),
125 other => panic!("Expected SqlSpec::Single, got {:?}", other),
126 }
127 }
128
129 #[test]
130 fn test_sql_multiple_strings() {
131 let yaml = r#"
132sql:
133 - "SELECT * FROM sales"
134 - "WHERE year = 2024"
135 - "ORDER BY revenue DESC"
136"#;
137 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
138 match spec.sql.unwrap() {
139 SqlSpec::Multiple(v) => {
140 assert_eq!(v.len(), 3);
141 assert_eq!(v[0], "SELECT * FROM sales");
142 assert_eq!(v[1], "WHERE year = 2024");
143 assert_eq!(v[2], "ORDER BY revenue DESC");
144 }
145 other => panic!("Expected SqlSpec::Multiple, got {:?}", other),
146 }
147 }
148
149 #[test]
150 fn test_forecast_all_fields() {
151 let yaml = r#"
152forecast:
153 timestamp: date
154 value: revenue
155 horizon: 60
156 confidenceLevel: 0.99
157 model: arima
158 groupBy:
159 - region
160 - category
161"#;
162 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
163 assert!(spec.sql.is_none());
164 assert!(spec.aggregate.is_none());
165 let forecast = spec.forecast.unwrap();
166 assert_eq!(forecast.timestamp, "date");
167 assert_eq!(forecast.value, "revenue");
168 assert_eq!(forecast.horizon, Some(60));
169 assert_eq!(forecast.confidence_level, Some(0.99));
170 assert_eq!(forecast.model, Some("arima".to_string()));
171 let groups = forecast.group_by.unwrap();
172 assert_eq!(groups.len(), 2);
173 assert_eq!(groups[0], "region");
174 assert_eq!(groups[1], "category");
175 }
176
177 #[test]
178 fn test_forecast_required_fields_only() {
179 let yaml = r#"
180forecast:
181 timestamp: date
182 value: revenue
183"#;
184 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
185 let forecast = spec.forecast.unwrap();
186 assert_eq!(forecast.timestamp, "date");
187 assert_eq!(forecast.value, "revenue");
188 assert!(forecast.horizon.is_none());
189 assert!(forecast.confidence_level.is_none());
190 assert!(forecast.model.is_none());
191 assert!(forecast.group_by.is_none());
192 }
193
194 #[test]
195 fn test_all_three_fields() {
196 let yaml = r#"
197sql: "SELECT * FROM sales"
198aggregate:
199 dimensions:
200 - region
201 measures:
202 - column: amount
203 aggregation: sum
204 name: totalAmount
205forecast:
206 timestamp: date
207 value: totalAmount
208 horizon: 30
209"#;
210 let spec: TransformSpec = serde_yaml::from_str(yaml).unwrap();
211 match spec.sql.unwrap() {
212 SqlSpec::Single(s) => assert_eq!(s, "SELECT * FROM sales"),
213 other => panic!("Expected SqlSpec::Single, got {:?}", other),
214 }
215 let agg = spec.aggregate.unwrap();
216 assert_eq!(agg.dimensions.len(), 1);
217 assert_eq!(agg.measures.len(), 1);
218 let forecast = spec.forecast.unwrap();
219 assert_eq!(forecast.timestamp, "date");
220 assert_eq!(forecast.value, "totalAmount");
221 assert_eq!(forecast.horizon, Some(30));
222 }
223}