mollendorff_forge/parser/
variables.rs1use crate::error::{ForgeError, ForgeResult};
6use crate::types::{Column, Metadata, Table, Variable};
7use serde_yaml_ng::Value;
8
9use super::arrays::parse_array_value;
10
11pub fn parse_table(name: &str, map: &serde_yaml_ng::Mapping) -> ForgeResult<Table> {
17 let mut table = Table::new(name.to_string());
18
19 for (key, value) in map {
20 let col_name = key
21 .as_str()
22 .ok_or_else(|| ForgeError::Parse("Column name must be a string".to_string()))?;
23
24 if col_name == "_metadata" {
26 continue;
27 }
28
29 if let Value::String(s) = value {
31 if s.starts_with('=') {
32 table.add_row_formula(col_name.to_string(), s.clone());
34 continue;
35 }
36 }
37
38 if let Value::Mapping(col_map) = value {
40 if let Some(Value::Sequence(seq)) = col_map.get("value") {
42 let column_value = parse_array_value(col_name, seq)?;
43 let metadata = parse_metadata(col_map);
44 let column = Column::with_metadata(col_name.to_string(), column_value, metadata);
45 table.add_column(column);
46 continue;
47 }
48 if let Some(formula_val) = col_map.get("formula") {
50 if let Some(formula_str) = formula_val.as_str() {
51 if formula_str.starts_with('=') {
52 table.add_row_formula(col_name.to_string(), formula_str.to_string());
55 continue;
56 }
57 }
58 }
59 }
60
61 if let Value::Sequence(seq) = value {
63 let column_value = parse_array_value(col_name, seq)?;
64 let column = Column::new(col_name.to_string(), column_value);
65 table.add_column(column);
66 } else {
67 return Err(ForgeError::Parse(format!(
68 "Column '{col_name}' in table '{name}' must be an array or formula"
69 )));
70 }
71 }
72
73 Ok(table)
74}
75
76pub fn parse_scalar_variable(value: &Value, path: &str) -> ForgeResult<Variable> {
82 if let Value::Mapping(map) = value {
83 let val = map.get("value").and_then(serde_yaml_ng::Value::as_f64);
84 let formula = map
85 .get("formula")
86 .and_then(|f| f.as_str().map(std::string::ToString::to_string));
87
88 let metadata = parse_metadata(map);
90
91 Ok(Variable {
92 path: path.to_string(),
93 value: val,
94 formula,
95 metadata,
96 })
97 } else {
98 Err(ForgeError::Parse(format!(
99 "Expected mapping for scalar variable '{path}'"
100 )))
101 }
102}
103
104#[must_use]
106pub fn parse_metadata(map: &serde_yaml_ng::Mapping) -> Metadata {
107 Metadata {
108 unit: map
109 .get("unit")
110 .and_then(|v| v.as_str().map(std::string::ToString::to_string)),
111 notes: map
112 .get("notes")
113 .and_then(|v| v.as_str().map(std::string::ToString::to_string)),
114 source: map
115 .get("source")
116 .and_then(|v| v.as_str().map(std::string::ToString::to_string)),
117 validation_status: map
118 .get("validation_status")
119 .and_then(|v| v.as_str().map(std::string::ToString::to_string)),
120 last_updated: map
121 .get("last_updated")
122 .and_then(|v| v.as_str().map(std::string::ToString::to_string)),
123 }
124}
125
126#[must_use]
129pub fn is_nested_scalar_section(map: &serde_yaml_ng::Mapping) -> bool {
130 for (_key, value) in map {
132 if let Value::Mapping(child_map) = value {
133 if child_map.contains_key("value") || child_map.contains_key("formula") {
135 if let Some(val) = child_map.get("value") {
137 if matches!(val, Value::Sequence(_)) {
138 return false; }
140 }
141 return true; }
143 }
144 }
145 false
146}
147
148#[cfg(test)]
149mod tests {
150 use super::*;
151 use crate::types::ColumnValue;
152 use std::io::Write;
153 use tempfile::NamedTempFile;
154
155 #[test]
156 fn test_parse_table_with_arrays() {
157 let yaml = r"
158 month: ['Jan', 'Feb', 'Mar']
159 revenue: [100, 200, 300]
160 ";
161 let parsed: Value = serde_yaml_ng::from_str(yaml).unwrap();
162
163 if let Value::Mapping(map) = parsed {
164 let table = parse_table("test_table", &map).unwrap();
165
166 assert_eq!(table.name, "test_table");
167 assert_eq!(table.columns.len(), 2);
168 assert!(table.columns.contains_key("month"));
169 assert!(table.columns.contains_key("revenue"));
170 assert_eq!(table.row_count(), 3);
171 } else {
172 panic!("Expected mapping");
173 }
174 }
175
176 #[test]
177 fn test_parse_table_with_formula() {
178 let yaml = r"
179 revenue: [100, 200, 300]
180 expenses: [50, 100, 150]
181 profit: '=revenue - expenses'
182 ";
183 let parsed: Value = serde_yaml_ng::from_str(yaml).unwrap();
184
185 if let Value::Mapping(map) = parsed {
186 let table = parse_table("test_table", &map).unwrap();
187
188 assert_eq!(table.columns.len(), 2);
189 assert_eq!(table.row_formulas.len(), 1);
190 assert!(table.row_formulas.contains_key("profit"));
191 assert_eq!(
192 table.row_formulas.get("profit").unwrap(),
193 "=revenue - expenses"
194 );
195 } else {
196 panic!("Expected mapping");
197 }
198 }
199
200 #[test]
201 fn test_table_validate_lengths_ok() {
202 let mut table = Table::new("test".to_string());
203 table.add_column(Column::new(
204 "col1".to_string(),
205 ColumnValue::Number(vec![1.0, 2.0, 3.0]),
206 ));
207 table.add_column(Column::new(
208 "col2".to_string(),
209 ColumnValue::Number(vec![4.0, 5.0, 6.0]),
210 ));
211
212 assert!(table.validate_lengths().is_ok());
213 }
214
215 #[test]
216 fn test_table_validate_lengths_error() {
217 let mut table = Table::new("test".to_string());
218 table.add_column(Column::new(
219 "col1".to_string(),
220 ColumnValue::Number(vec![1.0, 2.0, 3.0]),
221 ));
222 table.add_column(Column::new(
223 "col2".to_string(),
224 ColumnValue::Number(vec![4.0, 5.0]),
225 ));
226
227 let result = table.validate_lengths();
228 assert!(result.is_err());
229 let err_msg = result.unwrap_err();
230 assert!(err_msg.contains("col1") || err_msg.contains("col2"));
231 assert!(err_msg.contains("2 rows"));
232 assert!(err_msg.contains("3 rows"));
233 }
234
235 #[test]
236 fn test_column_value_type_name() {
237 let num_col = ColumnValue::Number(vec![1.0]);
238 let text_col = ColumnValue::Text(vec!["A".to_string()]);
239 let date_col = ColumnValue::Date(vec!["2025-01".to_string()]);
240 let bool_col = ColumnValue::Boolean(vec![true]);
241
242 assert_eq!(num_col.type_name(), "Number");
243 assert_eq!(text_col.type_name(), "Text");
244 assert_eq!(date_col.type_name(), "Date");
245 assert_eq!(bool_col.type_name(), "Boolean");
246 }
247
248 #[test]
249 fn test_column_value_len() {
250 let col = ColumnValue::Number(vec![1.0, 2.0, 3.0]);
251 assert_eq!(col.len(), 3);
252 assert!(!col.is_empty());
253
254 let empty_col = ColumnValue::Number(vec![]);
255 assert_eq!(empty_col.len(), 0);
256 assert!(empty_col.is_empty());
257 }
258
259 #[test]
260 fn test_parse_v4_scalar_with_metadata() {
261 let yaml_content = r#"
262_forge_version: "5.0.0"
263
264price:
265 value: 100
266 formula: null
267 unit: "CAD"
268 notes: "Base price per unit"
269 source: "market_research.yaml"
270 validation_status: "VALIDATED"
271"#;
272
273 let mut temp_file = NamedTempFile::new().unwrap();
274 temp_file.write_all(yaml_content.as_bytes()).unwrap();
275
276 let content = std::fs::read_to_string(temp_file.path()).unwrap();
277 let yaml: Value = serde_yaml_ng::from_str(&content).unwrap();
278
279 if let Some(price_val) = yaml.get("price") {
280 let price = parse_scalar_variable(price_val, "price").unwrap();
281 assert_eq!(price.value, Some(100.0));
282 assert!(price.formula.is_none());
283 assert_eq!(price.metadata.unit, Some("CAD".to_string()));
284 assert_eq!(
285 price.metadata.notes,
286 Some("Base price per unit".to_string())
287 );
288 assert_eq!(
289 price.metadata.source,
290 Some("market_research.yaml".to_string())
291 );
292 assert_eq!(
293 price.metadata.validation_status,
294 Some("VALIDATED".to_string())
295 );
296 }
297 }
298
299 #[test]
300 fn test_parse_scalar_variable_not_mapping() {
301 let val = Value::String("not a mapping".to_string());
302 let result = parse_scalar_variable(&val, "test");
303 assert!(result.is_err());
304 assert!(result.unwrap_err().to_string().contains("Expected mapping"));
305 }
306
307 #[test]
308 fn test_metadata_last_updated() {
309 let mut map = serde_yaml_ng::Mapping::new();
310 map.insert(
311 Value::String("last_updated".to_string()),
312 Value::String("2025-01-01".to_string()),
313 );
314 let metadata = parse_metadata(&map);
315 assert_eq!(metadata.last_updated, Some("2025-01-01".to_string()));
316 }
317
318 #[test]
319 fn test_is_nested_scalar_section_false_for_v4_rich_column() {
320 let mut map = serde_yaml_ng::Mapping::new();
321 let mut child = serde_yaml_ng::Mapping::new();
322 child.insert(
323 Value::String("value".to_string()),
324 Value::Sequence(vec![Value::Number(1.into())]),
325 );
326 map.insert(Value::String("col".to_string()), Value::Mapping(child));
327 assert!(!is_nested_scalar_section(&map));
328 }
329
330 #[test]
331 fn test_is_nested_scalar_section_true_for_scalar() {
332 let mut map = serde_yaml_ng::Mapping::new();
333 let mut child = serde_yaml_ng::Mapping::new();
334 child.insert(Value::String("value".to_string()), Value::Number(42.into()));
335 map.insert(Value::String("total".to_string()), Value::Mapping(child));
336 assert!(is_nested_scalar_section(&map));
337 }
338
339 #[test]
340 fn test_is_nested_scalar_section_empty_child() {
341 let mut map = serde_yaml_ng::Mapping::new();
342 map.insert(
343 Value::String("empty".to_string()),
344 Value::Mapping(serde_yaml_ng::Mapping::new()),
345 );
346 assert!(!is_nested_scalar_section(&map));
347 }
348
349 #[test]
350 fn test_parse_table_column_scalar_not_array() {
351 let mut map = serde_yaml_ng::Mapping::new();
352 map.insert(Value::String("col".to_string()), Value::Number(42.into()));
353 let result = parse_table("test", &map);
354 assert!(result.is_err());
355 }
356}