Skip to main content

rustnetconf_yang/
serialize.rs

1//! XML serialization helpers for YANG-generated types.
2//!
3//! Converts YANG-typed Rust structs into NETCONF-compatible XML for
4//! use with `edit_config()` and deserializes XML responses back into
5//! typed structs.
6
7use quick_xml::events::{BytesEnd, BytesStart, BytesText, Event};
8pub use quick_xml::Writer;
9pub use std::io::Cursor;
10
11/// Trait for types that can serialize their child fields into an XML writer.
12///
13/// Implemented by the code generator for all YANG structs (containers and list
14/// entries). This allows containers and list entries to embed their XML into a
15/// parent writer without creating a standalone document.
16pub trait WriteXmlFields {
17    /// Write this value's child elements into `writer`.
18    ///
19    /// Does **not** write the surrounding element start/end tags — the caller
20    /// is responsible for those. This makes it easy to compose nested XML.
21    fn write_xml_fields(&self, writer: &mut Writer<Cursor<Vec<u8>>>) -> Result<(), XmlError>;
22}
23
24/// Trait for types that can be serialized to NETCONF XML.
25///
26/// Implemented by the code generator for each YANG container/list.
27pub trait ToNetconfXml: WriteXmlFields {
28    /// The YANG module namespace URI.
29    fn namespace(&self) -> &str;
30
31    /// The root element name.
32    fn root_element(&self) -> &str;
33
34    /// Serialize this value to NETCONF-compatible XML.
35    fn to_xml(&self) -> Result<String, XmlError>;
36}
37
38/// Trait for types that can be deserialized from NETCONF XML.
39pub trait FromNetconfXml: Sized {
40    /// Deserialize from an XML string.
41    fn from_xml(xml: &str) -> Result<Self, XmlError>;
42}
43
44/// XML serialization/deserialization error.
45#[derive(Debug, thiserror::Error)]
46pub enum XmlError {
47    #[error("XML write error: {0}")]
48    Write(String),
49
50    #[error("XML parse error: {0}")]
51    Parse(String),
52
53    #[error("missing required field: {0}")]
54    MissingField(String),
55
56    #[error("invalid value for field '{field}': {message}")]
57    InvalidValue { field: String, message: String },
58}
59
60/// Helper to write an XML element with text content.
61pub fn write_text_element(
62    writer: &mut Writer<Cursor<Vec<u8>>>,
63    name: &str,
64    value: &str,
65) -> Result<(), XmlError> {
66    let start = BytesStart::new(name);
67    writer
68        .write_event(Event::Start(start))
69        .map_err(|e| XmlError::Write(e.to_string()))?;
70    writer
71        .write_event(Event::Text(BytesText::new(value)))
72        .map_err(|e| XmlError::Write(e.to_string()))?;
73    writer
74        .write_event(Event::End(BytesEnd::new(name)))
75        .map_err(|e| XmlError::Write(e.to_string()))?;
76    Ok(())
77}
78
79/// Helper to write an XML element with a namespace attribute.
80pub fn write_start_with_ns(
81    writer: &mut Writer<Cursor<Vec<u8>>>,
82    name: &str,
83    namespace: &str,
84) -> Result<(), XmlError> {
85    let mut start = BytesStart::new(name);
86    start.push_attribute(("xmlns", namespace));
87    writer
88        .write_event(Event::Start(start))
89        .map_err(|e| XmlError::Write(e.to_string()))?;
90    Ok(())
91}
92
93/// Helper to write a closing element.
94pub fn write_end(writer: &mut Writer<Cursor<Vec<u8>>>, name: &str) -> Result<(), XmlError> {
95    writer
96        .write_event(Event::End(BytesEnd::new(name)))
97        .map_err(|e| XmlError::Write(e.to_string()))?;
98    Ok(())
99}
100
101/// Write a named XML element whose content comes from a [`WriteXmlFields`] value.
102///
103/// Writes `<name>`, calls `value.write_xml_fields()`, then writes `</name>`.
104pub fn write_element_with_fields<T: WriteXmlFields>(
105    writer: &mut Writer<Cursor<Vec<u8>>>,
106    name: &str,
107    value: &T,
108) -> Result<(), XmlError> {
109    writer
110        .write_event(Event::Start(BytesStart::new(name)))
111        .map_err(|e| XmlError::Write(e.to_string()))?;
112    value.write_xml_fields(writer)?;
113    writer
114        .write_event(Event::End(BytesEnd::new(name)))
115        .map_err(|e| XmlError::Write(e.to_string()))?;
116    Ok(())
117}
118
119/// Create a new XML writer.
120pub fn new_writer() -> Writer<Cursor<Vec<u8>>> {
121    Writer::new_with_indent(Cursor::new(Vec::new()), b' ', 2)
122}
123
124/// Extract the XML string from a writer.
125pub fn finish_writer(writer: Writer<Cursor<Vec<u8>>>) -> Result<String, XmlError> {
126    let buf = writer.into_inner().into_inner();
127    String::from_utf8(buf).map_err(|e| XmlError::Write(e.to_string()))
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    #[test]
135    fn test_write_text_element() {
136        let mut writer = new_writer();
137        write_text_element(&mut writer, "host-name", "spine-01").unwrap();
138        let xml = finish_writer(writer).unwrap();
139        assert_eq!(xml, "<host-name>spine-01</host-name>");
140    }
141
142    #[test]
143    fn test_write_start_with_ns() {
144        let mut writer = new_writer();
145        write_start_with_ns(
146            &mut writer,
147            "interfaces",
148            "urn:ietf:params:xml:ns:yang:ietf-interfaces",
149        )
150        .unwrap();
151        write_end(&mut writer, "interfaces").unwrap();
152        let xml = finish_writer(writer).unwrap();
153        assert!(xml.contains("xmlns=\"urn:ietf:params:xml:ns:yang:ietf-interfaces\""));
154    }
155}