Create XML Writer that accepts any Write trait

I have a little XML writing helper. It writes to an inner impl Write. But how do I make it accept either fmt::Write or io::Write?

Usage:

let mut string = String::new(); // what if I want this to be a File?
let mut xml = Xml::new(&mut string);
xml.open_element("html")?;
xml.text("hi")?;
xml.close_element()?;
println!("{}", string); // prints <html>hi</html>
pub(crate) struct Xml<'a, W>
where
    W: Write,
{
    writer: &'a mut W,
    elements: Vec<&'static str>,
    in_tag: bool,
}

impl<'a, W> Xml<'a, W>
where
    W: Write,
{
    pub fn new(writer: &'a mut W) -> Self {
        Self {
            writer,
            elements: Vec::new(),
            in_tag: false,
        }
    }
}

impl<W> Xml<'_, W>
where
    W: Write,
{
    pub fn open_element(&mut self, name: &'static str) -> Result {
        if self.in_tag {
            writeln!(self.writer, ">")?;
        } else {
            self.in_tag = true;
        }
        self.elements.push(name);
        write!(self.writer, "<{}", name)
    }

    pub fn attribute(&mut self, name: &'static str, value: impl Display) -> Result {
        write!(self.writer, r#" {}="{}""#, name, value)
    }

    pub fn close_element(&mut self) -> Result {
        let name = self.elements.pop().unwrap();
        if self.in_tag {
            self.in_tag = false;
            writeln!(self.writer, "/>")
        } else {
            writeln!(self.writer, "</{}>", name)
        }
    }

    pub fn text(&mut self, text: impl Display) -> Result {
        if self.in_tag {
            write!(self.writer, ">")?;
            self.in_tag = false;
        }
        write!(self.writer, "{}", text)
    }
}

You could create a wrapper that takes an io::Write type and produces a fmt::Write type:

pub struct WriteShim<T>(pub T);

impl<T> fmt::Write for WriteShim<T> where T: io::Write {
    fn write_str(&mut self, s: &str) -> Result<(), fmt::Error> {
        self.0.write_all(s.as_bytes()).or(Err(fmt::Error))
    }
}

This would be used like this:

let mut file = WriteShim(File::create("/tmp/foo")?);
let mut xml = Xml::new(&mut file);

This doesn't feel ideal, but as far as I know there isn't a great built-in way to do this in the standard library.

Right. With that approach, I think it would be a little better to shim the other way around so as to not drop the io errors which are more specific. But this approach still feels hacky.

I came up with another interesting workaround: I can take a lambda which generates xml and turn it into a Display.

pub(crate) fn xml_display(f: impl Fn(&mut Xml<'_, Formatter<'_>>) -> Result) -> impl Display {
    struct D<F: Fn(&mut Xml<'_, Formatter<'_>>) -> Result>(F);
    impl<F: Fn(&mut Xml<'_, Formatter<'_>>) -> Result> Display for D<F> {
        fn fmt(&self, fmt: &mut Formatter<'_>) -> Result {
            let mut xml = Xml::new(fmt);
            (self.0)(&mut xml)
        }
    }
    D(f)
}

fn usage(file: &File) -> std::io::Result<()> {
    let display = xml_display(|xml| {
        xml.open_element("html")?;
        // more xml fun here...
        Ok(())
    });
    write!(file, "{}", display)?;
    Ok(())
}

Also, this shim already exists behind the scenes in std (source). I wonder if it should be public.

Edit: No it should not be public (at least as is) because it requires the error to be extracted in a non-obvious way.

1 Like

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.