appcore_filemaker/
table_spec.rs1use super::*;
14
15impl TableSpec {
16 pub fn validate(&self) -> Result<()> {
18 let fields: std::collections::BTreeSet<_> =
19 self.columns.iter().map(|column| &column.field).collect();
20 let total_fields: std::collections::BTreeSet<_> = self.total_fields.iter().collect();
21 if self.columns.is_empty()
22 || self.columns.len() > 1_024
23 || self.auto_sample_rows == 0
24 || self.max_rows == 0
25 || self.style_expression_steps == 0
26 || self.max_row_fields == 0
27 || self.max_cell_bytes == 0
28 || self.columns.len() > self.max_row_fields
29 || self.columns.iter().any(|column| column.field.is_empty())
30 || self.columns.iter().any(|column| !column.padding.is_valid())
31 || fields.len() != self.columns.len()
32 || self
33 .group_by
34 .as_ref()
35 .is_some_and(|field| !fields.contains(field))
36 || self
37 .keep_together_by
38 .as_ref()
39 .is_some_and(|field| field.is_empty())
40 || self
41 .row_anchor_field
42 .as_ref()
43 .is_some_and(|field| field.is_empty())
44 || (self.row_anchor_field.is_none()
45 && self
46 .conditional_styles
47 .iter()
48 .any(|rule| rule.reserve_after.is_some()))
49 || self
50 .total_fields
51 .iter()
52 .any(|field| !fields.contains(field))
53 || total_fields.len() != self.total_fields.len()
54 || self.conditional_styles.len() > 1_024
55 || self.conditional_styles.iter().any(|rule| {
56 rule.when.is_empty()
57 || [
58 rule.padding,
59 rule.padding_first_page,
60 rule.padding_continuation,
61 ]
62 .into_iter()
63 .flatten()
64 .any(|padding| !padding.is_valid())
65 || rule
66 .text_offset_y
67 .into_iter()
68 .chain(rule.text_offset_y_first_page)
69 .chain(rule.text_offset_y_continuation)
70 .any(|offset| matches!(offset, Length::Percent(_) | Length::Auto))
71 || [
72 rule.min_height,
73 rule.min_height_first_page,
74 rule.min_height_continuation,
75 ]
76 .into_iter()
77 .flatten()
78 .any(|height| match height {
79 Length::Absolute(value) => value <= crate::Unit::ZERO,
80 Length::Logical(value) => value <= 0,
81 Length::Percent(value) => value <= 0,
82 Length::Auto => true,
83 })
84 || rule.reserve_after.is_some_and(|reserve| {
85 !matches!(reserve, Length::Absolute(value) if value > crate::Unit::ZERO)
86 })
87 || rule.padding.is_some_and(|padding| !padding.is_valid())
88 })
89 || self
90 .columns
91 .iter()
92 .any(|column| matches!(column.width, ColumnWidth::Flex(0)))
93 {
94 return Err(table_error("table specification is invalid"));
95 }
96 for rule in &self.conditional_styles {
97 Expression::parse(&rule.when)?;
98 rule.style.validate()?;
99 }
100 Ok(())
101 }
102
103 pub fn style_for(&self, row: &DataRow) -> Result<Style> {
105 let root = DataValue::Object(row.clone());
106 let mut computed = Style::default();
107 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
108 for rule in &self.conditional_styles {
109 if Expression::parse(&rule.when)?
110 .evaluate(&root, &mut budget)?
111 .is_truthy()
112 {
113 rule.style.validate()?;
114 computed.overlay(&rule.style);
115 }
116 }
117 Ok(computed)
118 }
119
120 pub(crate) fn padding_for(&self, row: &DataRow, page_index: usize) -> Result<CellPadding> {
122 let root = DataValue::Object(row.clone());
123 let mut padding = CellPadding::default();
124 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
125 for rule in &self.conditional_styles {
126 let page_padding = if page_index == 0 {
127 rule.padding_first_page.or(rule.padding)
128 } else {
129 rule.padding_continuation.or(rule.padding)
130 };
131 if page_padding.is_some()
132 && Expression::parse(&rule.when)?
133 .evaluate(&root, &mut budget)?
134 .is_truthy()
135 {
136 if let Some(next) = page_padding {
137 padding = next;
138 }
139 }
140 }
141 Ok(padding)
142 }
143
144 pub(crate) fn text_offset_y_for(
146 &self,
147 row: &DataRow,
148 logical_unit: crate::Unit,
149 page_index: usize,
150 ) -> Result<crate::Unit> {
151 let root = DataValue::Object(row.clone());
152 let mut offset = crate::Unit::ZERO;
153 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
154 for rule in &self.conditional_styles {
155 let page_offset = if page_index == 0 {
156 rule.text_offset_y_first_page
157 } else {
158 rule.text_offset_y_continuation
159 };
160 if let Some(value) = page_offset.or(rule.text_offset_y) {
161 if Expression::parse(&rule.when)?
162 .evaluate(&root, &mut budget)?
163 .is_truthy()
164 {
165 offset = match value {
166 Length::Absolute(value) => value,
167 Length::Logical(value) => logical_unit.checked_scale(value)?,
168 Length::Percent(_) | Length::Auto => {
169 return Err(table_error(
170 "table text offset must be an absolute or logical length",
171 ));
172 }
173 };
174 }
175 }
176 }
177 Ok(offset)
178 }
179
180 pub(crate) fn minimum_row_height_for(
182 &self,
183 row: &DataRow,
184 page_index: usize,
185 reference: crate::Unit,
186 logical_unit: crate::Unit,
187 ) -> Result<Option<crate::Unit>> {
188 let root = DataValue::Object(row.clone());
189 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
190 let mut minimum = None;
191 for rule in &self.conditional_styles {
192 let page_minimum = if page_index == 0 {
193 rule.min_height_first_page
194 } else {
195 rule.min_height_continuation
196 };
197 if (rule.min_height.is_some() || page_minimum.is_some())
198 && Expression::parse(&rule.when)?
199 .evaluate(&root, &mut budget)?
200 .is_truthy()
201 {
202 for height in [rule.min_height, page_minimum].into_iter().flatten() {
203 let Some(candidate) = height.resolve(reference, logical_unit)? else {
204 return Err(table_error("conditional minimum row height cannot be auto"));
205 };
206 minimum = Some(
207 minimum.map_or(candidate, |current: crate::Unit| current.max(candidate)),
208 );
209 }
210 }
211 }
212 Ok(minimum)
213 }
214
215 pub(crate) fn reserve_after_for(&self, row: &DataRow) -> Result<crate::Unit> {
217 let Some(field) = self.row_anchor_field.as_deref() else {
218 return Ok(crate::Unit::ZERO);
219 };
220 if !matches!(row.get(field), Some(DataValue::String(name)) if !name.is_empty()) {
221 return Ok(crate::Unit::ZERO);
222 }
223 let root = DataValue::Object(row.clone());
224 let mut budget = ExpressionBudget::new(self.style_expression_steps)?;
225 self.conditional_styles
226 .iter()
227 .filter_map(|rule| rule.reserve_after.map(|reserve| (&rule.when, reserve)))
228 .try_fold(crate::Unit::ZERO, |largest, (when, reserve)| {
229 if Expression::parse(when)?
230 .evaluate(&root, &mut budget)?
231 .is_truthy()
232 {
233 let Length::Absolute(candidate) = reserve else {
234 return Err(table_error(
235 "row anchor reserve must be an absolute positive length",
236 ));
237 };
238 Ok(largest.max(candidate))
239 } else {
240 Ok(largest)
241 }
242 })
243 }
244
245 pub fn visit_bounded(
247 &self,
248 dataset: &dyn Dataset,
249 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<()>,
250 ) -> Result<()> {
251 self.visit_bounded_until(dataset, &mut |index, row| {
252 visitor(index, row)?;
253 Ok(true)
254 })
255 }
256
257 pub fn visit_bounded_until(
259 &self,
260 dataset: &dyn Dataset,
261 visitor: &mut dyn FnMut(u64, &DataRow) -> Result<bool>,
262 ) -> Result<()> {
263 self.validate()?;
264 let mut row_anchors = std::collections::BTreeSet::new();
265 dataset.visit_rows_until(&mut |index, row| {
266 if index >= self.max_rows {
267 return Err(FileMakerError::new(
268 ErrorCode::LimitExceeded,
269 "dataset row limit exceeded",
270 ));
271 }
272 if row.len() > self.max_row_fields
273 || row.iter().any(|(field, value)| {
274 field.len() > self.max_cell_bytes
275 || value.display().len() > self.max_cell_bytes
276 || matches!(value, DataValue::Array(_) | DataValue::Object(_))
277 })
278 {
279 return Err(FileMakerError::new(
280 ErrorCode::LimitExceeded,
281 "dataset row exceeds its field, cell, or scalar-value limit",
282 ));
283 }
284 if let Some(field) = &self.row_anchor_field {
285 if let Some(value) = row.get(field) {
286 match value {
287 DataValue::Null => {}
288 DataValue::String(name)
289 if !name.is_empty()
290 && name.len() <= self.max_cell_bytes
291 && !name.contains('.')
292 && !name.contains('+')
293 && row_anchors.insert(name.clone()) => {}
294 _ => {
295 return Err(table_error(
296 "row anchor names must be unique bounded strings without anchor delimiters",
297 ));
298 }
299 }
300 }
301 }
302 visitor(index, row)
303 })
304 }
305}