1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 DataValue, ErrorCode, Expression, ExpressionBudget, FileMakerError, Length, Result, Style,
19};
20
21pub type DataRow = BTreeMap<String, DataValue>;
23
24pub trait Dataset: Send + Sync {
26 fn row_count_hint(&self) -> Option<u64>;
28 fn visit_rows_until(
30 &self,
31 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
32 ) -> Result<()>;
33
34 fn visit_rows(&self, visitor: &mut dyn FnMut(u64, &DataRow) -> Result<()>) -> Result<()> {
36 self.visit_rows_until(&mut |index, row| {
37 visitor(index, row)?;
38 Ok(true)
39 })
40 }
41}
42
43#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
45pub struct InMemoryDataset {
46 pub rows: Vec<DataRow>,
48}
49
50pub struct BorrowedDataset<'a> {
52 rows: &'a [DataRow],
53}
54
55impl<'a> BorrowedDataset<'a> {
56 #[must_use]
58 pub const fn new(rows: &'a [DataRow]) -> Self {
59 Self { rows }
60 }
61}
62
63impl Dataset for BorrowedDataset<'_> {
64 fn row_count_hint(&self) -> Option<u64> {
65 u64::try_from(self.rows.len()).ok()
66 }
67
68 fn visit_rows_until(
69 &self,
70 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
71 ) -> Result<()> {
72 visit_slice(self.rows, visitor)
73 }
74}
75
76impl Dataset for InMemoryDataset {
77 fn row_count_hint(&self) -> Option<u64> {
78 u64::try_from(self.rows.len()).ok()
79 }
80
81 fn visit_rows_until(
82 &self,
83 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
84 ) -> Result<()> {
85 visit_slice(&self.rows, visitor)
86 }
87}
88
89fn visit_slice(
90 rows: &[DataRow],
91 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
92) -> Result<()> {
93 for (index, row) in rows.iter().enumerate() {
94 let index = u64::try_from(index).map_err(|_| table_error("row index overflow"))?;
95 if !visitor(index, row)? {
96 break;
97 }
98 }
99 Ok(())
100}
101
102pub struct StreamingDataset<F> {
104 factory: F,
105 row_count_hint: Option<u64>,
106}
107
108impl<F> StreamingDataset<F> {
109 #[must_use]
111 pub const fn new(factory: F, row_count_hint: Option<u64>) -> Self {
112 Self {
113 factory,
114 row_count_hint,
115 }
116 }
117}
118
119impl<F, I> Dataset for StreamingDataset<F>
120where
121 F: Fn() -> I + Send + Sync,
122 I: Iterator<Item = Result<DataRow>>,
123{
124 fn row_count_hint(&self) -> Option<u64> {
125 self.row_count_hint
126 }
127
128 fn visit_rows_until(
129 &self,
130 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
131 ) -> Result<()> {
132 for (index, row) in (self.factory)().enumerate() {
133 let row = row?;
134 if !visitor(
135 u64::try_from(index).map_err(|_| table_error("row index overflow"))?,
136 &row,
137 )? {
138 break;
139 }
140 }
141 Ok(())
142 }
143}
144
145#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
147#[serde(tag = "mode", content = "value", rename_all = "snake_case")]
148pub enum ColumnWidth {
149 Fixed(Length),
151 Flex(u32),
153 Auto,
155}
156
157#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
159pub struct TableColumn {
160 pub field: String,
162 pub header: String,
164 pub width: ColumnWidth,
166}
167
168#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
170#[serde(deny_unknown_fields)]
171pub struct TableStyleRule {
172 pub when: String,
174 pub style: Style,
176}
177
178#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
180pub struct TableSpec {
181 pub columns: Vec<TableColumn>,
183 pub repeat_header: bool,
185 pub group_by: Option<String>,
187 pub total_fields: Vec<String>,
189 pub conditional_styles: Vec<TableStyleRule>,
191 pub style_expression_steps: usize,
193 pub auto_sample_rows: usize,
195 pub max_rows: u64,
197 pub max_row_fields: usize,
199 pub max_cell_bytes: usize,
201}
202
203#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
205pub struct TablePage {
206 pub index: usize,
208 pub header: bool,
210 pub rows: Vec<DataRow>,
212 pub row_heights: Vec<crate::Unit>,
214 pub row_styles: Vec<Style>,
216 pub group_starts: Vec<Option<String>>,
218 pub starting_group: Option<String>,
220 pub totals: BTreeMap<String, DataValue>,
222}
223
224pub trait TablePageSink {
226 fn page(&mut self, page: TablePage) -> Result<()>;
228}
229
230pub struct TablePaginator {
232 pub available_height: crate::Unit,
234 pub header_height: crate::Unit,
236 pub row_height: crate::Unit,
238 pub max_pages: usize,
240}
241
242impl TablePaginator {
243 pub fn paginate(
245 &self,
246 spec: &TableSpec,
247 dataset: &dyn Dataset,
248 sink: &mut dyn TablePageSink,
249 ) -> Result<()> {
250 if self.row_height <= crate::Unit::ZERO {
251 return Err(table_error("fixed table row height must be positive"));
252 }
253 self.paginate_measured(spec, dataset, &mut |_| Ok(self.row_height), sink)
254 }
255
256 pub fn paginate_measured(
258 &self,
259 spec: &TableSpec,
260 dataset: &dyn Dataset,
261 measure: &mut dyn FnMut(&DataRow) -> Result<crate::Unit>,
262 sink: &mut dyn TablePageSink,
263 ) -> Result<()> {
264 spec.validate()?;
265 if self.available_height <= crate::Unit::ZERO
266 || self.header_height < crate::Unit::ZERO
267 || self.max_pages == 0
268 {
269 return Err(table_error("table pagination dimensions are invalid"));
270 }
271 let mut state = PaginationState::default();
272 spec.visit_bounded(dataset, &mut |_, row| {
273 let height = measure(row)?;
274 let mut content_height = self.content_height(spec, state.page_index)?;
275 if height <= crate::Unit::ZERO || height > content_height {
276 return Err(table_error("measured table row does not fit on a page"));
277 }
278 if !state.rows.is_empty() && state.used_height.checked_add(height)? > content_height {
279 state.flush(spec, sink, self.max_pages, false)?;
280 content_height = self.content_height(spec, state.page_index)?;
281 if height > content_height {
282 return Err(table_error("measured table row does not fit on a page"));
283 }
284 }
285 state.push(spec, row, height)
286 })?;
287 if !state.rows.is_empty() {
288 state.flush(spec, sink, self.max_pages, true)?;
289 }
290 Ok(())
291 }
292
293 fn content_height(&self, spec: &TableSpec, page_index: usize) -> Result<crate::Unit> {
294 if page_index == 0 || spec.repeat_header {
295 self.available_height.checked_sub(self.header_height)
296 } else {
297 Ok(self.available_height)
298 }
299 }
300}
301
302#[derive(Default)]
303struct PaginationState {
304 page_index: usize,
305 rows: Vec<DataRow>,
306 row_heights: Vec<crate::Unit>,
307 row_styles: Vec<Style>,
308 group_starts: Vec<Option<String>>,
309 starting_group: Option<String>,
310 last_group: Option<String>,
311 used_height: crate::Unit,
312 totals: BTreeMap<String, DataValue>,
313}
314
315impl PaginationState {
316 fn push(&mut self, spec: &TableSpec, row: &DataRow, height: crate::Unit) -> Result<()> {
317 let group = spec
318 .group_by
319 .as_ref()
320 .and_then(|field| row.get(field))
321 .map(DataValue::display);
322 if self.rows.is_empty() {
323 self.starting_group.clone_from(&group);
324 }
325 self.group_starts
326 .push((group != self.last_group).then(|| group.clone()).flatten());
327 self.last_group = group;
328 accumulate_totals(&mut self.totals, &spec.total_fields, row)?;
329 self.row_styles.push(spec.style_for(row)?);
330 self.row_heights.push(height);
331 self.rows.push(row.clone());
332 self.used_height = self.used_height.checked_add(height)?;
333 Ok(())
334 }
335
336 fn flush(
337 &mut self,
338 spec: &TableSpec,
339 sink: &mut dyn TablePageSink,
340 max_pages: usize,
341 final_page: bool,
342 ) -> Result<()> {
343 if self.page_index >= max_pages {
344 return Err(FileMakerError::new(
345 ErrorCode::LimitExceeded,
346 "table page limit exceeded",
347 ));
348 }
349 sink.page(TablePage {
350 index: self.page_index,
351 header: self.page_index == 0 || spec.repeat_header,
352 rows: std::mem::take(&mut self.rows),
353 row_heights: std::mem::take(&mut self.row_heights),
354 row_styles: std::mem::take(&mut self.row_styles),
355 group_starts: std::mem::take(&mut self.group_starts),
356 starting_group: self.starting_group.take(),
357 totals: if final_page {
358 std::mem::take(&mut self.totals)
359 } else {
360 BTreeMap::new()
361 },
362 })?;
363 self.page_index += 1;
364 self.used_height = crate::Unit::ZERO;
365 Ok(())
366 }
367}
368
369fn accumulate_totals(
370 totals: &mut BTreeMap<String, DataValue>,
371 fields: &[String],
372 row: &DataRow,
373) -> Result<()> {
374 for field in fields {
375 let Some(value) = row.get(field) else {
376 continue;
377 };
378 if matches!(value, DataValue::Null) {
379 continue;
380 }
381 let next = add_total(totals.get(field), value)?;
382 totals.insert(field.clone(), next);
383 }
384 Ok(())
385}
386
387fn add_total(current: Option<&DataValue>, value: &DataValue) -> Result<DataValue> {
388 match (current, value) {
389 (None, DataValue::Integer(value)) => Ok(DataValue::Integer(*value)),
390 (None, DataValue::Decimal(value)) => Ok(DataValue::Decimal(*value)),
391 (None, DataValue::Currency(value)) => Ok(DataValue::Currency(value.clone())),
392 (Some(DataValue::Integer(left)), DataValue::Integer(right)) => left
393 .checked_add(*right)
394 .map(DataValue::Integer)
395 .ok_or_else(|| table_error("integer table total overflow")),
396 (Some(DataValue::Decimal(left)), DataValue::Decimal(right)) => left
397 .checked_add(*right)
398 .map(DataValue::Decimal)
399 .ok_or_else(|| table_error("decimal table total overflow")),
400 (Some(DataValue::Integer(left)), DataValue::Decimal(right)) => {
401 rust_decimal::Decimal::from(*left)
402 .checked_add(*right)
403 .map(DataValue::Decimal)
404 .ok_or_else(|| table_error("decimal table total overflow"))
405 }
406 (Some(DataValue::Decimal(left)), DataValue::Integer(right)) => left
407 .checked_add(rust_decimal::Decimal::from(*right))
408 .map(DataValue::Decimal)
409 .ok_or_else(|| table_error("decimal table total overflow")),
410 (Some(DataValue::Currency(left)), DataValue::Currency(right))
411 if left.code == right.code =>
412 {
413 left.amount
414 .checked_add(right.amount)
415 .map(|amount| {
416 DataValue::Currency(crate::CurrencyValue {
417 code: left.code.clone(),
418 amount,
419 })
420 })
421 .ok_or_else(|| table_error("currency table total overflow"))
422 }
423 _ => Err(table_error(
424 "table total field contains incompatible or non-numeric values",
425 )),
426 }
427}
428
429impl TableSpec {
430 pub fn validate(&self) -> Result<()> {
432 let fields: std::collections::BTreeSet<_> =
433 self.columns.iter().map(|column| &column.field).collect();
434 let total_fields: std::collections::BTreeSet<_> = self.total_fields.iter().collect();
435 if self.columns.is_empty()
436 || self.columns.len() > 1_024
437 || self.auto_sample_rows == 0
438 || self.max_rows == 0
439 || self.style_expression_steps == 0
440 || self.max_row_fields == 0
441 || self.max_cell_bytes == 0
442 || self.columns.len() > self.max_row_fields
443 || self.columns.iter().any(|column| column.field.is_empty())
444 || fields.len() != self.columns.len()
445 || self
446 .group_by
447 .as_ref()
448 .is_some_and(|field| !fields.contains(field))
449 || self
450 .total_fields
451 .iter()
452 .any(|field| !fields.contains(field))
453 || total_fields.len() != self.total_fields.len()
454 || self.conditional_styles.len() > 1_024
455 || self
456 .conditional_styles
457 .iter()
458 .any(|rule| rule.when.is_empty())
459 || self
460 .columns
461 .iter()
462 .any(|column| matches!(column.width, ColumnWidth::Flex(0)))
463 {
464 return Err(table_error("table specification is invalid"));
465 }
466 for rule in &self.conditional_styles {
467 Expression::parse(&rule.when)?;
468 rule.style.validate()?;
469 }
470 Ok(())
471 }
472
473 pub fn style_for(&self, row: &DataRow) -> Result<Style> {
475 let root = DataValue::Object(row.clone());
476 let mut computed = Style::default();
477 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
478 for rule in &self.conditional_styles {
479 if Expression::parse(&rule.when)?
480 .evaluate(&root, &mut budget)?
481 .is_truthy()
482 {
483 rule.style.validate()?;
484 computed.overlay(&rule.style);
485 }
486 }
487 Ok(computed)
488 }
489
490 pub fn visit_bounded(
492 &self,
493 dataset: &dyn Dataset,
494 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<()>,
495 ) -> Result<()> {
496 self.visit_bounded_until(dataset, &mut |index, row| {
497 visitor(index, row)?;
498 Ok(true)
499 })
500 }
501
502 pub fn visit_bounded_until(
504 &self,
505 dataset: &dyn Dataset,
506 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
507 ) -> Result<()> {
508 self.validate()?;
509 dataset.visit_rows_until(&mut |index, row| {
510 if index >= self.max_rows {
511 return Err(FileMakerError::new(
512 ErrorCode::LimitExceeded,
513 "dataset row limit exceeded",
514 ));
515 }
516 if row.len() > self.max_row_fields
517 || row.iter().any(|(field, value)| {
518 field.len() > self.max_cell_bytes
519 || value.display().len() > self.max_cell_bytes
520 || matches!(value, DataValue::Array(_) | DataValue::Object(_))
521 })
522 {
523 return Err(FileMakerError::new(
524 ErrorCode::LimitExceeded,
525 "dataset row exceeds its field, cell, or scalar-value limit",
526 ));
527 }
528 visitor(index, row)
529 })
530 }
531}
532
533fn table_error(message: impl Into<String>) -> FileMakerError {
534 FileMakerError::new(ErrorCode::DataType, message)
535}