use std::collections::BTreeMap;
use crate::builder::{text, ElementBuilder, FragmentBuilder, IntoXmlFragment, XmlNode};
use crate::core::{
Attribute, Document, ElementData, ErrorKind, NamespaceDeclaration, NodeId, NodeKind, QName,
XmlError, XmlResult,
};
use crate::parser;
use crate::query::{NamespaceContext, Query, QueryValue};
use crate::security::TransformSecurityConfig;
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct BindingContext {
params: BTreeMap<String, String>,
namespaces: NamespaceContext,
}
impl BindingContext {
pub fn new() -> Self {
Self::default()
}
pub fn with_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.params.insert(name.into(), value.into());
self
}
pub fn with_namespace(
mut self,
alias: impl Into<String>,
uri: impl Into<String>,
) -> XmlResult<Self> {
self.namespaces = self.namespaces.with_alias(alias, uri)?;
Ok(self)
}
pub fn param(&self, name: &str) -> XmlResult<&str> {
self.params
.get(name)
.map(String::as_str)
.ok_or_else(|| transform_error(format!("missing transform parameter `{name}`")))
}
pub fn namespaces(&self) -> &NamespaceContext {
&self.namespaces
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct TransformConfig {
security: TransformSecurityConfig,
}
impl TransformConfig {
pub fn new() -> Self {
Self::default()
}
pub fn with_security(mut self, security: TransformSecurityConfig) -> Self {
self.security = security;
self
}
pub fn security(&self) -> &TransformSecurityConfig {
&self.security
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Transform {
template: Template,
}
impl Transform {
pub fn new(template: Template) -> Self {
Self { template }
}
pub fn apply(&self, source: &Document, context: &BindingContext) -> XmlResult<FragmentBuilder> {
self.apply_with_config(source, context, &TransformConfig::default())
}
pub fn apply_with_config(
&self,
source: &Document,
context: &BindingContext,
config: &TransformConfig,
) -> XmlResult<FragmentBuilder> {
let mut expansion = TransformExpansionCounter::new(config.security());
let fragment = self
.template
.render_with_counter(source, context, Some(&mut expansion))?;
expansion.check_fragment(&fragment)?;
Ok(fragment)
}
pub fn apply_document(
&self,
source: &Document,
context: &BindingContext,
) -> XmlResult<Document> {
let root = self.apply(source, context)?.into_single_element()?;
crate::builder::DocumentBuilder::new().root(root)?.build()
}
pub fn apply_document_with_config(
&self,
source: &Document,
context: &BindingContext,
config: &TransformConfig,
) -> XmlResult<Document> {
let root = self
.apply_with_config(source, context, config)?
.into_single_element()?;
crate::builder::DocumentBuilder::new().root(root)?.build()
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum Template {
Fragment(Vec<Template>),
Element(ElementTemplate),
Text(String),
ParamText(String),
SelectText(Query),
Repeat {
select: Query,
template: Box<Template>,
},
IfParam {
name: String,
template: Box<Template>,
},
StaticFragment(FragmentBuilder),
}
impl Template {
pub fn fragment(children: impl IntoIterator<Item = Template>) -> Self {
Self::Fragment(children.into_iter().collect())
}
pub fn element(name: impl Into<String>) -> XmlResult<ElementTemplate> {
Ok(ElementTemplate {
name: QName::new(name)?,
attributes: Vec::new(),
namespaces: Vec::new(),
children: Vec::new(),
})
}
pub fn text(value: impl Into<String>) -> Self {
Self::Text(value.into())
}
pub fn param_text(name: impl Into<String>) -> Self {
Self::ParamText(name.into())
}
pub fn select_text(query: impl AsRef<str>) -> XmlResult<Self> {
Ok(Self::SelectText(Query::parse(query.as_ref())?))
}
pub fn repeat(select: impl AsRef<str>, template: Template) -> XmlResult<Self> {
Ok(Self::Repeat {
select: Query::parse(select.as_ref())?,
template: Box::new(template),
})
}
pub fn if_param(name: impl Into<String>, template: Template) -> Self {
Self::IfParam {
name: name.into(),
template: Box::new(template),
}
}
pub fn from_fragment(fragment: impl IntoXmlFragment) -> XmlResult<Self> {
Ok(Self::StaticFragment(fragment.into_xml_fragment()?))
}
pub fn from_xml_str(xml: &str) -> XmlResult<Self> {
let document = parser::parse_str(xml).map_err(|error| {
transform_error(format!(
"failed to parse external template: {}",
error.message()
))
})?;
Ok(Self::StaticFragment(document_to_fragment(&document)?))
}
pub fn render(
&self,
source: &Document,
context: &BindingContext,
) -> XmlResult<FragmentBuilder> {
self.render_with_counter(source, context, None)
}
fn render_with_counter(
&self,
source: &Document,
context: &BindingContext,
mut expansion: Option<&mut TransformExpansionCounter<'_>>,
) -> XmlResult<FragmentBuilder> {
match self {
Self::Fragment(children) => {
let mut fragment = crate::builder::fragment();
for child in children {
fragment = fragment.child(child.render_with_counter(
source,
context,
expansion.as_deref_mut(),
)?)?;
}
Ok(fragment)
}
Self::Element(element) => element.render_with_counter(source, context, expansion),
Self::Text(value) => Ok(crate::builder::fragment().child(text(value.clone()))?),
Self::ParamText(name) => {
Ok(crate::builder::fragment().child(text(context.param(name)?))?)
}
Self::SelectText(query) => {
let values = query.evaluate_with_context(source, context.namespaces())?;
let mut fragment = crate::builder::fragment();
for value in values.strings() {
fragment = fragment.child(text(value))?;
}
Ok(fragment)
}
Self::Repeat { select, template } => {
let values = select.evaluate_with_context(source, context.namespaces())?;
let mut fragment = crate::builder::fragment();
for value in values.values() {
let QueryValue::Node(node_id) = value else {
continue;
};
let iteration_source = subtree_document(source, *node_id)?;
let rendered = template.render_with_counter(
&iteration_source,
context,
expansion.as_deref_mut(),
)?;
if let Some(expansion) = expansion.as_deref_mut() {
expansion.record_fragment(&rendered)?;
}
fragment = fragment.child(rendered)?;
}
Ok(fragment)
}
Self::IfParam { name, template } => {
if context.params.contains_key(name) {
template.render_with_counter(source, context, expansion)
} else {
Ok(crate::builder::fragment())
}
}
Self::StaticFragment(fragment) => Ok(fragment.clone()),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ElementTemplate {
name: QName,
attributes: Vec<AttributeTemplate>,
namespaces: Vec<NamespaceDeclaration>,
children: Vec<Template>,
}
impl ElementTemplate {
pub fn attr(mut self, name: impl Into<String>, value: impl Into<String>) -> XmlResult<Self> {
self.attributes.push(AttributeTemplate {
name: QName::new(name)?,
value: AttributeValue::Literal(value.into()),
});
Ok(self)
}
pub fn attr_param(
mut self,
name: impl Into<String>,
param: impl Into<String>,
) -> XmlResult<Self> {
self.attributes.push(AttributeTemplate {
name: QName::new(name)?,
value: AttributeValue::Param(param.into()),
});
Ok(self)
}
pub fn child(mut self, child: Template) -> Self {
self.children.push(child);
self
}
pub fn default_namespace(mut self, uri: impl Into<String>) -> XmlResult<Self> {
self.namespaces.push(NamespaceDeclaration::default(uri)?);
Ok(self)
}
pub fn namespace(
mut self,
prefix: impl Into<String>,
uri: impl Into<String>,
) -> XmlResult<Self> {
self.namespaces
.push(NamespaceDeclaration::prefixed(prefix, uri)?);
Ok(self)
}
pub fn build(self) -> Template {
Template::Element(self)
}
fn render_with_counter(
&self,
source: &Document,
context: &BindingContext,
mut expansion: Option<&mut TransformExpansionCounter<'_>>,
) -> XmlResult<FragmentBuilder> {
let mut element = ElementBuilder::from_qname(self.name.clone())?;
for namespace in &self.namespaces {
element = apply_namespace(element, namespace)?;
}
for attribute in &self.attributes {
element = apply_attribute(element, attribute, context)?;
}
for child in &self.children {
element = element.child(child.render_with_counter(
source,
context,
expansion.as_deref_mut(),
)?)?;
}
crate::builder::fragment().child(element)
}
}
#[derive(Debug)]
struct TransformExpansionCounter<'a> {
security: &'a TransformSecurityConfig,
expansion: usize,
}
impl<'a> TransformExpansionCounter<'a> {
fn new(security: &'a TransformSecurityConfig) -> Self {
Self {
security,
expansion: 0,
}
}
fn record_fragment(&mut self, fragment: &FragmentBuilder) -> XmlResult<()> {
self.expansion = self
.expansion
.saturating_add(count_fragment_nodes(fragment));
self.security.check_expansion(self.expansion)
}
fn check_fragment(&self, fragment: &FragmentBuilder) -> XmlResult<()> {
self.security
.check_expansion(count_fragment_nodes(fragment))
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct AttributeTemplate {
name: QName,
value: AttributeValue,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum AttributeValue {
Literal(String),
Param(String),
}
fn apply_attribute(
element: ElementBuilder,
attribute: &AttributeTemplate,
context: &BindingContext,
) -> XmlResult<ElementBuilder> {
let value = match &attribute.value {
AttributeValue::Literal(value) => value.as_str(),
AttributeValue::Param(name) => context.param(name)?,
};
apply_qname_attribute(element, &attribute.name, value)
}
fn apply_qname_attribute(
element: ElementBuilder,
name: &QName,
value: &str,
) -> XmlResult<ElementBuilder> {
match (name.prefix(), name.namespace_uri()) {
(Some(prefix), Some(uri)) => {
element.qualified_attr(prefix.as_str(), name.local(), uri.as_str(), value)
}
_ => element.attr(name.local(), value),
}
}
fn apply_namespace(
element: ElementBuilder,
namespace: &NamespaceDeclaration,
) -> XmlResult<ElementBuilder> {
match namespace.prefix() {
Some(prefix) => element.namespace(prefix.as_str(), namespace.uri().as_str()),
None => element.default_namespace(namespace.uri().as_str()),
}
}
fn document_to_fragment(document: &Document) -> XmlResult<FragmentBuilder> {
let Some(root) = document.root() else {
return Ok(crate::builder::fragment());
};
crate::builder::fragment().child(node_to_xml_node(document, root)?)
}
fn subtree_document(document: &Document, root: NodeId) -> XmlResult<Document> {
let fragment = crate::builder::fragment().child(node_to_xml_node(document, root)?)?;
let root = fragment.into_single_element()?;
crate::builder::DocumentBuilder::new().root(root)?.build()
}
fn node_to_xml_node(document: &Document, node_id: NodeId) -> XmlResult<XmlNode> {
Ok(match document.node(node_id)?.kind() {
NodeKind::Element(element) => XmlNode::Element(element_to_builder(document, element)?),
NodeKind::Text(value) => XmlNode::Text(value.clone()),
NodeKind::Comment(value) => XmlNode::Comment(value.clone()),
NodeKind::CData(value) => XmlNode::CData(value.clone()),
NodeKind::ProcessingInstruction { target, data } => XmlNode::ProcessingInstruction {
target: target.clone(),
data: data.clone(),
},
})
}
fn element_to_builder(document: &Document, element: &ElementData) -> XmlResult<ElementBuilder> {
let mut builder = ElementBuilder::from_qname(element.name().clone())?;
for namespace in element.namespace_declarations() {
builder = apply_namespace(builder, namespace)?;
}
for attribute in element.attributes() {
builder = apply_core_attribute(builder, attribute)?;
}
for child in element.children() {
builder = builder.child(node_to_xml_node(document, *child)?)?;
}
Ok(builder)
}
fn apply_core_attribute(
element: ElementBuilder,
attribute: &Attribute,
) -> XmlResult<ElementBuilder> {
apply_qname_attribute(element, attribute.name(), attribute.value())
}
fn count_fragment_nodes(fragment: &FragmentBuilder) -> usize {
count_xml_nodes(fragment.nodes())
}
fn count_xml_nodes(nodes: &[XmlNode]) -> usize {
nodes.iter().fold(0usize, |count, node| {
count.saturating_add(count_xml_node(node))
})
}
fn count_xml_node(node: &XmlNode) -> usize {
match node {
XmlNode::Element(element) => 1usize.saturating_add(count_xml_nodes(element.child_nodes())),
XmlNode::Text(_)
| XmlNode::Comment(_)
| XmlNode::CData(_)
| XmlNode::ProcessingInstruction { .. } => 1,
}
}
fn transform_error(message: impl Into<String>) -> XmlError {
XmlError::new(ErrorKind::Validation, message)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::builder::element;
use crate::parser::parse_str;
use crate::security::{SecurityLimits, TransformSecurityConfig};
use crate::writer::to_string_compact;
fn source_document() -> XmlResult<Document> {
parse_str(
r#"<Root><Item code="A1"><Name>Alpha</Name></Item><Item code="B2"><Name>Beta</Name></Item></Root>"#,
)
}
fn names_template() -> XmlResult<Template> {
Ok(Template::element("Names")?
.child(Template::repeat(
"/Root/Item",
Template::element("Name")?
.child(Template::select_text("/Item/Name/text()")?)
.build(),
)?)
.build())
}
#[test]
fn transform_template_can_produce_document() -> XmlResult<()> {
let source = source_document()?;
let template = Template::element("FirstName")?
.child(Template::select_text("/Root/Item[@code='A1']/Name/text()")?)
.build();
let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
assert_eq!(
to_string_compact(&document)?,
"<FirstName>Alpha</FirstName>"
);
Ok(())
}
#[test]
fn transform_template_can_produce_fragment() -> XmlResult<()> {
let source = source_document()?;
let fragment = Template::fragment([
Template::element("A")?.child(Template::text("one")).build(),
Template::element("B")?.child(Template::text("two")).build(),
])
.render(&source, &BindingContext::new())?;
let document = crate::builder::DocumentBuilder::new()
.root(element("Root")?.child(fragment)?)?
.build()?;
assert_eq!(
to_string_compact(&document)?,
"<Root><A>one</A><B>two</B></Root>"
);
Ok(())
}
#[test]
fn transform_select_reads_values_from_source() -> XmlResult<()> {
let source = source_document()?;
let template = Template::element("Value")?
.child(Template::select_text("/Root/Item[@code='B2']/Name/text()")?)
.build();
let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
assert_eq!(to_string_compact(&document)?, "<Value>Beta</Value>");
Ok(())
}
#[test]
fn transform_repeat_generates_multiple_nodes() -> XmlResult<()> {
let source = source_document()?;
let document =
Transform::new(names_template()?).apply_document(&source, &BindingContext::new())?;
assert_eq!(
to_string_compact(&document)?,
"<Names><Name>Alpha</Name><Name>Beta</Name></Names>"
);
Ok(())
}
#[test]
fn transform_config_allows_expansion_within_limit() -> XmlResult<()> {
let source = source_document()?;
let config = TransformConfig::new().with_security(
TransformSecurityConfig::new()
.with_limits(SecurityLimits::new().with_max_transform_expansion(5)),
);
let document = Transform::new(names_template()?).apply_document_with_config(
&source,
&BindingContext::new(),
&config,
)?;
assert_eq!(
to_string_compact(&document)?,
"<Names><Name>Alpha</Name><Name>Beta</Name></Names>"
);
Ok(())
}
#[test]
fn transform_config_rejects_repeat_expansion_excess() -> XmlResult<()> {
let source = source_document()?;
let config = TransformConfig::new().with_security(
TransformSecurityConfig::new()
.with_limits(SecurityLimits::new().with_max_transform_expansion(3)),
);
let error = Transform::new(names_template()?)
.apply_with_config(&source, &BindingContext::new(), &config)
.expect_err("repeat expansion must be limited");
assert_eq!(error.kind(), &ErrorKind::Parse);
assert!(error.message().contains("transform expansion exceeds"));
Ok(())
}
#[test]
fn transform_conditionals_cover_true_and_false() -> XmlResult<()> {
let source = source_document()?;
let template = Template::element("Root")?
.child(Template::if_param(
"include",
Template::element("Included")?
.child(Template::text("yes"))
.build(),
))
.build();
let included = Transform::new(template.clone())
.apply_document(&source, &BindingContext::new().with_param("include", "1"))?;
let omitted = Transform::new(template).apply_document(&source, &BindingContext::new())?;
assert_eq!(
to_string_compact(&included)?,
"<Root><Included>yes</Included></Root>"
);
assert_eq!(to_string_compact(&omitted)?, "<Root/>");
Ok(())
}
#[test]
fn transform_params_resolve_and_missing_param_errors() -> XmlResult<()> {
let source = source_document()?;
let template = Template::element("Report")?
.attr_param("title", "title")?
.child(Template::param_text("title"))
.build();
let document = Transform::new(template.clone()).apply_document(
&source,
&BindingContext::new().with_param("title", "Inventory"),
)?;
let error = Transform::new(template)
.apply_document(&source, &BindingContext::new())
.expect_err("missing param must fail");
assert_eq!(
to_string_compact(&document)?,
"<Report title=\"Inventory\">Inventory</Report>"
);
assert_eq!(error.kind(), &ErrorKind::Validation);
assert!(error.message().contains("missing transform parameter"));
Ok(())
}
#[test]
fn transform_external_template_can_load_from_string() -> XmlResult<()> {
let source = source_document()?;
let template = Template::from_xml_str("<External><Ready>yes</Ready></External>")?;
let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
assert_eq!(
to_string_compact(&document)?,
"<External><Ready>yes</Ready></External>"
);
Ok(())
}
#[test]
fn transform_component_can_act_as_programmatic_template() -> XmlResult<()> {
fn badge(label: &str) -> XmlResult<ElementBuilder> {
element("Badge")?.attr("label", label)
}
let source = source_document()?;
let template = Template::from_fragment(badge("ok")?)?;
let document = Transform::new(template).apply_document(&source, &BindingContext::new())?;
assert_eq!(to_string_compact(&document)?, "<Badge label=\"ok\"/>");
Ok(())
}
}