mint_cli/variant/
mod.rs

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