mod parser;
use facet_xml as xml;
use std::collections::HashMap;
pub use parser::{
ElementParseError, ElementParser, ElementSerializeError, ElementSerializer, from_element,
to_element,
};
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum PathError {
EmptyPath { path: Vec<usize> },
IndexOutOfBounds {
path: Vec<usize>,
index: usize,
len: usize,
},
TextNodeHasNoChildren { path: Vec<usize> },
}
impl std::fmt::Display for PathError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
PathError::EmptyPath { path } => write!(f, "empty path: {path:?}"),
PathError::IndexOutOfBounds { path, index, len } => {
write!(
f,
"index {index} out of bounds (len={len}) at path {path:?}"
)
}
PathError::TextNodeHasNoChildren { path } => {
write!(f, "text node has no children at path {path:?}")
}
}
}
}
impl std::error::Error for PathError {}
#[derive(Debug, Clone, PartialEq, Eq, facet::Facet)]
#[repr(u8)]
pub enum Content {
#[facet(xml::text)]
Text(String),
#[facet(xml::custom_element)]
Element(Element),
}
impl Content {
pub fn as_text(&self) -> Option<&str> {
match self {
Content::Text(t) => Some(t),
_ => None,
}
}
pub fn as_element(&self) -> Option<&Element> {
match self {
Content::Element(e) => Some(e),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default, facet::Facet)]
pub struct Element {
#[facet(xml::tag, default)]
pub tag: String,
#[facet(flatten, default)]
pub attrs: HashMap<String, String>,
#[facet(flatten, default)]
#[facet(recursive_type)]
pub children: Vec<Content>,
}
impl Element {
pub fn new(tag: impl Into<String>) -> Self {
Self {
tag: tag.into(),
attrs: HashMap::new(),
children: Vec::new(),
}
}
pub fn with_attr(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.attrs.insert(name.into(), value.into());
self
}
pub fn with_child(mut self, child: Element) -> Self {
self.children.push(Content::Element(child));
self
}
pub fn with_text(mut self, text: impl Into<String>) -> Self {
self.children.push(Content::Text(text.into()));
self
}
pub fn get_attr(&self, name: &str) -> Option<&str> {
self.attrs.get(name).map(|s| s.as_str())
}
pub fn child_elements(&self) -> impl Iterator<Item = &Element> {
self.children.iter().filter_map(|c| c.as_element())
}
pub fn text_content(&self) -> String {
let mut result = String::new();
for child in &self.children {
match child {
Content::Text(t) => result.push_str(t),
Content::Element(e) => result.push_str(&e.text_content()),
}
}
result
}
pub fn get_content_mut(&mut self, path: &[usize]) -> Result<&mut Content, PathError> {
if path.is_empty() {
return Err(PathError::EmptyPath { path: vec![] });
}
let idx = path[0];
let len = self.children.len();
let child = self
.children
.get_mut(idx)
.ok_or_else(|| PathError::IndexOutOfBounds {
path: path.to_vec(),
index: idx,
len,
})?;
if path.len() == 1 {
return Ok(child);
}
match child {
Content::Element(e) => e.get_content_mut(&path[1..]),
Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
path: path.to_vec(),
}),
}
}
pub fn children_mut(&mut self, path: &[usize]) -> Result<&mut Vec<Content>, PathError> {
if path.is_empty() {
return Ok(&mut self.children);
}
match self.get_content_mut(path)? {
Content::Element(e) => Ok(&mut e.children),
Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
path: path.to_vec(),
}),
}
}
pub fn attrs_mut(&mut self, path: &[usize]) -> Result<&mut HashMap<String, String>, PathError> {
if path.is_empty() {
return Ok(&mut self.attrs);
}
match self.get_content_mut(path)? {
Content::Element(e) => Ok(&mut e.attrs),
Content::Text(_) => Err(PathError::TextNodeHasNoChildren {
path: path.to_vec(),
}),
}
}
pub fn to_html(&self) -> String {
let mut out = String::new();
self.write_html(&mut out);
out
}
pub fn write_html(&self, out: &mut String) {
out.push('<');
out.push_str(&self.tag);
let mut attr_list: Vec<_> = self.attrs.iter().collect();
attr_list.sort_by_key(|(k, _)| *k);
for (k, v) in attr_list {
out.push(' ');
out.push_str(k);
out.push_str("=\"");
out.push_str(&html_escape(v));
out.push('"');
}
out.push('>');
for child in &self.children {
match child {
Content::Text(s) => out.push_str(s),
Content::Element(e) => e.write_html(out),
}
}
out.push_str("</");
out.push_str(&self.tag);
out.push('>');
}
}
fn html_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
impl From<Element> for Content {
fn from(e: Element) -> Self {
Content::Element(e)
}
}
impl From<String> for Content {
fn from(s: String) -> Self {
Content::Text(s)
}
}
impl From<&str> for Content {
fn from(s: &str) -> Self {
Content::Text(s.to_owned())
}
}
#[cfg(test)]
mod tests {
use std::{fmt::Display, str::FromStr};
use super::*;
use facet::Facet;
use facet_testhelpers::test;
#[test]
fn element_builder_api() {
let elem = Element::new("root")
.with_attr("id", "123")
.with_child(Element::new("child").with_text("hello world"));
assert_eq!(elem.tag, "root");
assert_eq!(elem.get_attr("id"), Some("123"));
assert_eq!(elem.children.len(), 1);
let child = elem.child_elements().next().unwrap();
assert_eq!(child.tag, "child");
assert_eq!(child.text_content(), "hello world");
}
#[test]
fn parse_simple_xml() {
let xml = r#"<root><child>hello</child></root>"#;
let elem: Element = facet_xml::from_str(xml).unwrap();
assert_eq!(elem.tag, "root");
assert_eq!(elem.children.len(), 1);
let child = elem.child_elements().next().unwrap();
assert_eq!(child.tag, "child");
assert_eq!(child.text_content(), "hello");
}
#[test]
fn parse_with_attributes() {
let xml = r#"<root id="123" class="test"><child name="foo">bar</child></root>"#;
let elem: Element = facet_xml::from_str(xml).unwrap();
assert_eq!(elem.tag, "root");
assert_eq!(elem.get_attr("id"), Some("123"));
assert_eq!(elem.get_attr("class"), Some("test"));
let child = elem.child_elements().next().unwrap();
assert_eq!(child.get_attr("name"), Some("foo"));
assert_eq!(child.text_content(), "bar");
}
#[test]
fn parse_mixed_content() {
let xml = r#"<p>Hello <b>world</b>!</p>"#;
let elem: Element = facet_xml::from_str(xml).unwrap();
assert_eq!(elem.tag, "p");
assert_eq!(elem.children.len(), 3);
assert_eq!(elem.children[0].as_text(), Some("Hello"));
assert_eq!(elem.children[1].as_element().unwrap().tag, "b");
assert_eq!(elem.children[2].as_text(), Some("!"));
assert_eq!(elem.text_content(), "Helloworld!");
}
#[test]
fn from_element_to_struct() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Person {
name: String,
age: u32,
}
let elem = Element::new("person")
.with_child(Element::new("name").with_text("Alice"))
.with_child(Element::new("age").with_text("30"));
let person: Person = from_element(&elem).unwrap();
assert_eq!(person.name, "Alice");
assert_eq!(person.age, 30);
}
#[test]
fn from_element_with_attrs() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Item {
#[facet(xml::attribute)]
id: String,
value: String,
}
let elem = Element::new("item")
.with_attr("id", "123")
.with_child(Element::new("value").with_text("hello"));
let item: Item = from_element(&elem).unwrap();
assert_eq!(item.id, "123");
assert_eq!(item.value, "hello");
}
#[test]
fn to_element_simple() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Person {
name: String,
age: u32,
}
let person = Person {
name: "Alice".to_string(),
age: 30,
};
let elem = to_element(&person).unwrap();
assert_eq!(elem.tag, "person");
assert_eq!(elem.children.len(), 2);
let name_child = elem.child_elements().find(|e| e.tag == "name").unwrap();
assert_eq!(name_child.text_content(), "Alice");
let age_child = elem.child_elements().find(|e| e.tag == "age").unwrap();
assert_eq!(age_child.text_content(), "30");
}
#[test]
fn to_element_with_attrs() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Item {
#[facet(xml::attribute)]
id: String,
value: String,
}
let item = Item {
id: "123".to_string(),
value: "hello".to_string(),
};
let elem = to_element(&item).unwrap();
assert_eq!(elem.tag, "item");
assert_eq!(elem.get_attr("id"), Some("123"));
let value_child = elem.child_elements().find(|e| e.tag == "value").unwrap();
assert_eq!(value_child.text_content(), "hello");
}
#[test]
fn roundtrip_simple() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Person {
name: String,
age: u32,
}
let original = Person {
name: "Bob".to_string(),
age: 42,
};
let elem = to_element(&original).unwrap();
let roundtripped: Person = from_element(&elem).unwrap();
assert_eq!(original, roundtripped);
}
#[test]
fn roundtrip_with_attrs() {
#[derive(facet::Facet, Debug, PartialEq)]
struct Item {
#[facet(xml::attribute)]
id: String,
#[facet(xml::attribute)]
version: u32,
value: String,
}
let original = Item {
id: "test-123".to_string(),
version: 5,
value: "content".to_string(),
};
let elem = to_element(&original).unwrap();
let roundtripped: Item = from_element(&elem).unwrap();
assert_eq!(original, roundtripped);
}
#[test]
fn vec_element_matches_any_tag() {
#[derive(facet::Facet, Debug)]
#[facet(rename = "any")]
struct AnyContainer {
#[facet(xml::elements)]
elements: Vec<Element>,
}
let xml = r#"<any><foo a="b" /><bar c="d" /></any>"#;
let result: AnyContainer = facet_xml::from_str(xml).unwrap();
assert_eq!(result.elements.len(), 2);
assert_eq!(result.elements[0].tag, "foo");
assert_eq!(result.elements[0].get_attr("a"), Some("b"));
assert_eq!(result.elements[1].tag, "bar");
assert_eq!(result.elements[1].get_attr("c"), Some("d"));
}
#[test]
fn vec_element_catch_all_with_specific_field() {
#[derive(facet::Facet, Debug)]
#[facet(rename = "container")]
struct MixedContainer {
name: String,
#[facet(xml::elements)]
others: Vec<Element>,
}
let xml = r#"<container><name>test</name><foo>a</foo><bar>b</bar></container>"#;
let result: MixedContainer = facet_xml::from_str(xml).unwrap();
assert_eq!(result.name, "test");
assert_eq!(result.others.len(), 2);
assert_eq!(result.others[0].tag, "foo");
assert_eq!(result.others[1].tag, "bar");
}
#[test]
fn vec_element_ignores_text_nodes() {
#[derive(facet::Facet, Debug)]
#[facet(rename = "any")]
struct AnyContainer {
#[facet(xml::elements)]
elements: Vec<Element>,
}
let xml = r#"<any>text before<foo/>middle text<bar/>text after</any>"#;
let result: AnyContainer = facet_xml::from_str(xml).unwrap();
assert_eq!(result.elements.len(), 2);
assert_eq!(result.elements[0].tag, "foo");
assert_eq!(result.elements[1].tag, "bar");
}
#[test]
fn vec_element_roundtrip() {
#[derive(facet::Facet, Debug, PartialEq)]
#[facet(rename = "container")]
struct Container {
#[facet(xml::elements)]
elements: Vec<Element>,
}
let original = Container {
elements: vec![
Element::new("foo").with_attr("a", "1"),
Element::new("bar").with_text("hello"),
],
};
let xml = facet_xml::to_string(&original).unwrap();
assert!(xml.contains("<foo"), "expected <foo>, got: {}", xml);
assert!(xml.contains("<bar"), "expected <bar>, got: {}", xml);
let roundtripped: Container = facet_xml::from_str(&xml).unwrap();
assert_eq!(roundtripped.elements.len(), 2);
assert_eq!(roundtripped.elements[0].tag, "foo");
assert_eq!(roundtripped.elements[0].get_attr("a"), Some("1"));
assert_eq!(roundtripped.elements[1].tag, "bar");
assert_eq!(roundtripped.elements[1].text_content(), "hello");
}
#[test]
fn vec_element_empty_container() {
#[derive(facet::Facet, Debug)]
#[facet(rename = "empty")]
struct EmptyContainer {
#[facet(xml::elements)]
elements: Vec<Element>,
}
let xml = r#"<empty></empty>"#;
let result: EmptyContainer = facet_xml::from_str(xml).unwrap();
assert!(result.elements.is_empty());
}
#[derive(Debug, Facet)]
#[facet(proxy = StringRepr)]
struct ConstantName;
#[derive(Debug, Facet)]
#[repr(transparent)]
pub(crate) struct StringRepr(pub String);
impl Display for ConstantName {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "CONSTANT")
}
}
impl FromStr for ConstantName {
type Err = &'static str;
fn from_str(s: &str) -> Result<Self, Self::Err> {
if s == "CONSTANT" {
Ok(Self)
} else {
Err("expected `CONSTANT`")
}
}
}
impl From<ConstantName> for StringRepr {
fn from(value: ConstantName) -> Self {
Self(value.to_string())
}
}
impl From<&ConstantName> for StringRepr {
fn from(value: &ConstantName) -> Self {
Self(value.to_string())
}
}
impl TryFrom<StringRepr> for ConstantName {
type Error = <ConstantName as core::str::FromStr>::Err;
fn try_from(value: StringRepr) -> Result<Self, Self::Error> {
value.0.parse()
}
}
impl TryFrom<&StringRepr> for ConstantName {
type Error = <ConstantName as core::str::FromStr>::Err;
fn try_from(value: &StringRepr) -> Result<Self, Self::Error> {
value.0.parse()
}
}
#[derive(Debug, Facet)]
#[repr(C)]
enum Foo {
#[facet(rename = "foo")]
Value {
#[facet(xml::attribute)]
#[allow(unused)]
name: ConstantName,
#[facet(xml::attribute)]
#[allow(unused)]
exists: String,
},
}
#[test]
fn transparent_attribute_not_discarded() {
let raw_xml = r#"
<foo name="CONSTANT" exists="i do exist and am not discarded"></foo>"#;
let x: Foo = facet_xml::from_str(raw_xml).unwrap();
let element = crate::to_element(&x).unwrap();
let _ = facet_xml::to_string(&x).unwrap();
let _ = facet_xml::to_string(&element).unwrap();
assert!(
element.attrs.contains_key("exists"),
"this attribute is not discarded"
);
assert_eq!(element.attrs["name"], "CONSTANT", "name is not discarded");
}
}