1pub mod args;
2pub mod errors;
3mod helpers;
4
5use calamine::{open_workbook, Data, Range, Reader, Xlsx};
6use std::collections::{HashMap, HashSet};
7
8use crate::layout::value::{DataValue, ValueSource};
9use errors::VariantError;
10
11pub struct DataSheet {
12 names: Vec<String>,
13 default_values: Vec<Data>,
14 variant_columns: Vec<Vec<Data>>,
15 sheets: HashMap<String, Range<Data>>,
16}
17
18impl DataSheet {
19 pub fn new(args: &args::VariantArgs) -> Result<Option<Self>, VariantError> {
20 let Some(xlsx_path) = &args.xlsx else {
21 return Ok(None);
22 };
23
24 let mut workbook: Xlsx<_> = open_workbook(xlsx_path)
25 .map_err(|_| VariantError::FileError(format!("failed to open file: {}", xlsx_path)))?;
26
27 let main_sheet = workbook
28 .worksheet_range(&args.main_sheet)
29 .map_err(|_| VariantError::MiscError("Main sheet not found.".to_string()))?;
30
31 let rows: Vec<_> = main_sheet.rows().collect();
32 let (headers, data_rows) = match rows.split_first() {
33 Some((hdr, tail)) => (hdr, tail.len()),
34 None => {
35 return Err(VariantError::RetrievalError(
36 "invalid main sheet format.".to_string(),
37 ));
38 }
39 };
40
41 let name_index = headers
42 .iter()
43 .position(|cell| Self::cell_eq_ascii(cell, "Name"))
44 .ok_or(VariantError::ColumnNotFound("Name".to_string()))?;
45
46 let default_index = headers
47 .iter()
48 .position(|cell| Self::cell_eq_ascii(cell, "Default"))
49 .ok_or(VariantError::ColumnNotFound("Default".to_string()))?;
50
51 let mut names: Vec<String> = Vec::with_capacity(data_rows);
52 names.extend(rows.iter().skip(1).map(|row| {
53 row.get(name_index)
54 .map(|c| c.to_string().trim().to_string())
55 .unwrap_or_default()
56 }));
57 helpers::warn_duplicate_names(&names);
58
59 let default_values = Self::collect_column(&rows, default_index, data_rows);
60
61 let variant_columns = Self::collect_variant_columns(headers, &rows, data_rows, args)?;
62
63 let mut sheets: HashMap<String, Range<Data>> =
64 HashMap::with_capacity(workbook.worksheets().len().saturating_sub(1));
65 for (name, sheet) in workbook.worksheets() {
66 if name != args.main_sheet {
67 sheets.insert(name.to_string(), sheet);
68 }
69 }
70
71 Ok(Some(Self {
72 names,
73 default_values,
74 variant_columns,
75 sheets,
76 }))
77 }
78
79 pub fn retrieve_single_value(&self, name: &str) -> Result<DataValue, VariantError> {
80 let result = (|| match self.retrieve_cell(name)? {
81 Data::Int(i) => Ok(DataValue::I64(*i)),
82 Data::Float(f) => Ok(DataValue::F64(*f)),
83 Data::Bool(b) => Ok(DataValue::Bool(*b)),
84 _ => Err(VariantError::RetrievalError(
85 "Found non-numeric single value".to_string(),
86 )),
87 })();
88
89 result.map_err(|e| VariantError::WhileRetrieving {
90 name: name.to_string(),
91 source: Box::new(e),
92 })
93 }
94
95 pub fn retrieve_1d_array_or_string(&self, name: &str) -> Result<ValueSource, VariantError> {
96 let result = (|| {
97 let Data::String(cell_string) = self.retrieve_cell(name)? else {
98 return Err(VariantError::RetrievalError(
99 "Expected string value for 1D array or string".to_string(),
100 ));
101 };
102
103 if let Some(sheet_name) = cell_string.strip_prefix('#') {
105 let sheet = self.sheets.get(sheet_name).ok_or_else(|| {
106 let available: Vec<_> = self.sheets.keys().map(|s| s.as_str()).collect();
107 VariantError::RetrievalError(format!(
108 "Sheet not found: '{}'. Available sheets: {}",
109 sheet_name,
110 available.join(", ")
111 ))
112 })?;
113
114 let mut out = Vec::new();
115
116 for row in sheet.rows().skip(1) {
117 match row.first() {
118 Some(cell) if !Self::cell_is_empty(cell) => {
119 let v = match cell {
120 Data::Int(i) => DataValue::I64(*i),
121 Data::Float(f) => DataValue::F64(*f),
122 Data::Bool(b) => DataValue::Bool(*b),
123 Data::String(s) => DataValue::Str(s.to_owned()),
124 _ => {
125 return Err(VariantError::RetrievalError(
126 "Unsupported data type in 1D array".to_string(),
127 ));
128 }
129 };
130 out.push(v);
131 }
132 _ => break,
133 }
134 }
135 return Ok(ValueSource::Array(out));
136 }
137
138 Ok(ValueSource::Single(DataValue::Str(cell_string.to_owned())))
140 })();
141
142 result.map_err(|e| VariantError::WhileRetrieving {
143 name: name.to_string(),
144 source: Box::new(e),
145 })
146 }
147
148 pub fn retrieve_2d_array(&self, name: &str) -> Result<Vec<Vec<DataValue>>, VariantError> {
149 let result = (|| {
150 let Data::String(cell_string) = self.retrieve_cell(name)? else {
151 return Err(VariantError::RetrievalError(
152 "Expected string value for 2D array".to_string(),
153 ));
154 };
155
156 let sheet_name = cell_string.strip_prefix('#').ok_or_else(|| {
157 VariantError::RetrievalError(format!(
158 "2D array reference must start with '#' prefix, got: {}",
159 cell_string
160 ))
161 })?;
162
163 let sheet = self.sheets.get(sheet_name).ok_or_else(|| {
164 let available: Vec<_> = self.sheets.keys().map(|s| s.as_str()).collect();
165 VariantError::RetrievalError(format!(
166 "Sheet not found: '{}'. Available sheets: {}",
167 sheet_name,
168 available.join(", ")
169 ))
170 })?;
171
172 let convert = |cell: &Data| -> Result<DataValue, VariantError> {
173 match cell {
174 Data::Int(i) => Ok(DataValue::I64(*i)),
175 Data::Float(f) => Ok(DataValue::F64(*f)),
176 Data::Bool(b) => Ok(DataValue::Bool(*b)),
177 _ => Err(VariantError::RetrievalError(
178 "Unsupported data type in 2D array".to_string(),
179 )),
180 }
181 };
182
183 let mut rows = sheet.rows();
184 let hdrs = rows.next().ok_or_else(|| {
185 VariantError::RetrievalError("No headers found in 2D array".to_string())
186 })?;
187 let width = hdrs.iter().take_while(|c| !Self::cell_is_empty(c)).count();
188 if width == 0 {
189 return Err(VariantError::RetrievalError(
190 "Detected zero width 2D array".to_string(),
191 ));
192 }
193
194 let mut out = Vec::new();
195
196 'outer: for row in rows {
197 if row.first().is_none_or(Self::cell_is_empty) {
198 break;
199 }
200
201 let mut vals = Vec::with_capacity(width);
202 for col in 0..width {
203 let Some(cell) = row.get(col) else {
204 break 'outer;
205 };
206 if Self::cell_is_empty(cell) {
207 break 'outer;
208 };
209 vals.push(convert(cell)?);
210 }
211 out.push(vals);
212 }
213
214 Ok(out)
215 })();
216
217 result.map_err(|e| VariantError::WhileRetrieving {
218 name: name.to_string(),
219 source: Box::new(e),
220 })
221 }
222
223 fn retrieve_cell(&self, name: &str) -> Result<&Data, VariantError> {
224 let index =
225 self.names
226 .iter()
227 .position(|n| n == name)
228 .ok_or(VariantError::RetrievalError(
229 "index not found in data sheet".to_string(),
230 ))?;
231
232 for column in &self.variant_columns {
233 if let Some(value) = column.get(index) {
234 if !Self::cell_is_empty(value) {
235 return Ok(value);
236 }
237 }
238 }
239
240 if let Some(default) = self.default_values.get(index) {
241 if !Self::cell_is_empty(default) {
242 return Ok(default);
243 }
244 }
245
246 Err(VariantError::RetrievalError(
247 "data not found in any variant column".to_string(),
248 ))
249 }
250
251 fn cell_eq_ascii(cell: &Data, target: &str) -> bool {
252 match cell {
253 Data::String(s) => s.trim().eq_ignore_ascii_case(target),
254 _ => false,
255 }
256 }
257
258 fn cell_is_empty(cell: &Data) -> bool {
259 match cell {
260 Data::Empty => true,
261 Data::String(s) => s.trim().is_empty(),
262 _ => false,
263 }
264 }
265
266 fn collect_column(rows: &[&[Data]], index: usize, data_rows: usize) -> Vec<Data> {
267 let mut column = Vec::with_capacity(data_rows);
268 column.extend(
269 rows.iter()
270 .skip(1)
271 .map(|row| row.get(index).cloned().unwrap_or(Data::Empty)),
272 );
273 column
274 }
275
276 fn parse_variant_stack(raw: &str) -> Vec<String> {
277 raw.split('/')
278 .map(|name| name.trim())
279 .filter(|name| !name.is_empty())
280 .map(|name| name.to_string())
281 .collect()
282 }
283
284 fn collect_variant_columns(
285 headers: &[Data],
286 rows: &[&[Data]],
287 data_rows: usize,
288 args: &args::VariantArgs,
289 ) -> Result<Vec<Vec<Data>>, VariantError> {
290 let mut names: Vec<String> = Vec::new();
291
292 if args.debug {
293 eprintln!("Warning: --debug flag is deprecated; include 'Debug' in --variant instead.");
294 names.push("Debug".to_string());
295 }
296
297 if let Some(raw) = &args.variant {
298 names.extend(Self::parse_variant_stack(raw));
299 }
300
301 let mut seen = HashSet::new();
302 let mut columns = Vec::new();
303
304 for name in names {
305 if !seen.insert(name.clone()) {
306 continue;
307 }
308
309 let index = headers
310 .iter()
311 .position(|cell| Self::cell_eq_ascii(cell, &name))
312 .ok_or_else(|| VariantError::ColumnNotFound(name.clone()))?;
313
314 columns.push(Self::collect_column(rows, index, data_rows));
315 }
316
317 Ok(columns)
318 }
319}
320
321#[cfg(test)]
322mod tests {
323 use super::*;
324 use std::collections::HashMap;
325
326 fn datasheet_with_default(value: Data) -> DataSheet {
327 DataSheet {
328 names: vec!["Flag".to_string()],
329 default_values: vec![value],
330 variant_columns: Vec::new(),
331 sheets: HashMap::new(),
332 }
333 }
334
335 #[test]
336 fn retrieve_single_value_accepts_bool_cell() {
337 let ds = datasheet_with_default(Data::Bool(true));
338 let value = ds.retrieve_single_value("Flag").expect("bool cell");
339 match value {
340 DataValue::Bool(v) => assert!(v),
341 _ => panic!("expected bool value"),
342 }
343 }
344}