1use std::collections::BTreeMap;
14
15use serde::{Deserialize, Serialize};
16
17use crate::{
18 DataValue, ErrorCode, Expression, ExpressionBudget, FileMakerError, Length, Result, Style,
19};
20#[path = "table_dataset.rs"]
21mod dataset;
22pub use dataset::{BorrowedDataset, DataRow, Dataset, InMemoryDataset, StreamingDataset};
23#[path = "table_spec.rs"]
24mod spec;
25
26#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
28#[serde(
29 tag = "mode",
30 content = "value",
31 rename_all = "snake_case",
32 deny_unknown_fields
33)]
34pub enum ColumnWidth {
35 Fixed(Length),
37 Flex(u32),
39 Auto,
41}
42
43#[derive(Clone, Copy, Debug, Eq, PartialEq, Serialize, Deserialize)]
45#[serde(default, deny_unknown_fields)]
46pub struct CellPadding {
47 pub top: Length,
49 pub right: Length,
51 pub bottom: Length,
53 pub left: Length,
55}
56
57impl Default for CellPadding {
58 fn default() -> Self {
59 Self {
60 top: Length::Absolute(crate::Unit::ZERO),
61 right: Length::Absolute(crate::Unit::ZERO),
62 bottom: Length::Absolute(crate::Unit::ZERO),
63 left: Length::Absolute(crate::Unit::ZERO),
64 }
65 }
66}
67
68impl CellPadding {
69 fn is_valid(self) -> bool {
70 [self.top, self.right, self.bottom, self.left]
71 .into_iter()
72 .all(|length| match length {
73 Length::Absolute(value) => value >= crate::Unit::ZERO,
74 Length::Logical(value) => value >= 0,
75 Length::Percent(_) | Length::Auto => false,
76 })
77 }
78
79 pub fn resolve(self, logical_unit: crate::Unit) -> Result<crate::Insets> {
81 let resolve = |length| match length {
82 Length::Absolute(value) if value >= crate::Unit::ZERO => Ok(value),
83 Length::Logical(value) if value >= 0 => logical_unit.checked_scale(value),
84 _ => Err(table_error(
85 "cell padding must use nonnegative absolute or logical lengths",
86 )),
87 };
88 Ok(crate::Insets {
89 top: resolve(self.top)?,
90 right: resolve(self.right)?,
91 bottom: resolve(self.bottom)?,
92 left: resolve(self.left)?,
93 })
94 }
95}
96
97#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
99#[serde(deny_unknown_fields)]
100pub struct TableColumn {
101 pub field: String,
103 pub header: String,
105 pub width: ColumnWidth,
107 #[serde(default)]
109 pub align_x: crate::Alignment,
110 #[serde(default)]
112 pub padding: CellPadding,
113}
114
115#[derive(Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
117#[serde(deny_unknown_fields)]
118pub struct TableStyleRule {
119 pub when: String,
121 pub style: Style,
123 #[serde(default)]
125 pub padding: Option<CellPadding>,
126 #[serde(default)]
128 pub padding_first_page: Option<CellPadding>,
129 #[serde(default)]
131 pub padding_continuation: Option<CellPadding>,
132 #[serde(default)]
134 pub text_offset_y: Option<Length>,
135 #[serde(default)]
137 pub text_offset_y_first_page: Option<Length>,
138 #[serde(default)]
140 pub text_offset_y_continuation: Option<Length>,
141 #[serde(default)]
143 pub min_height: Option<Length>,
144 #[serde(default)]
146 pub min_height_first_page: Option<Length>,
147 #[serde(default)]
149 pub min_height_continuation: Option<Length>,
150 #[serde(default)]
152 pub reserve_after: Option<Length>,
153}
154
155#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
157pub struct TableSpec {
158 pub columns: Vec<TableColumn>,
160 pub repeat_header: bool,
162 pub group_by: Option<String>,
164 #[serde(default)]
167 pub keep_together_by: Option<String>,
168 #[serde(default)]
170 pub row_anchor_field: Option<String>,
171 pub total_fields: Vec<String>,
173 pub conditional_styles: Vec<TableStyleRule>,
175 pub style_expression_steps: usize,
177 pub auto_sample_rows: usize,
179 pub max_rows: u64,
181 pub max_row_fields: usize,
183 pub max_cell_bytes: usize,
185}
186
187#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
189pub struct TablePage {
190 pub index: usize,
192 pub header: bool,
194 pub rows: Vec<DataRow>,
196 pub row_heights: Vec<crate::Unit>,
198 pub row_styles: Vec<Style>,
200 #[serde(default)]
202 pub row_padding: Vec<CellPadding>,
203 pub group_starts: Vec<Option<String>>,
205 pub starting_group: Option<String>,
207 pub totals: BTreeMap<String, DataValue>,
209}
210
211pub trait TablePageSink {
213 fn page(&mut self, page: TablePage) -> Result<()>;
215}
216
217pub struct TablePaginator {
219 pub available_height: crate::Unit,
221 pub header_height: crate::Unit,
223 pub row_height: crate::Unit,
225 pub max_pages: usize,
227}
228
229impl TablePaginator {
230 pub fn paginate(
232 &self,
233 spec: &TableSpec,
234 dataset: &dyn Dataset,
235 sink: &mut dyn TablePageSink,
236 ) -> Result<()> {
237 if self.row_height <= crate::Unit::ZERO {
238 return Err(table_error("fixed table row height must be positive"));
239 }
240 self.paginate_measured(spec, dataset, &mut |_| Ok(self.row_height), sink)
241 }
242
243 pub fn paginate_measured(
245 &self,
246 spec: &TableSpec,
247 dataset: &dyn Dataset,
248 measure: &mut dyn FnMut(&DataRow) -> Result<crate::Unit>,
249 sink: &mut dyn TablePageSink,
250 ) -> Result<()> {
251 self.paginate_measured_with_first_page_height(
252 spec,
253 dataset,
254 measure,
255 sink,
256 self.available_height,
257 )
258 }
259
260 pub(crate) fn paginate_measured_with_first_page_height(
261 &self,
262 spec: &TableSpec,
263 dataset: &dyn Dataset,
264 measure: &mut dyn FnMut(&DataRow) -> Result<crate::Unit>,
265 sink: &mut dyn TablePageSink,
266 first_page_height: crate::Unit,
267 ) -> Result<()> {
268 let mut contextual_measure = |_: usize, row: &DataRow| measure(row);
269 self.paginate_measured_with_page_index(
270 spec,
271 dataset,
272 &mut contextual_measure,
273 sink,
274 first_page_height,
275 false,
276 )
277 }
278
279 pub(crate) fn paginate_measured_with_page_index(
280 &self,
281 spec: &TableSpec,
282 dataset: &dyn Dataset,
283 measure: &mut dyn FnMut(usize, &DataRow) -> Result<crate::Unit>,
284 sink: &mut dyn TablePageSink,
285 first_page_height: crate::Unit,
286 page_sensitive: bool,
287 ) -> Result<()> {
288 if self.available_height <= crate::Unit::ZERO
289 || first_page_height <= crate::Unit::ZERO
290 || self.header_height < crate::Unit::ZERO
291 || self.max_pages == 0
292 {
293 return Err(table_error("table pagination dimensions are invalid"));
294 }
295 crate::table_group_pagination::paginate_measured(
296 self,
297 spec,
298 dataset,
299 measure,
300 sink,
301 first_page_height,
302 page_sensitive,
303 )
304 }
305
306 pub(crate) fn content_height(
307 &self,
308 spec: &TableSpec,
309 page_index: usize,
310 first_page_height: crate::Unit,
311 ) -> Result<crate::Unit> {
312 let available_height = if page_index == 0 {
313 first_page_height
314 } else {
315 self.available_height
316 };
317 if page_index == 0 || spec.repeat_header {
318 available_height.checked_sub(self.header_height)
319 } else {
320 Ok(available_height)
321 }
322 }
323}
324
325#[derive(Default)]
326pub(crate) struct PaginationState {
327 page_index: usize,
328 rows: Vec<DataRow>,
329 row_heights: Vec<crate::Unit>,
330 row_styles: Vec<Style>,
331 row_padding: Vec<CellPadding>,
332 group_starts: Vec<Option<String>>,
333 starting_group: Option<String>,
334 last_group: Option<String>,
335 used_height: crate::Unit,
336 totals: BTreeMap<String, DataValue>,
337}
338
339impl PaginationState {
340 pub(crate) fn push(
341 &mut self,
342 spec: &TableSpec,
343 row: &DataRow,
344 height: crate::Unit,
345 reserve_after: crate::Unit,
346 ) -> Result<()> {
347 let group = spec
348 .group_by
349 .as_ref()
350 .and_then(|field| row.get(field))
351 .map(DataValue::display);
352 if self.rows.is_empty() {
353 self.starting_group.clone_from(&group);
354 }
355 self.group_starts
356 .push((group != self.last_group).then(|| group.clone()).flatten());
357 self.last_group = group;
358 accumulate_totals(&mut self.totals, &spec.total_fields, row)?;
359 self.row_styles.push(spec.style_for(row)?);
360 self.row_padding
361 .push(spec.padding_for(row, self.page_index)?);
362 self.row_heights.push(height);
363 self.rows.push(row.clone());
364 self.used_height = self
365 .used_height
366 .checked_add(height)?
367 .checked_add(reserve_after)?;
368 Ok(())
369 }
370
371 pub(crate) fn flush(
372 &mut self,
373 spec: &TableSpec,
374 sink: &mut dyn TablePageSink,
375 max_pages: usize,
376 final_page: bool,
377 ) -> Result<()> {
378 if self.page_index >= max_pages {
379 return Err(FileMakerError::new(
380 ErrorCode::LimitExceeded,
381 "table page limit exceeded",
382 ));
383 }
384 sink.page(TablePage {
385 index: self.page_index,
386 header: self.page_index == 0 || spec.repeat_header,
387 rows: std::mem::take(&mut self.rows),
388 row_heights: std::mem::take(&mut self.row_heights),
389 row_styles: std::mem::take(&mut self.row_styles),
390 row_padding: std::mem::take(&mut self.row_padding),
391 group_starts: std::mem::take(&mut self.group_starts),
392 starting_group: self.starting_group.take(),
393 totals: if final_page {
394 std::mem::take(&mut self.totals)
395 } else {
396 BTreeMap::new()
397 },
398 })?;
399 self.page_index += 1;
400 self.used_height = crate::Unit::ZERO;
401 Ok(())
402 }
403
404 pub(crate) fn is_empty(&self) -> bool {
405 self.rows.is_empty()
406 }
407
408 pub(crate) fn can_fit(&self, height: crate::Unit, capacity: crate::Unit) -> Result<bool> {
409 Ok(self.used_height.checked_add(height)? <= capacity)
410 }
411
412 pub(crate) fn page_index(&self) -> usize {
413 self.page_index
414 }
415
416 pub(crate) fn skip_empty_page(&mut self) -> Result<()> {
417 if !self.rows.is_empty() {
418 return Err(table_error("cannot defer a non-empty table page"));
419 }
420 self.page_index = self
421 .page_index
422 .checked_add(1)
423 .ok_or_else(|| table_error("table page index overflow"))?;
424 Ok(())
425 }
426}
427
428fn accumulate_totals(
429 totals: &mut BTreeMap<String, DataValue>,
430 fields: &[String],
431 row: &DataRow,
432) -> Result<()> {
433 for field in fields {
434 let Some(value) = row.get(field) else {
435 continue;
436 };
437 if matches!(value, DataValue::Null) {
438 continue;
439 }
440 let next = add_total(totals.get(field), value)?;
441 totals.insert(field.clone(), next);
442 }
443 Ok(())
444}
445
446fn add_total(current: Option<&DataValue>, value: &DataValue) -> Result<DataValue> {
447 match (current, value) {
448 (None, DataValue::Integer(value)) => Ok(DataValue::Integer(*value)),
449 (None, DataValue::Decimal(value)) => Ok(DataValue::Decimal(*value)),
450 (None, DataValue::Currency(value)) => Ok(DataValue::Currency(value.clone())),
451 (Some(DataValue::Integer(left)), DataValue::Integer(right)) => left
452 .checked_add(*right)
453 .map(DataValue::Integer)
454 .ok_or_else(|| table_error("integer table total overflow")),
455 (Some(DataValue::Decimal(left)), DataValue::Decimal(right)) => left
456 .checked_add(*right)
457 .map(DataValue::Decimal)
458 .ok_or_else(|| table_error("decimal table total overflow")),
459 (Some(DataValue::Integer(left)), DataValue::Decimal(right)) => {
460 rust_decimal::Decimal::from(*left)
461 .checked_add(*right)
462 .map(DataValue::Decimal)
463 .ok_or_else(|| table_error("decimal table total overflow"))
464 }
465 (Some(DataValue::Decimal(left)), DataValue::Integer(right)) => left
466 .checked_add(rust_decimal::Decimal::from(*right))
467 .map(DataValue::Decimal)
468 .ok_or_else(|| table_error("decimal table total overflow")),
469 (Some(DataValue::Currency(left)), DataValue::Currency(right))
470 if left.code == right.code =>
471 {
472 left.amount
473 .checked_add(right.amount)
474 .map(|amount| {
475 DataValue::Currency(crate::CurrencyValue {
476 code: left.code.clone(),
477 amount,
478 })
479 })
480 .ok_or_else(|| table_error("currency table total overflow"))
481 }
482 _ => Err(table_error(
483 "table total field contains incompatible or non-numeric values",
484 )),
485 }
486}
487
488pub(super) fn table_error(message: impl Into<String>) -> FileMakerError {
489 FileMakerError::new(ErrorCode::DataType, message)
490}