Skip to main content

appcore_filemaker/
source_table.rs

1// =============================================================================
2//        #######
3//     ###       ###     F: source_table.rs
4//    ##   ## ##   ##    P: AppCore-Runtime
5//         ## ##
6//                       C: 2026/08/30 05:00:00 by dnettoRaw
7//    ##   ## ##   ##    U: 2026/08/30 05:00:00 by dnettoRaw
8//      ###########      S: 1.0.2-rc
9// =============================================================================
10
11//! Defines bounded source table contracts and behavior for this crate.
12
13use serde::{Deserialize, Serialize};
14
15use crate::source::StyleSource;
16use crate::source_style::convert_style;
17use crate::{
18    ErrorCode, FileMakerError, Length, ResourceLimits, Result, TableColumn, TableIr, TableSpec,
19    TableStyleRule,
20};
21
22/// Conditional table-row style in frontend color/length syntax.
23#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
24#[serde(deny_unknown_fields)]
25pub struct TableStyleRuleSource {
26    /// Deterministic expression evaluated against one row.
27    pub when: String,
28    /// Partial data-rule style.
29    #[serde(default)]
30    pub style: StyleSource,
31}
32
33/// Declarative first-class table options.
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35#[serde(deny_unknown_fields)]
36pub struct TableSource {
37    /// Columns in stable visual order.
38    pub columns: Vec<TableColumn>,
39    /// Repeat the header on continuation pages.
40    #[serde(default = "default_true")]
41    pub repeat_header: bool,
42    /// Optional grouping field.
43    #[serde(default)]
44    pub group_by: Option<String>,
45    /// Exact numeric total fields.
46    #[serde(default)]
47    pub total_fields: Vec<String>,
48    /// Ordered conditional row styles.
49    #[serde(default)]
50    pub conditional_styles: Vec<TableStyleRuleSource>,
51    /// Maximum rows sampled for auto columns.
52    #[serde(default = "default_auto_samples")]
53    pub auto_sample_rows: usize,
54    /// Optional stricter row limit.
55    #[serde(default)]
56    pub max_rows: Option<u64>,
57    /// Optional stricter field-count limit per row.
58    #[serde(default)]
59    pub max_row_fields: Option<usize>,
60    /// Optional stricter displayed byte limit per cell.
61    #[serde(default)]
62    pub max_cell_bytes: Option<usize>,
63    /// Header height.
64    #[serde(default = "default_header_height")]
65    pub header_height: Length,
66    /// Fixed row height or `auto` for measured rows.
67    #[serde(default)]
68    pub row_height: Option<Length>,
69}
70
71pub(crate) fn convert_table(source: &TableSource, limits: &ResourceLimits) -> Result<TableIr> {
72    if matches!(source.header_height, Length::Auto) {
73        return Err(table_source_error("table header height cannot be auto"));
74    }
75    reject_raised_limit(source.max_rows, limits.max_rows, "table row")?;
76    reject_raised_limit(
77        source.max_row_fields,
78        limits.max_elements,
79        "table row field",
80    )?;
81    reject_raised_limit(
82        source.max_cell_bytes,
83        limits.max_text_bytes,
84        "table cell byte",
85    )?;
86    let spec = TableSpec {
87        columns: source.columns.clone(),
88        repeat_header: source.repeat_header,
89        group_by: source.group_by.clone(),
90        total_fields: source.total_fields.clone(),
91        conditional_styles: source
92            .conditional_styles
93            .iter()
94            .map(|rule| {
95                Ok(TableStyleRule {
96                    when: rule.when.clone(),
97                    style: convert_style(&rule.style)?,
98                })
99            })
100            .collect::<Result<Vec<_>>>()?,
101        style_expression_steps: limits.max_expression_steps,
102        auto_sample_rows: source.auto_sample_rows,
103        max_rows: source.max_rows.unwrap_or(limits.max_rows),
104        max_row_fields: source.max_row_fields.unwrap_or(limits.max_elements),
105        max_cell_bytes: source.max_cell_bytes.unwrap_or(limits.max_text_bytes),
106    };
107    spec.validate()?;
108    Ok(TableIr {
109        spec,
110        header_height: source.header_height,
111        row_height: source.row_height,
112        rows: Vec::new(),
113    })
114}
115
116const fn default_true() -> bool {
117    true
118}
119
120const fn default_auto_samples() -> usize {
121    16
122}
123
124fn default_header_height() -> Length {
125    Length::Absolute(crate::Unit::from_raw(18_000_000))
126}
127
128fn table_source_error(message: impl Into<String>) -> FileMakerError {
129    FileMakerError::new(ErrorCode::SchemaField, message)
130}
131
132fn reject_raised_limit<T>(requested: Option<T>, global: T, label: &str) -> Result<()>
133where
134    T: Copy + PartialOrd,
135{
136    if requested.is_some_and(|value| value > global) {
137        return Err(FileMakerError::new(
138            ErrorCode::LimitExceeded,
139            format!("{label} limit cannot exceed the compiler limit"),
140        ));
141    }
142    Ok(())
143}