1use std::collections::BTreeMap;
7
8use crate::builder::{text, ElementBuilder, FragmentBuilder, IntoXmlFragment, XmlNode};
9use crate::core::{
10 Attribute, Document, ElementData, ErrorKind, NamespaceDeclaration, NodeId, NodeKind, QName,
11 XmlError, XmlResult,
12};
13use crate::parser;
14use crate::query::{NamespaceContext, Query, QueryValue};
15use crate::security::TransformSecurityConfig;
16
17#[derive(Debug, Clone, Default, PartialEq, Eq)]
18pub struct BindingContext {
19 params: BTreeMap<String, String>,
20 namespaces: NamespaceContext,
21}
22
23impl BindingContext {
24 pub fn new() -> Self {
25 Self::default()
26 }
27
28 pub fn with_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
29 self.params.insert(name.into(), value.into());
30 self
31 }
32
33 pub fn with_namespace(
34 mut self,
35 alias: impl Into<String>,
36 uri: impl Into<String>,
37 ) -> XmlResult<Self> {
38 self.namespaces = self.namespaces.with_alias(alias, uri)?;
39 Ok(self)
40 }
41
42 pub fn param(&self, name: &str) -> XmlResult<&str> {
43 self.params
44 .get(name)
45 .map(String::as_str)
46 .ok_or_else(|| transform_error(format!("missing transform parameter `{name}`")))
47 }
48
49 pub fn namespaces(&self) -> &NamespaceContext {
50 &self.namespaces
51 }
52}
53
54#[derive(Debug, Clone, Default, PartialEq, Eq)]
55pub struct TransformConfig {
56 security: TransformSecurityConfig,
57}
58
59impl TransformConfig {
60 pub fn new() -> Self {
61 Self::default()
62 }
63
64 pub fn with_security(mut self, security: TransformSecurityConfig) -> Self {
65 self.security = security;
66 self
67 }
68
69 pub fn security(&self) -> &TransformSecurityConfig {
70 &self.security
71 }
72}
73
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub struct Transform {
76 template: Template,
77}
78
79impl Transform {
80 pub fn new(template: Template) -> Self {
81 Self { template }
82 }
83
84 pub fn apply(&self, source: &Document, context: &BindingContext) -> XmlResult<FragmentBuilder> {
85 self.apply_with_config(source, context, &TransformConfig::default())
86 }
87
88 pub fn apply_with_config(
89 &self,
90 source: &Document,
91 context: &BindingContext,
92 config: &TransformConfig,
93 ) -> XmlResult<FragmentBuilder> {
94 let mut expansion = TransformExpansionCounter::new(config.security());
95 let fragment = self
96 .template
97 .render_with_counter(source, context, Some(&mut expansion))?;
98 expansion.check_fragment(&fragment)?;
99 Ok(fragment)
100 }
101
102 pub fn apply_document(
103 &self,
104 source: &Document,
105 context: &BindingContext,
106 ) -> XmlResult<Document> {
107 let root = self.apply(source, context)?.into_single_element()?;
108 crate::builder::DocumentBuilder::new().root(root)?.build()
109 }
110
111 pub fn apply_document_with_config(
112 &self,
113 source: &Document,
114 context: &BindingContext,
115 config: &TransformConfig,
116 ) -> XmlResult<Document> {
117 let root = self
118 .apply_with_config(source, context, config)?
119 .into_single_element()?;
120 crate::builder::DocumentBuilder::new().root(root)?.build()
121 }
122}
123
124#[derive(Debug, Clone, PartialEq, Eq)]
125pub enum Template {
126 Fragment(Vec<Template>),
127 Element(ElementTemplate),
128 Text(String),
129 ParamText(String),
130 SelectText(Query),
131 Repeat {
132 select: Query,
133 template: Box<Template>,
134 },
135 IfParam {
136 name: String,
137 template: Box<Template>,
138 },
139 StaticFragment(FragmentBuilder),
140}
141
142impl Template {
143 pub fn fragment(children: impl IntoIterator<Item = Template>) -> Self {
144 Self::Fragment(children.into_iter().collect())
145 }
146
147 pub fn element(name: impl Into<String>) -> XmlResult<ElementTemplate> {
148 Ok(ElementTemplate {
149 name: QName::new(name)?,
150 attributes: Vec::new(),
151 namespaces: Vec::new(),
152 children: Vec::new(),
153 })
154 }
155
156 pub fn text(value: impl Into<String>) -> Self {
157 Self::Text(value.into())
158 }
159
160 pub fn param_text(name: impl Into<String>) -> Self {
161 Self::ParamText(name.into())
162 }
163
164 pub fn select_text(query: impl AsRef<str>) -> XmlResult<Self> {
165 Ok(Self::SelectText(Query::parse(query.as_ref())?))
166 }
167
168 pub fn repeat(select: impl AsRef<str>, template: Template) -> XmlResult<Self> {
169 Ok(Self::Repeat {
170 select: Query::parse(select.as_ref())?,
171 template: Box::new(template),
172 })
173 }
174
175 pub fn if_param(name: impl Into<String>, template: Template) -> Self {
176 Self::IfParam {
177 name: name.into(),
178 template: Box::new(template),
179 }
180 }
181
182 pub fn from_fragment(fragment: impl IntoXmlFragment) -> XmlResult<Self> {
183 Ok(Self::StaticFragment(fragment.into_xml_fragment()?))
184 }
185
186 pub fn from_xml_str(xml: &str) -> XmlResult<Self> {
187 let document = parser::parse_str(xml).map_err(|error| {
188 transform_error(format!(
189 "failed to parse external template: {}",
190 error.message()
191 ))
192 })?;
193 Ok(Self::StaticFragment(document_to_fragment(&document)?))
194 }
195
196 pub fn render(
197 &self,
198 source: &Document,
199 context: &BindingContext,
200 ) -> XmlResult<FragmentBuilder> {
201 self.render_with_counter(source, context, None)
202 }
203
204 fn render_with_counter(
205 &self,
206 source: &Document,
207 context: &BindingContext,
208 mut expansion: Option<&mut TransformExpansionCounter<'_>>,
209 ) -> XmlResult<FragmentBuilder> {
210 match self {
211 Self::Fragment(children) => {
212 let mut fragment = crate::builder::fragment();
213 for child in children {
214 fragment = fragment.child(child.render_with_counter(
215 source,
216 context,
217 expansion.as_deref_mut(),
218 )?)?;
219 }
220 Ok(fragment)
221 }
222 Self::Element(element) => element.render_with_counter(source, context, expansion),
223 Self::Text(value) => Ok(crate::builder::fragment().child(text(value.clone()))?),
224 Self::ParamText(name) => {
225 Ok(crate::builder::fragment().child(text(context.param(name)?))?)
226 }
227 Self::SelectText(query) => {
228 let values = query.evaluate_with_context(source, context.namespaces())?;
229 let mut fragment = crate::builder::fragment();
230 for value in values.strings() {
231 fragment = fragment.child(text(value))?;
232 }
233 Ok(fragment)
234 }
235 Self::Repeat { select, template } => {
236 let values = select.evaluate_with_context(source, context.namespaces())?;
237 let mut fragment = crate::builder::fragment();
238 for value in values.values() {
239 let QueryValue::Node(node_id) = value else {
240 continue;
241 };
242 let iteration_source = subtree_document(source, *node_id)?;
243 let rendered = template.render_with_counter(
244 &iteration_source,
245 context,
246 expansion.as_deref_mut(),
247 )?;
248 if let Some(expansion) = expansion.as_deref_mut() {
249 expansion.record_fragment(&rendered)?;
250 }
251 fragment = fragment.child(rendered)?;
252 }
253 Ok(fragment)
254 }
255 Self::IfParam { name, template } => {
256 if context.params.contains_key(name) {
257 template.render_with_counter(source, context, expansion)
258 } else {
259 Ok(crate::builder::fragment())
260 }
261 }
262 Self::StaticFragment(fragment) => Ok(fragment.clone()),
263 }
264 }
265}
266
267#[derive(Debug, Clone, PartialEq, Eq)]
268pub struct ElementTemplate {
269 name: QName,
270 attributes: Vec<AttributeTemplate>,
271 namespaces: Vec<NamespaceDeclaration>,
272 children: Vec<Template>,
273}
274
275impl ElementTemplate {
276 pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> XmlResult<Self> {
277 self.attributes.push(AttributeTemplate {
278 name: QName::new(name)?,
279 value: AttributeValue::Literal(value.into()),
280 });
281 Ok(self)
282 }
283
284 pub fn attr_param(
285 mut self,
286 name: impl Into<String>,
287 param: impl Into<String>,
288 ) -> XmlResult<Self> {
289 self.attributes.push(AttributeTemplate {
290 name: QName::new(name)?,
291 value: AttributeValue::Param(param.into()),
292 });
293 Ok(self)
294 }
295
296 pub fn child(mut self, child: Template) -> Self {
297 self.children.push(child);
298 self
299 }
300
301 pub fn default_namespace(mut self, uri: impl Into<String>) -> XmlResult<Self> {
302 self.namespaces.push(NamespaceDeclaration::default(uri)?);
303 Ok(self)
304 }
305
306 pub fn namespace(
307 mut self,
308 prefix: impl Into<String>,
309 uri: impl Into<String>,
310 ) -> XmlResult<Self> {
311 self.namespaces
312 .push(NamespaceDeclaration::prefixed(prefix, uri)?);
313 Ok(self)
314 }
315
316 pub fn build(self) -> Template {
317 Template::Element(self)
318 }
319
320 fn render_with_counter(
321 &self,
322 source: &Document,
323 context: &BindingContext,
324 mut expansion: Option<&mut TransformExpansionCounter<'_>>,
325 ) -> XmlResult<FragmentBuilder> {
326 let mut element = ElementBuilder::from_qname(self.name.clone())?;
327 for namespace in &self.namespaces {
328 element = apply_namespace(element, namespace)?;
329 }
330 for attribute in &self.attributes {
331 element = apply_attribute(element, attribute, context)?;
332 }
333 for child in &self.children {
334 element = element.child(child.render_with_counter(
335 source,
336 context,
337 expansion.as_deref_mut(),
338 )?)?;
339 }
340 crate::builder::fragment().child(element)
341 }
342}
343
344#[derive(Debug)]
345struct TransformExpansionCounter<'a> {
346 security: &'a TransformSecurityConfig,
347 expansion: usize,
348}
349
350impl<'a> TransformExpansionCounter<'a> {
351 fn new(security: &'a TransformSecurityConfig) -> Self {
352 Self {
353 security,
354 expansion: 0,
355 }
356 }
357
358 fn record_fragment(&mut self, fragment: &FragmentBuilder) -> XmlResult<()> {
359 self.expansion = self
360 .expansion
361 .saturating_add(count_fragment_nodes(fragment));
362 self.security.check_expansion(self.expansion)
363 }
364
365 fn check_fragment(&self, fragment: &FragmentBuilder) -> XmlResult<()> {
366 self.security
367 .check_expansion(count_fragment_nodes(fragment))
368 }
369}
370
371#[derive(Debug, Clone, PartialEq, Eq)]
372struct AttributeTemplate {
373 name: QName,
374 value: AttributeValue,
375}
376
377#[derive(Debug, Clone, PartialEq, Eq)]
378enum AttributeValue {
379 Literal(String),
380 Param(String),
381}
382
383fn apply_attribute(
384 element: ElementBuilder,
385 attribute: &AttributeTemplate,
386 context: &BindingContext,
387) -> XmlResult<ElementBuilder> {
388 let value = match &attribute.value {
389 AttributeValue::Literal(value) => value.as_str(),
390 AttributeValue::Param(name) => context.param(name)?,
391 };
392 apply_qname_attribute(element, &attribute.name, value)
393}
394
395fn apply_qname_attribute(
396 element: ElementBuilder,
397 name: &QName,
398 value: &str,
399) -> XmlResult<ElementBuilder> {
400 match (name.prefix(), name.namespace_uri()) {
401 (Some(prefix), Some(uri)) => {
402 element.qualified_attr(prefix.as_str(), name.local(), uri.as_str(), value)
403 }
404 _ => element.attr(name.local(), value),
405 }
406}
407
408fn apply_namespace(
409 element: ElementBuilder,
410 namespace: &NamespaceDeclaration,
411) -> XmlResult<ElementBuilder> {
412 match namespace.prefix() {
413 Some(prefix) => element.namespace(prefix.as_str(), namespace.uri().as_str()),
414 None => element.default_namespace(namespace.uri().as_str()),
415 }
416}
417
418fn document_to_fragment(document: &Document) -> XmlResult<FragmentBuilder> {
419 let Some(root) = document.root() else {
420 return Ok(crate::builder::fragment());
421 };
422 crate::builder::fragment().child(node_to_xml_node(document, root)?)
423}
424
425fn subtree_document(document: &Document, root: NodeId) -> XmlResult<Document> {
426 let fragment = crate::builder::fragment().child(node_to_xml_node(document, root)?)?;
427 let root = fragment.into_single_element()?;
428 crate::builder::DocumentBuilder::new().root(root)?.build()
429}
430
431fn node_to_xml_node(document: &Document, node_id: NodeId) -> XmlResult<XmlNode> {
432 Ok(match document.node(node_id)?.kind() {
433 NodeKind::Element(element) => XmlNode::Element(element_to_builder(document, element)?),
434 NodeKind::Text(value) => XmlNode::Text(value.clone()),
435 NodeKind::Comment(value) => XmlNode::Comment(value.clone()),
436 NodeKind::CData(value) => XmlNode::CData(value.clone()),
437 NodeKind::ProcessingInstruction { target, data } => XmlNode::ProcessingInstruction {
438 target: target.clone(),
439 data: data.clone(),
440 },
441 })
442}
443
444fn element_to_builder(document: &Document, element: &ElementData) -> XmlResult<ElementBuilder> {
445 let mut builder = ElementBuilder::from_qname(element.name().clone())?;
446 for namespace in element.namespace_declarations() {
447 builder = apply_namespace(builder, namespace)?;
448 }
449 for attribute in element.attributes() {
450 builder = apply_core_attribute(builder, attribute)?;
451 }
452 for child in element.children() {
453 builder = builder.child(node_to_xml_node(document, *child)?)?;
454 }
455 Ok(builder)
456}
457
458fn apply_core_attribute(
459 element: ElementBuilder,
460 attribute: &Attribute,
461) -> XmlResult<ElementBuilder> {
462 apply_qname_attribute(element, attribute.name(), attribute.value())
463}
464
465fn count_fragment_nodes(fragment: &FragmentBuilder) -> usize {
466 count_xml_nodes(fragment.nodes())
467}
468
469fn count_xml_nodes(nodes: &[XmlNode]) -> usize {
470 nodes.iter().fold(0usize, |count, node| {
471 count.saturating_add(count_xml_node(node))
472 })
473}
474
475fn count_xml_node(node: &XmlNode) -> usize {
476 match node {
477 XmlNode::Element(element) => 1usize.saturating_add(count_xml_nodes(element.child_nodes())),
478 XmlNode::Text(_)
479 | XmlNode::Comment(_)
480 | XmlNode::CData(_)
481 | XmlNode::ProcessingInstruction { .. } => 1,
482 }
483}
484
485fn transform_error(message: impl Into<String>) -> XmlError {
486 XmlError::new(ErrorKind::Validation, message)
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use crate::builder::element;
493 use crate::parser::parse_str;
494 use crate::security::{SecurityLimits, TransformSecurityConfig};
495 use crate::writer::to_string_compact;
496
497 fn source_document() -> XmlResult<Document> {
498 parse_str(
499 r#"<Root><Item code="A1"><Name>Alpha</Name></Item><Item code="B2"><Name>Beta</Name></Item></Root>"#,
500 )
501 }
502
503 fn names_template() -> XmlResult<Template> {
504 Ok(Template::element("Names")?
505 .child(Template::repeat(
506 "/Root/Item",
507 Template::element("Name")?
508 .child(Template::select_text("/Item/Name/text()")?)
509 .build(),
510 )?)
511 .build())
512 }
513
514 #[test]
515 fn transform_template_can_produce_document() -> XmlResult<()> {
516 let source = source_document()?;
517 let template = Template::element("FirstName")?
518 .child(Template::select_text("/Root/Item[@code='A1']/Name/text()")?)
519 .build();
520 let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
521
522 assert_eq!(
523 to_string_compact(&document)?,
524 "<FirstName>Alpha</FirstName>"
525 );
526 Ok(())
527 }
528
529 #[test]
530 fn transform_template_can_produce_fragment() -> XmlResult<()> {
531 let source = source_document()?;
532 let fragment = Template::fragment([
533 Template::element("A")?.child(Template::text("one")).build(),
534 Template::element("B")?.child(Template::text("two")).build(),
535 ])
536 .render(&source, &BindingContext::new())?;
537 let document = crate::builder::DocumentBuilder::new()
538 .root(element("Root")?.child(fragment)?)?
539 .build()?;
540
541 assert_eq!(
542 to_string_compact(&document)?,
543 "<Root><A>one</A><B>two</B></Root>"
544 );
545 Ok(())
546 }
547
548 #[test]
549 fn transform_select_reads_values_from_source() -> XmlResult<()> {
550 let source = source_document()?;
551 let template = Template::element("Value")?
552 .child(Template::select_text("/Root/Item[@code='B2']/Name/text()")?)
553 .build();
554 let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
555
556 assert_eq!(to_string_compact(&document)?, "<Value>Beta</Value>");
557 Ok(())
558 }
559
560 #[test]
561 fn transform_repeat_generates_multiple_nodes() -> XmlResult<()> {
562 let source = source_document()?;
563 let document =
564 Transform::new(names_template()?).apply_document(&source, &BindingContext::new())?;
565
566 assert_eq!(
567 to_string_compact(&document)?,
568 "<Names><Name>Alpha</Name><Name>Beta</Name></Names>"
569 );
570 Ok(())
571 }
572
573 #[test]
574 fn transform_config_allows_expansion_within_limit() -> XmlResult<()> {
575 let source = source_document()?;
576 let config = TransformConfig::new().with_security(
577 TransformSecurityConfig::new()
578 .with_limits(SecurityLimits::new().with_max_transform_expansion(5)),
579 );
580 let document = Transform::new(names_template()?).apply_document_with_config(
581 &source,
582 &BindingContext::new(),
583 &config,
584 )?;
585
586 assert_eq!(
587 to_string_compact(&document)?,
588 "<Names><Name>Alpha</Name><Name>Beta</Name></Names>"
589 );
590 Ok(())
591 }
592
593 #[test]
594 fn transform_config_rejects_repeat_expansion_excess() -> XmlResult<()> {
595 let source = source_document()?;
596 let config = TransformConfig::new().with_security(
597 TransformSecurityConfig::new()
598 .with_limits(SecurityLimits::new().with_max_transform_expansion(3)),
599 );
600 let error = Transform::new(names_template()?)
601 .apply_with_config(&source, &BindingContext::new(), &config)
602 .expect_err("repeat expansion must be limited");
603
604 assert_eq!(error.kind(), &ErrorKind::Parse);
605 assert!(error.message().contains("transform expansion exceeds"));
606 Ok(())
607 }
608
609 #[test]
610 fn transform_conditionals_cover_true_and_false() -> XmlResult<()> {
611 let source = source_document()?;
612 let template = Template::element("Root")?
613 .child(Template::if_param(
614 "include",
615 Template::element("Included")?
616 .child(Template::text("yes"))
617 .build(),
618 ))
619 .build();
620
621 let included = Transform::new(template.clone())
622 .apply_document(&source, &BindingContext::new().with_param("include", "1"))?;
623 let omitted = Transform::new(template).apply_document(&source, &BindingContext::new())?;
624
625 assert_eq!(
626 to_string_compact(&included)?,
627 "<Root><Included>yes</Included></Root>"
628 );
629 assert_eq!(to_string_compact(&omitted)?, "<Root/>");
630 Ok(())
631 }
632
633 #[test]
634 fn transform_params_resolve_and_missing_param_errors() -> XmlResult<()> {
635 let source = source_document()?;
636 let template = Template::element("Report")?
637 .attr_param("title", "title")?
638 .child(Template::param_text("title"))
639 .build();
640
641 let document = Transform::new(template.clone()).apply_document(
642 &source,
643 &BindingContext::new().with_param("title", "Inventory"),
644 )?;
645 let error = Transform::new(template)
646 .apply_document(&source, &BindingContext::new())
647 .expect_err("missing param must fail");
648
649 assert_eq!(
650 to_string_compact(&document)?,
651 "<Report title=\"Inventory\">Inventory</Report>"
652 );
653 assert_eq!(error.kind(), &ErrorKind::Validation);
654 assert!(error.message().contains("missing transform parameter"));
655 Ok(())
656 }
657
658 #[test]
659 fn transform_external_template_can_load_from_string() -> XmlResult<()> {
660 let source = source_document()?;
661 let template = Template::from_xml_str("<External><Ready>yes</Ready></External>")?;
662 let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
663
664 assert_eq!(
665 to_string_compact(&document)?,
666 "<External><Ready>yes</Ready></External>"
667 );
668 Ok(())
669 }
670
671 #[test]
672 fn transform_component_can_act_as_programmatic_template() -> XmlResult<()> {
673 fn badge(label: &str) -> XmlResult<ElementBuilder> {
674 element("Badge")?.attr("label", label)
675 }
676
677 let source = source_document()?;
678 let template = Template::from_fragment(badge("ok")?)?;
679 let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
680
681 assert_eq!(to_string_compact(&document)?, "<Badge label=\"ok\"/>");
682 Ok(())
683 }
684}