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