1use std::collections::{BTreeMap, BTreeSet};
14use std::sync::Arc;
15
16use crate::compiler_style::apply_named_styles;
17use crate::source::{ComponentSource, ElementSource, TemplateSourceV1};
18use crate::{
19 DataValue, DocumentIr, ErrorCode, FileMakerError, OperationControl, Patch, PatchTransaction,
20 PresetRegistry, ProgressPhase, ResourceLimits, Result, TemplateIr, TemplateResolver,
21};
22
23#[derive(Default)]
25pub struct CompilerBuilder {
26 limits: ResourceLimits,
27 presets: PresetRegistry,
28 template_resolver: Option<Arc<dyn TemplateResolver>>,
29 control: OperationControl,
30}
31
32impl CompilerBuilder {
33 #[must_use]
35 pub fn limits(mut self, limits: ResourceLimits) -> Self {
36 self.limits = limits;
37 self
38 }
39
40 #[must_use]
42 pub fn presets(mut self, presets: PresetRegistry) -> Self {
43 self.presets = presets;
44 self
45 }
46
47 #[must_use]
49 pub fn template_resolver(mut self, resolver: Arc<dyn TemplateResolver>) -> Self {
50 self.template_resolver = Some(resolver);
51 self
52 }
53
54 #[must_use]
56 pub fn control(mut self, control: OperationControl) -> Self {
57 self.control = control;
58 self
59 }
60
61 pub fn build(self) -> Result<Compiler> {
63 self.limits.validate()?;
64 Ok(Compiler {
65 limits: self.limits,
66 presets: self.presets,
67 template_resolver: self.template_resolver,
68 control: self.control,
69 })
70 }
71}
72
73pub struct Compiler {
75 limits: ResourceLimits,
76 presets: PresetRegistry,
77 template_resolver: Option<Arc<dyn TemplateResolver>>,
78 control: OperationControl,
79}
80
81impl Compiler {
82 #[must_use]
84 pub fn builder() -> CompilerBuilder {
85 CompilerBuilder::default()
86 }
87
88 pub fn compile_template_yaml(&self, bytes: &[u8]) -> Result<TemplateIr> {
90 self.control
91 .checkpoint(ProgressPhase::Compile, 0, Some(4))?;
92 let mut source = TemplateSourceV1::parse_yaml(bytes, &self.limits)?;
93 self.control
94 .checkpoint(ProgressPhase::Compile, 1, Some(4))?;
95 let mut include_stack = BTreeSet::new();
96 let mut include_bytes = 0_usize;
97 self.expand_includes(&mut source, &mut include_stack, &mut include_bytes, 0)?;
98 self.control
99 .checkpoint(ProgressPhase::Compile, 2, Some(4))?;
100 expand_components(&mut source, &self.limits)?;
101 apply_named_styles(&mut source)?;
102 self.control
103 .checkpoint(ProgressPhase::Compile, 3, Some(4))?;
104 let ir = source.to_ir(&self.presets, &self.limits)?;
105 crate::validate_template(&ir, &self.limits).enforce(false)?;
106 self.control
107 .checkpoint(ProgressPhase::Compile, 4, Some(4))?;
108 Ok(ir)
109 }
110
111 pub fn bind(
113 &self,
114 template: &TemplateIr,
115 data: &DataValue,
116 patches: &[Patch],
117 ) -> Result<DocumentIr> {
118 self.control.checkpoint(ProgressPhase::Bind, 0, Some(3))?;
119 data.validate(64, self.limits.max_elements, self.limits.max_text_bytes)?;
120 let resolved_data = crate::data::resolve_computed_fields(
121 &template.data_schema,
122 data,
123 self.limits.max_expression_steps,
124 )?;
125 self.control.checkpoint(ProgressPhase::Bind, 1, Some(3))?;
126 let mut document = DocumentIr {
127 template_id: template.id.clone(),
128 model: template.model,
129 page_size: template.page_size,
130 page_template: template.page_template.clone(),
131 collision: template.collision.clone(),
132 page_collision: template.page_collision.clone(),
133 guides: template.guides.clone(),
134 regions: template.regions.clone(),
135 exclusions: template.exclusions.clone(),
136 ai_policy: template.ai_policy.clone(),
137 elements: crate::compiler_bind::bind_elements(
138 &template.elements,
139 &resolved_data,
140 &self.limits,
141 &self.control,
142 )?,
143 };
144 self.control.checkpoint(ProgressPhase::Bind, 2, Some(3))?;
145 let total_patch_operations = patches.iter().try_fold(0_usize, |total, patch| {
146 total
147 .checked_add(patch.operations.len())
148 .ok_or_else(|| limit_error("runtime patch operation accounting overflow"))
149 })?;
150 if total_patch_operations > self.limits.max_patch_operations {
151 return Err(limit_error(
152 "runtime patches exceed the configured total operation limit",
153 ));
154 }
155 for (index, patch) in patches.iter().enumerate() {
156 self.control.checkpoint(
157 ProgressPhase::Bind,
158 u64::try_from(index).unwrap_or(u64::MAX),
159 u64::try_from(patches.len()).ok(),
160 )?;
161 PatchTransaction::new(&mut document, self.limits.max_patch_operations).apply(patch)?;
162 }
163 self.control.checkpoint(ProgressPhase::Bind, 3, Some(3))?;
164 Ok(document)
165 }
166
167 fn expand_includes(
168 &self,
169 source: &mut TemplateSourceV1,
170 stack: &mut BTreeSet<String>,
171 total_bytes: &mut usize,
172 depth: usize,
173 ) -> Result<()> {
174 self.control.checkpoint(
175 ProgressPhase::Compile,
176 u64::try_from(depth).unwrap_or(u64::MAX),
177 u64::try_from(self.limits.max_include_depth).ok(),
178 )?;
179 if depth > self.limits.max_include_depth {
180 return Err(limit_error("include depth exceeds configured limit"));
181 }
182 let includes = std::mem::take(&mut source.includes);
183 for include in includes {
184 if !stack.insert(include.path.clone()) {
185 return Err(
186 FileMakerError::new(ErrorCode::DataCycle, "include cycle detected")
187 .at(include.path),
188 );
189 }
190 let resolver = self.template_resolver.as_ref().ok_or_else(|| {
191 FileMakerError::new(
192 ErrorCode::AssetInvalid,
193 "template contains includes but no resolver is configured",
194 )
195 })?;
196 let bytes = resolver.resolve_template(&include.path, self.limits.max_include_bytes)?;
197 *total_bytes = total_bytes
198 .checked_add(bytes.len())
199 .ok_or_else(|| limit_error("include byte accounting overflow"))?;
200 if *total_bytes > self.limits.max_include_bytes {
201 return Err(limit_error("total include bytes exceed configured limit"));
202 }
203 let mut child = TemplateSourceV1::parse_yaml(&bytes, &self.limits)?;
204 if child.model != source.model {
205 return Err(schema_error("included template model does not match root"));
206 }
207 self.expand_includes(&mut child, stack, total_bytes, depth + 1)?;
208 merge_include(source, child, include.namespace.as_deref(), &include.path)?;
209 stack.remove(&include.path);
210 }
211 Ok(())
212 }
213}
214
215fn merge_include(
216 root: &mut TemplateSourceV1,
217 mut child: TemplateSourceV1,
218 namespace: Option<&str>,
219 source_path: &str,
220) -> Result<()> {
221 if child
222 .page
223 .as_ref()
224 .is_some_and(|page| page.has_layer_elements())
225 {
226 return Err(schema_error(
227 "included templates cannot declare page layers; the root owns physical pages",
228 ));
229 }
230 let prefix = namespace
231 .map(|value| format!("{value}/"))
232 .unwrap_or_default();
233 for element in &mut child.elements {
234 namespace_element(element, &prefix, source_path);
235 }
236 namespace_components(&mut child.components, namespace);
237 merge_map(&mut root.components, child.components, "component")?;
238 merge_map(&mut root.styles, child.styles, "style")?;
239 merge_map(&mut root.themes, child.themes, "theme")?;
240 merge_map(&mut root.guides, child.guides, "guide")?;
241 merge_map(&mut root.regions, child.regions, "region")?;
242 merge_map(&mut root.exclusions, child.exclusions, "exclusion")?;
243 merge_map(&mut root.data_schema, child.data_schema, "data field")?;
244 root.elements.extend(child.elements);
245 Ok(())
246}
247
248fn merge_map<T>(
249 target: &mut BTreeMap<String, T>,
250 source: BTreeMap<String, T>,
251 label: &str,
252) -> Result<()> {
253 for (name, value) in source {
254 if target.insert(name.clone(), value).is_some() {
255 return Err(schema_error(format!(
256 "duplicate {label} `{name}` after include expansion"
257 )));
258 }
259 }
260 Ok(())
261}
262
263fn namespace_components(
264 components: &mut BTreeMap<String, ComponentSource>,
265 namespace: Option<&str>,
266) {
267 let Some(namespace) = namespace else {
268 return;
269 };
270 let old = std::mem::take(components);
271 for (name, mut component) in old {
272 let prefix = format!("{namespace}/");
273 for element in &mut component.elements {
274 namespace_element(element, &prefix, "component");
275 }
276 components.insert(format!("{namespace}.{name}"), component);
277 }
278}
279
280fn namespace_element(element: &mut ElementSource, prefix: &str, source_path: &str) {
281 element.id = format!("{prefix}{}", element.id);
282 element.provenance_source = Some(source_path.to_owned());
283 for child in &mut element.children {
284 namespace_element(child, prefix, source_path);
285 }
286 for values in element.slots.values_mut() {
287 for child in values {
288 namespace_element(child, prefix, source_path);
289 }
290 }
291}
292
293fn expand_components(source: &mut TemplateSourceV1, limits: &ResourceLimits) -> Result<()> {
294 let components = source.components.clone();
295 let mut count = 0_usize;
296 source.elements = expand_element_list(
297 std::mem::take(&mut source.elements),
298 &components,
299 limits,
300 &mut count,
301 &mut Vec::new(),
302 )?;
303 if let Some(page) = &mut source.page {
304 for elements in page.element_lists_mut() {
305 *elements = expand_element_list(
306 std::mem::take(elements),
307 &components,
308 limits,
309 &mut count,
310 &mut Vec::new(),
311 )?;
312 }
313 }
314 source.components.clear();
315 Ok(())
316}
317
318fn expand_element_list(
319 elements: Vec<ElementSource>,
320 components: &BTreeMap<String, ComponentSource>,
321 limits: &ResourceLimits,
322 count: &mut usize,
323 component_stack: &mut Vec<String>,
324) -> Result<Vec<ElementSource>> {
325 let mut expanded = Vec::new();
326 for mut element in elements {
327 *count = count.saturating_add(1);
328 if *count > limits.max_elements {
329 return Err(limit_error("expanded element limit exceeded"));
330 }
331 if element.element_type == "slot" {
332 return Err(schema_error(
333 "slot placeholder is only valid inside a component",
334 ));
335 }
336 if let Some(name) = element.component.clone() {
337 if component_stack.contains(&name) {
338 return Err(FileMakerError::new(
339 ErrorCode::DataCycle,
340 "component cycle detected",
341 ));
342 }
343 let component = components
344 .get(&name)
345 .ok_or_else(|| schema_error(format!("component `{name}` was not found")))?;
346 component_stack.push(name.clone());
347 let mut props = component.props.clone();
348 props.extend(element.props.clone());
349 let body =
350 fill_component_slots(component.elements.clone(), &component.slots, &element.slots);
351 let mut body = expand_element_list(body, components, limits, count, component_stack)?;
352 for child in &mut body {
353 prefix_component_child(child, &element.id, &name, &props);
354 }
355 component_stack.pop();
356 element.component = None;
357 "group".clone_into(&mut element.element_type);
358 element.children = body;
359 element.slots.clear();
360 } else {
361 element.children =
362 expand_element_list(element.children, components, limits, count, component_stack)?;
363 }
364 expanded.push(element);
365 }
366 Ok(expanded)
367}
368
369fn fill_component_slots(
370 elements: Vec<ElementSource>,
371 defaults: &BTreeMap<String, Vec<ElementSource>>,
372 supplied: &BTreeMap<String, Vec<ElementSource>>,
373) -> Vec<ElementSource> {
374 let mut result = Vec::new();
375 for mut element in elements {
376 if element.element_type == "slot" {
377 let name = element.text.as_deref().unwrap_or("default");
378 result.extend(
379 supplied
380 .get(name)
381 .or_else(|| defaults.get(name))
382 .cloned()
383 .unwrap_or_default(),
384 );
385 } else {
386 element.children = fill_component_slots(element.children, defaults, supplied);
387 result.push(element);
388 }
389 }
390 result
391}
392
393fn prefix_component_child(
394 element: &mut ElementSource,
395 instance_id: &str,
396 component: &str,
397 props: &BTreeMap<String, serde_json::Value>,
398) {
399 element.id = format!("{instance_id}/{}", element.id);
400 element.provenance_components.push(component.to_owned());
401 if let Some(text) = &mut element.text {
402 *text = substitute_props(text, props);
403 }
404 for child in &mut element.children {
405 prefix_component_child(child, instance_id, component, props);
406 }
407}
408
409fn substitute_props(source: &str, props: &BTreeMap<String, serde_json::Value>) -> String {
410 let mut result = source.to_owned();
411 for (name, value) in props {
412 let replacement = value
413 .as_str()
414 .map_or_else(|| value.to_string(), ToOwned::to_owned);
415 result = result.replace(&format!("{{{{{name}}}}}"), &replacement);
416 }
417 result
418}
419
420fn schema_error(message: impl Into<String>) -> FileMakerError {
421 FileMakerError::new(ErrorCode::SchemaField, message)
422}
423
424fn limit_error(message: impl Into<String>) -> FileMakerError {
425 FileMakerError::new(ErrorCode::LimitExceeded, message)
426}