use std::fmt;
use std::io::Write;
#[cfg(feature = "serde")]
use serde::Serialize;
use crate::error::Result;
use crate::format::Format;
use crate::value::Value;
pub struct Encoder<W: Write> {
#[cfg_attr(
not(any(feature = "xml", feature = "binary", feature = "openstep")),
expect(dead_code, reason = "no codec is compiled in to consume the writer")
)]
writer: W,
format: Format,
indent: String,
}
impl<W: Write> Encoder<W> {
pub const fn new(writer: W) -> Self {
Self::for_format(writer, Format::Xml)
}
pub const fn for_format(writer: W, format: Format) -> Self {
Self {
writer,
format,
indent: String::new(),
}
}
pub const fn binary(writer: W) -> Self {
Self::for_format(writer, Format::Binary)
}
pub const fn automatic(writer: W) -> Self {
Self::for_format(writer, Format::Binary)
}
pub fn set_indent(&mut self, indent: impl Into<String>) {
self.indent = indent.into();
}
#[cfg(feature = "serde")]
pub fn encode<T>(&mut self, value: &T) -> Result<()>
where
T: Serialize + ?Sized,
{
let tree = crate::value::ser::to_value(value)?;
self.encode_value(&tree)
}
pub fn encode_value(&mut self, value: &Value) -> Result<()> {
match self.format {
Format::Xml => self.encode_xml(value),
Format::Binary => self.encode_binary(value),
Format::OpenStep | Format::GnuStep => self.encode_text(value),
}
}
#[cfg(feature = "xml")]
fn encode_xml(&mut self, value: &Value) -> Result<()> {
crate::xml::generator::generate(&mut self.writer, value, &self.indent)
}
#[cfg(not(feature = "xml"))]
fn encode_xml(&mut self, _value: &Value) -> Result<()> {
Err(crate::error::Error::FeatureDisabled {
format: Format::Xml,
})
}
#[cfg(feature = "binary")]
fn encode_binary(&mut self, value: &Value) -> Result<()> {
let document = crate::binary::generator::generate(value)?;
self.writer.write_all(&document)?;
Ok(())
}
#[cfg(not(feature = "binary"))]
fn encode_binary(&mut self, _value: &Value) -> Result<()> {
Err(crate::error::Error::FeatureDisabled {
format: Format::Binary,
})
}
#[cfg(feature = "openstep")]
fn encode_text(&mut self, value: &Value) -> Result<()> {
crate::text::generate(&mut self.writer, value, self.format, &self.indent)
}
#[cfg(not(feature = "openstep"))]
fn encode_text(&mut self, _value: &Value) -> Result<()> {
Err(crate::error::Error::FeatureDisabled {
format: self.format,
})
}
}
impl<W: Write> fmt::Debug for Encoder<W> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Encoder")
.field("format", &self.format)
.field("indent", &self.indent)
.finish_non_exhaustive()
}
}
#[cfg(feature = "serde")]
pub fn to_vec<T: Serialize>(value: &T, format: Format) -> Result<Vec<u8>> {
to_vec_indent(value, format, "")
}
#[cfg(feature = "serde")]
pub fn to_vec_indent<T: Serialize>(value: &T, format: Format, indent: &str) -> Result<Vec<u8>> {
let mut buffer = Vec::new();
let mut encoder = Encoder::for_format(&mut buffer, format);
encoder.set_indent(indent);
encoder.encode(value)?;
Ok(buffer)
}
#[cfg(feature = "serde")]
pub fn to_writer<W: Write, T: Serialize>(writer: W, value: &T, format: Format) -> Result<()> {
Encoder::for_format(writer, format).encode(value)
}
#[cfg(test)]
mod tests {
use super::*;
fn sample() -> Value {
Value::from_iter([
("name".to_owned(), Value::from("plist")),
("count".to_owned(), Value::from(3_u8)),
])
}
#[test]
fn debug_elides_the_writer() {
let encoder = Encoder::new(Vec::new());
let rendered = format!("{encoder:?}");
assert!(rendered.starts_with("Encoder"));
assert!(rendered.contains("Xml"));
}
#[cfg(all(feature = "xml", feature = "binary", feature = "openstep"))]
mod all_codecs {
#![expect(clippy::unwrap_used, reason = "test code: unwrap is the assertion")]
use super::*;
use crate::error::Error;
#[test]
fn new_defaults_to_xml_and_automatic_matches_binary() {
let mut xml = Vec::new();
Encoder::new(&mut xml).encode_value(&sample()).unwrap();
assert!(xml.starts_with(b"<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n"));
let mut automatic = Vec::new();
Encoder::automatic(&mut automatic)
.encode_value(&sample())
.unwrap();
let mut binary = Vec::new();
Encoder::binary(&mut binary)
.encode_value(&sample())
.unwrap();
assert_eq!(automatic, binary);
assert!(binary.starts_with(b"bplist00"));
}
#[test]
fn indent_state_persists_and_is_reapplied_every_encode() {
let mut out = Vec::new();
let mut encoder = Encoder::for_format(&mut out, Format::OpenStep);
encoder.encode_value(&sample()).unwrap();
encoder.set_indent("\t");
encoder.encode_value(&sample()).unwrap();
encoder.set_indent("");
encoder.encode_value(&sample()).unwrap();
let compact = "{name=plist;count=3;}";
let pretty = "{\n\tname = plist;\n\tcount = 3;\n}";
assert_eq!(out, format!("{compact}{pretty}{compact}").into_bytes());
}
#[test]
fn binary_ignores_indent() {
let mut plain = Vec::new();
Encoder::binary(&mut plain).encode_value(&sample()).unwrap();
let mut indented = Vec::new();
let mut encoder = Encoder::binary(&mut indented);
encoder.set_indent("\t");
encoder.encode_value(&sample()).unwrap();
assert_eq!(plain, indented);
}
#[test]
fn repeated_encodes_append_complete_documents() {
let mut out = Vec::new();
let mut encoder = Encoder::new(&mut out);
encoder.encode_value(&Value::from(true)).unwrap();
encoder.encode_value(&Value::from(false)).unwrap();
let text = String::from_utf8(out).unwrap();
assert_eq!(text.matches("<?xml").count(), 2);
assert!(text.ends_with("<false/></plist>"));
}
#[test]
fn failing_writers_surface_io_for_every_format() {
struct FailingWriter;
impl Write for FailingWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("sink failure"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
for format in [
Format::Xml,
Format::Binary,
Format::OpenStep,
Format::GnuStep,
] {
let result = Encoder::for_format(FailingWriter, format).encode_value(&sample());
assert!(matches!(result, Err(Error::Io(_))), "{format}");
}
}
#[cfg(feature = "serde")]
#[test]
fn to_vec_equals_to_vec_indent_with_empty_indent() {
for format in [
Format::Xml,
Format::Binary,
Format::OpenStep,
Format::GnuStep,
] {
assert_eq!(
to_vec(&3_u8, format).unwrap(),
to_vec_indent(&3_u8, format, "").unwrap(),
"{format}"
);
}
}
#[cfg(feature = "serde")]
#[test]
fn nil_roots_fail_before_any_byte_is_written() {
struct PanickyWriter;
impl Write for PanickyWriter {
fn write(&mut self, _: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("must not be reached"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let result = Encoder::new(PanickyWriter).encode(&Option::<i32>::None);
assert!(matches!(result, Err(Error::NoRootElement)));
}
#[cfg(feature = "serde")]
#[test]
fn astral_runes_encode_in_every_format() {
let value = "grin 😀 end";
for format in [
Format::Xml,
Format::Binary,
Format::OpenStep,
Format::GnuStep,
] {
let bytes = to_vec(&value, format).unwrap();
assert!(!bytes.is_empty(), "{format}");
}
let xml: String = crate::de::from_slice(&to_vec(&value, Format::Xml).unwrap()).unwrap();
assert_eq!(xml, value);
let binary: String =
crate::de::from_slice(&to_vec(&value, Format::Binary).unwrap()).unwrap();
assert_eq!(binary, value);
}
}
}