Skip to main content

xdoc/transform/
mod.rs

1//! XML-to-XML transformation primitives.
2//!
3//! The MVP is a programmatic template layer over `builder`, `component`, and
4//! `query`. It deliberately does not implement XSLT or XQuery.
5
6use 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}