use std::cell::Cell;
use std::collections::HashMap;
use std::fmt;
use std::io::Write;
use serde::ser::{Impossible, Serialize, SerializeSeq, SerializeStruct, Serializer};
use serde_json::Value;
use crate::drive::{self, Steps};
use crate::error::Error;
use crate::finite::find_non_finite;
use crate::format::{self, FieldsLayout, Format};
use crate::markdown::Markdown;
use crate::parse::{
EMPTY_STRING_SECTION, Fields, SEPARATOR, check_middle_section, may_be_list, parse,
starts_with_dash_rule, would_read_fields,
};
pub fn to_string<T: Serialize + Markdown>(value: &T) -> Result<String, Error> {
to_string_with(value, Format::Yaml, FieldsLayout::Fenced)
}
pub fn to_string_with_format<T: Serialize + Markdown>(
value: &T,
format: Format,
) -> Result<String, Error> {
to_string_with(value, format, FieldsLayout::Fenced)
}
pub fn to_string_with<T: Serialize + Markdown>(
value: &T,
format: Format,
layout: FieldsLayout,
) -> Result<String, Error> {
drive::run(WriteDoc::new(value, format, layout))
}
pub(crate) struct WriteDoc<'a, T: ?Sized> {
value: &'a T,
body_fields: &'static [&'static str],
format: Format,
layout: FieldsLayout,
stage: Stage,
}
enum Stage {
Capture,
Front(Captured),
Sections {
front: Option<String>,
slots: std::vec::IntoIter<BodyValue>,
texts: Vec<String>,
},
Assemble {
front: Option<String>,
texts: Vec<String>,
},
Done,
}
impl<'a, T: Serialize + Markdown> WriteDoc<'a, T> {
pub(crate) fn new(value: &'a T, format: Format, layout: FieldsLayout) -> Self {
Self {
value,
body_fields: T::BODY_FIELDS,
format,
layout,
stage: Stage::Capture,
}
}
}
impl<T: Serialize + ?Sized> Steps for WriteDoc<'_, T> {
type Output = String;
fn step(&mut self) -> Result<Option<String>, Error> {
match std::mem::replace(&mut self.stage, Stage::Done) {
Stage::Capture => {
let captured = self.value.serialize(RootSerializer {
body_fields: self.body_fields,
format: self.format,
})?;
self.stage = Stage::Front(captured);
}
Stage::Front(captured) => {
let front = if captured.front_len > 0 {
let front = FrontMatter {
value: self.value,
body_fields: self.body_fields,
len: captured.front_len,
};
if let Some(number) = find_non_finite(&front) {
return Err(Error::front_matter_message(format_args!(
"the number {number} is not finite, which a fields block cannot hold"
)));
}
Some(format::dump(&front, self.format)?)
} else {
None
};
let slots = slots(captured.body, self.body_fields);
self.stage = Stage::Sections {
front,
texts: Vec::with_capacity(slots.len()),
slots: slots.into_iter(),
};
}
Stage::Sections {
front,
mut slots,
mut texts,
} => {
self.stage = match slots.next() {
Some(slot) => {
let last = texts.len() + 1 == self.body_fields.len();
texts.push(section_text(slot, last)?);
Stage::Sections {
front,
slots,
texts,
}
}
None => Stage::Assemble { front, texts },
};
}
Stage::Assemble { front, texts } => {
return render(
front,
texts,
self.body_fields.len(),
self.format,
self.layout,
)
.map(Some);
}
Stage::Done => unreachable!("a finished job is not stepped again"),
}
Ok(None)
}
}
pub fn to_vec<T: Serialize + Markdown>(value: &T) -> Result<Vec<u8>, Error> {
to_string(value).map(String::into_bytes)
}
pub fn to_writer<W, T>(mut writer: W, value: &T) -> Result<(), Error>
where
W: Write,
T: Serialize + Markdown,
{
writer.write_all(to_string(value)?.as_bytes())?;
Ok(())
}
struct Captured {
front_len: usize,
body: HashMap<&'static str, BodyValue>,
}
enum BodyValue {
Null,
Text {
text: String,
optional: bool,
},
Dumped(String),
}
struct RootSerializer {
body_fields: &'static [&'static str],
format: Format,
}
fn not_named_struct() -> Error {
Error::type_error("root must be a named struct")
}
fn deny_root<T>() -> Result<T, Error> {
Err(not_named_struct())
}
impl Serializer for RootSerializer {
type Ok = Captured;
type Error = Error;
type SerializeSeq = Impossible<Captured, Error>;
type SerializeTuple = Impossible<Captured, Error>;
type SerializeTupleStruct = Impossible<Captured, Error>;
type SerializeTupleVariant = Impossible<Captured, Error>;
type SerializeMap = Impossible<Captured, Error>;
type SerializeStruct = StructSerializer;
type SerializeStructVariant = Impossible<Captured, Error>;
fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_u8(self, _v: u8) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_u32(self, _v: u32) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_u64(self, _v: u64) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_char(self, _v: char) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_str(self, _v: &str) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok, Self::Error> {
deny_root()
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
Err(not_named_struct())
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(not_named_struct())
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(not_named_struct())
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Err(not_named_struct())
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(not_named_struct())
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Err(not_named_struct())
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
Ok(StructSerializer {
body_fields: self.body_fields,
format: self.format,
front_len: 0,
body: HashMap::new(),
seen_body: Vec::new(),
})
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(not_named_struct())
}
}
struct StructSerializer {
body_fields: &'static [&'static str],
format: Format,
front_len: usize,
body: HashMap<&'static str, BodyValue>,
seen_body: Vec<&'static str>,
}
impl SerializeStruct for StructSerializer {
type Ok = Captured;
type Error = Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
if self.body_fields.contains(&key) {
self.seen_body.push(key);
let captured = capture_body_field(value, self.format)?;
self.body.insert(key, captured);
} else {
self.front_len += 1;
}
Ok(())
}
fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> {
if self.body_fields.contains(&key) {
self.seen_body.push(key);
}
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
if let Some(name) = self
.body_fields
.iter()
.find(|name| !self.seen_body.contains(name))
{
return Err(Error::type_error(format!(
"the body field `{name}` is not a field of the value being written"
)));
}
Ok(Captured {
front_len: self.front_len,
body: self.body,
})
}
}
struct FrontMatter<'a, T: ?Sized> {
value: &'a T,
body_fields: &'static [&'static str],
len: usize,
}
impl<T: Serialize + ?Sized> Serialize for FrontMatter<'_, T> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
self.value.serialize(FrontOnly {
inner: serializer,
body_fields: self.body_fields,
len: self.len,
})
}
}
struct FrontOnly<S> {
inner: S,
body_fields: &'static [&'static str],
len: usize,
}
struct FrontStruct<S> {
inner: S,
body_fields: &'static [&'static str],
}
impl<S: SerializeStruct> SerializeStruct for FrontStruct<S> {
type Ok = S::Ok;
type Error = S::Error;
fn serialize_field<T: ?Sized + Serialize>(
&mut self,
key: &'static str,
value: &T,
) -> Result<(), Self::Error> {
if self.body_fields.contains(&key) {
return Ok(());
}
self.inner.serialize_field(key, value)
}
fn skip_field(&mut self, key: &'static str) -> Result<(), Self::Error> {
self.inner.skip_field(key)
}
fn end(self) -> Result<Self::Ok, Self::Error> {
self.inner.end()
}
}
macro_rules! front_denies {
($($method:ident($ty:ty)),* $(,)?) => {
$(
fn $method(self, _v: $ty) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
)*
};
}
fn front_not_struct<E: serde::ser::Error>() -> E {
E::custom("root must be a named struct")
}
impl<S: Serializer> Serializer for FrontOnly<S> {
type Ok = S::Ok;
type Error = S::Error;
type SerializeSeq = Impossible<S::Ok, S::Error>;
type SerializeTuple = Impossible<S::Ok, S::Error>;
type SerializeTupleStruct = Impossible<S::Ok, S::Error>;
type SerializeTupleVariant = Impossible<S::Ok, S::Error>;
type SerializeMap = Impossible<S::Ok, S::Error>;
type SerializeStruct = FrontStruct<S::SerializeStruct>;
type SerializeStructVariant = Impossible<S::Ok, S::Error>;
front_denies! {
serialize_bool(bool),
serialize_i8(i8),
serialize_i16(i16),
serialize_i32(i32),
serialize_i64(i64),
serialize_u8(u8),
serialize_u16(u16),
serialize_u32(u32),
serialize_u64(u64),
serialize_f32(f32),
serialize_f64(f64),
serialize_char(char),
serialize_str(&str),
serialize_bytes(&[u8]),
serialize_unit_struct(&'static str),
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
Err(front_not_struct())
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Err(front_not_struct())
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
Err(front_not_struct())
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
Err(front_not_struct())
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
Err(front_not_struct())
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
Err(front_not_struct())
}
fn serialize_struct(
self,
name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
let inner = self.inner.serialize_struct(name, self.len)?;
Ok(FrontStruct {
inner,
body_fields: self.body_fields,
})
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
Err(front_not_struct())
}
fn is_human_readable(&self) -> bool {
self.inner.is_human_readable()
}
}
fn slots(
mut body: HashMap<&'static str, BodyValue>,
body_fields: &[&'static str],
) -> Vec<BodyValue> {
let mut slots: Vec<BodyValue> = body_fields
.iter()
.map(|name| body.remove(name).unwrap_or(BodyValue::Null))
.collect();
while slots
.pop_if(|slot| matches!(slot, BodyValue::Null))
.is_some()
{}
slots
}
fn render(
front: Option<String>,
texts: Vec<String>,
count: usize,
format: Format,
layout: FieldsLayout,
) -> Result<String, Error> {
let front = front.filter(|dumped| layout != FieldsLayout::Bare || !dumped.trim().is_empty());
let after_fields = front.is_some();
let separated = after_fields && layout == FieldsLayout::Bare;
let mut region = String::new();
if count == 1 {
if let Some(text) = texts.first() {
if !separated && (text.is_empty() || starts_with_dash_rule(text)) {
region.push_str("---\n");
}
region.push_str(text);
}
} else {
for (i, text) in texts.iter().enumerate() {
if i == 1 && after_fields && texts[0].is_empty() {
region.push_str("---\n");
} else if i > 0 {
region.push_str(SEPARATOR);
}
region.push_str(text);
}
if texts.len() < count
&& let Some(last) = texts.last()
&& last.is_empty()
{
region.push_str(if texts.len() == 1 && after_fields {
"---\n"
} else {
SEPARATOR
});
}
}
let mut out = String::new();
match front {
Some(dumped) => {
out.push_str(&fields_block(dumped, format, layout)?);
if separated && !texts.is_empty() {
out.push_str("---\n");
}
}
None => {
if !region.is_empty() && would_read_fields(®ion, count)? {
if !format_enabled(format) {
return Err(Error::format_disabled(format));
}
out.push_str(empty_fields_block(format));
}
}
}
out.push_str(®ion);
Ok(out)
}
fn format_enabled(format: Format) -> bool {
match format {
Format::Yaml => cfg!(feature = "yaml"),
Format::Json => cfg!(feature = "json"),
Format::Toml => cfg!(feature = "toml"),
}
}
fn fields_block(dumped: String, format: Format, layout: FieldsLayout) -> Result<String, Error> {
let mut text = dumped;
if !text.is_empty() && !text.ends_with('\n') {
text.push('\n');
}
let (block, probe) = match layout {
FieldsLayout::Fenced => {
let ticks = "`".repeat(fence_len(&text));
let block = format!("{ticks}{}\n{text}{ticks}\n", format.as_str());
(block.clone(), block)
}
FieldsLayout::Bare => (text.clone(), format!("{text}---\n")),
};
let reads_back = match parse(&probe, 0).map(|doc| doc.fields) {
Ok(Fields::Fenced {
labeled: Some(lang),
inner,
..
}) => layout == FieldsLayout::Fenced && lang.format() == format && inner == text,
Ok(Fields::Bare { sniff, inner, .. }) => {
layout == FieldsLayout::Bare && sniff.format() == format && inner == text
}
Ok(Fields::Fenced { labeled: None, .. } | Fields::None) | Err(_) => false,
};
if !reads_back {
return Err(Error::front_matter_message(match layout {
FieldsLayout::Fenced => "the fields block would not read back as written".to_owned(),
FieldsLayout::Bare => "the fields block would not read back as written; \
write it with FieldsLayout::Fenced"
.to_owned(),
}));
}
format::load(&text, format)?;
Ok(block)
}
fn fence_len(text: &str) -> usize {
let longest = text
.lines()
.map(|line| {
line.trim_start_matches([' ', '\t'])
.bytes()
.take_while(|byte| *byte == b'`')
.count()
})
.max()
.unwrap_or(0);
(longest + 1).max(3)
}
fn empty_fields_block(format: Format) -> &'static str {
match format {
Format::Yaml => "```yaml\n{}\n```\n",
Format::Json => "```json\n{}\n```\n",
Format::Toml => "```toml\n```\n",
}
}
fn section_text(value: BodyValue, last: bool) -> Result<String, Error> {
let text = match value {
BodyValue::Null => return Ok(String::new()),
BodyValue::Dumped(dumped) if last => return Ok(dumped),
BodyValue::Dumped(mut dumped) => {
if dumped.ends_with('\n') {
dumped.pop();
}
dumped
}
BodyValue::Text { text, .. } if last => return Ok(text),
BodyValue::Text { text, optional } => {
if text.is_empty() {
return Ok(if optional {
EMPTY_STRING_SECTION.to_owned()
} else {
String::new()
});
}
if optional && text == EMPTY_STRING_SECTION {
return Err(Error::body(
"an optional body value that is not the last cannot be the two \
characters \"\"; they stand for an empty value there",
));
}
text
}
};
check_middle_section(&text)?;
Ok(text)
}
fn capture_body_field<T: ?Sized + Serialize>(
value: &T,
format: Format,
) -> Result<BodyValue, Error> {
let some = Cell::new(false);
match value.serialize(BodyProbe { some: &some }) {
Ok(Probed::Null) => Ok(BodyValue::Null),
Ok(Probed::Text { text, from_seq }) => {
if from_seq && reads_as_empty_list(&text, format) {
return Err(Error::body(
"a byte body field whose text reads as an empty list cannot be told \
from an empty list on read",
));
}
Ok(BodyValue::Text {
text,
optional: some.get(),
})
}
Err(ProbeError::NotText) => {
if let Some(number) = find_non_finite(value) {
return Err(Error::body(format_args!(
"the number {number} is not finite, which a structured section cannot hold"
)));
}
Ok(BodyValue::Dumped(format::dump(value, format)?))
}
Err(ProbeError::InvalidUtf8) => Err(Error::body("invalid UTF-8 in byte body field")),
Err(ProbeError::Other(err)) => Err(err),
}
}
fn reads_as_empty_list(text: &str, format: Format) -> bool {
if !may_be_list(text) {
return false;
}
[format, Format::Yaml].into_iter().any(|lang| {
matches!(format::load_body(text, lang), Ok(Value::Array(values)) if values.is_empty())
})
}
enum Probed {
Null,
Text {
text: String,
from_seq: bool,
},
}
enum ProbeError {
NotText,
InvalidUtf8,
Other(Error),
}
impl fmt::Debug for ProbeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotText => f.write_str("NotText"),
Self::InvalidUtf8 => f.write_str("InvalidUtf8"),
Self::Other(err) => fmt::Debug::fmt(err, f),
}
}
}
impl fmt::Display for ProbeError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::NotText => f.write_str("not a text body field"),
Self::InvalidUtf8 => f.write_str("invalid UTF-8 in byte body field"),
Self::Other(err) => fmt::Display::fmt(err, f),
}
}
}
impl std::error::Error for ProbeError {}
impl serde::ser::Error for ProbeError {
fn custom<T: fmt::Display>(msg: T) -> Self {
Self::Other(Error::type_error(msg))
}
}
struct BodyProbe<'a> {
some: &'a Cell<bool>,
}
fn not_text<T>() -> Result<T, ProbeError> {
Err(ProbeError::NotText)
}
fn text(text: impl Into<String>) -> Result<Probed, ProbeError> {
Ok(Probed::Text {
text: text.into(),
from_seq: false,
})
}
impl Serializer for BodyProbe<'_> {
type Ok = Probed;
type Error = ProbeError;
type SerializeSeq = ProbeSeq;
type SerializeTuple = Impossible<Probed, ProbeError>;
type SerializeTupleStruct = Impossible<Probed, ProbeError>;
type SerializeTupleVariant = Impossible<Probed, ProbeError>;
type SerializeMap = Impossible<Probed, ProbeError>;
type SerializeStruct = Impossible<Probed, ProbeError>;
type SerializeStructVariant = Impossible<Probed, ProbeError>;
fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u8(self, _v: u8) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u32(self, _v: u32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u64(self, _v: u64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_char(self, v: char) -> Result<Self::Ok, Self::Error> {
text(v)
}
fn serialize_str(self, v: &str) -> Result<Self::Ok, Self::Error> {
text(v)
}
fn serialize_bytes(self, v: &[u8]) -> Result<Self::Ok, Self::Error> {
std::str::from_utf8(v)
.map_err(|_| ProbeError::InvalidUtf8)
.and_then(text)
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
Ok(Probed::Null)
}
fn serialize_some<T: ?Sized + Serialize>(self, value: &T) -> Result<Self::Ok, Self::Error> {
self.some.set(true);
value.serialize(self)
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
Ok(Probed::Null)
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
Ok(Probed::Null)
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
text(variant)
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
value: &T,
) -> Result<Self::Ok, Self::Error> {
value.serialize(self)
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_seq(self, len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
Ok(ProbeSeq {
buf: Vec::with_capacity(len.unwrap_or(0)),
})
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
not_text()
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
not_text()
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
not_text()
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
not_text()
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
not_text()
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
not_text()
}
}
struct ProbeSeq {
buf: Vec<u8>,
}
struct U8Element;
impl Serializer for U8Element {
type Ok = u8;
type Error = ProbeError;
type SerializeSeq = Impossible<u8, ProbeError>;
type SerializeTuple = Impossible<u8, ProbeError>;
type SerializeTupleStruct = Impossible<u8, ProbeError>;
type SerializeTupleVariant = Impossible<u8, ProbeError>;
type SerializeMap = Impossible<u8, ProbeError>;
type SerializeStruct = Impossible<u8, ProbeError>;
type SerializeStructVariant = Impossible<u8, ProbeError>;
fn serialize_u8(self, v: u8) -> Result<Self::Ok, Self::Error> {
Ok(v)
}
fn serialize_bool(self, _v: bool) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i8(self, _v: i8) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i16(self, _v: i16) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i32(self, _v: i32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_i64(self, _v: i64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u16(self, _v: u16) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u32(self, _v: u32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_u64(self, _v: u64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_f32(self, _v: f32) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_f64(self, _v: f64) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_char(self, _v: char) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_str(self, _v: &str) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_bytes(self, _v: &[u8]) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_none(self) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_some<T: ?Sized + Serialize>(self, _value: &T) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_unit(self) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_unit_struct(self, _name: &'static str) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_unit_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_newtype_struct<T: ?Sized + Serialize>(
self,
_name: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_newtype_variant<T: ?Sized + Serialize>(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_value: &T,
) -> Result<Self::Ok, Self::Error> {
not_text()
}
fn serialize_seq(self, _len: Option<usize>) -> Result<Self::SerializeSeq, Self::Error> {
not_text()
}
fn serialize_tuple(self, _len: usize) -> Result<Self::SerializeTuple, Self::Error> {
not_text()
}
fn serialize_tuple_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleStruct, Self::Error> {
not_text()
}
fn serialize_tuple_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeTupleVariant, Self::Error> {
not_text()
}
fn serialize_map(self, _len: Option<usize>) -> Result<Self::SerializeMap, Self::Error> {
not_text()
}
fn serialize_struct(
self,
_name: &'static str,
_len: usize,
) -> Result<Self::SerializeStruct, Self::Error> {
not_text()
}
fn serialize_struct_variant(
self,
_name: &'static str,
_variant_index: u32,
_variant: &'static str,
_len: usize,
) -> Result<Self::SerializeStructVariant, Self::Error> {
not_text()
}
}
impl SerializeSeq for ProbeSeq {
type Ok = Probed;
type Error = ProbeError;
fn serialize_element<T: ?Sized + Serialize>(&mut self, value: &T) -> Result<(), Self::Error> {
self.buf.push(value.serialize(U8Element)?);
Ok(())
}
fn end(self) -> Result<Self::Ok, Self::Error> {
if self.buf.is_empty() {
return Err(ProbeError::NotText);
}
String::from_utf8(self.buf)
.map(|text| Probed::Text {
text,
from_seq: true,
})
.map_err(|_| ProbeError::InvalidUtf8)
}
}
#[cfg(test)]
mod tests {
use serde::Serialize;
#[cfg(any(feature = "yaml", feature = "json", feature = "toml"))]
use super::to_string_with;
use super::{to_string, to_string_with_format, to_vec, to_writer};
use crate::error::ErrorKind;
#[cfg(any(feature = "yaml", feature = "json", feature = "toml"))]
use crate::format::FieldsLayout;
use crate::format::Format;
use crate::markdown::Markdown;
#[cfg(feature = "yaml")]
use crate::testdata::goldens;
#[cfg(any(feature = "yaml", feature = "json", feature = "toml"))]
use crate::testdata::types;
use crate::testdata::values;
#[derive(Serialize)]
struct TupleRoot(i32);
impl Markdown for TupleRoot {
const BODY_FIELDS: &'static [&'static str] = &[];
}
#[derive(Serialize)]
struct UnitRoot;
impl Markdown for UnitRoot {
const BODY_FIELDS: &'static [&'static str] = &[];
}
#[cfg(feature = "yaml")]
#[test]
fn page_round_trip() {
let page = values::page();
let md = to_string(&page).expect("serialize");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(page, back);
}
#[cfg(feature = "yaml")]
#[test]
fn serialize_is_labeled_yaml_fenced() {
let md = to_string(&values::page()).expect("serialize");
assert!(md.starts_with("```yaml\n"), "{md}");
assert!(!md.starts_with('\n'), "{md}");
assert!(
md.ends_with("```\nText1 bla bla bla\n---\nText2 bal bla bla"),
"{md}"
);
assert!(!md.contains("```\n\n"), "{md}");
assert!(!md.contains("```yml"), "{md}");
assert!(md.contains("field1: foo"), "{md}");
assert!(md.contains("field2: bar"), "{md}");
assert!(md.contains("field3: 1"), "{md}");
assert!(md.contains("Text1 bla bla bla"), "{md}");
assert!(md.contains("Text2 bal bla bla"), "{md}");
assert!(
!md.contains("\"Text1 bla bla bla\""),
"body strings must be raw, got {md}"
);
let trimmed = md.trim_end();
assert!(!trimmed.ends_with("---"), "{md}");
}
#[cfg(feature = "yaml")]
#[test]
fn fields_only_omits_body() {
let md = to_string(&values::fields_only()).expect("serialize");
assert!(md.starts_with("```yaml\n"), "{md}");
assert!(md.contains("name: only"), "{md}");
assert!(md.contains("count: 2"), "{md}");
assert!(!md.contains("---"), "{md}");
assert!(!md.contains("Text1"), "{md}");
let back: types::FieldsOnly = crate::from_str(&md).expect("deserialize");
assert_eq!(values::fields_only(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn deserialize_fenced_yaml_golden() {
let page: types::Page =
crate::from_str(goldens::PAGE_FENCED_YAML).expect("page.fenced.yaml.md");
assert_eq!(page, values::page());
}
#[cfg(feature = "json")]
#[test]
fn serialize_json_is_labeled_pretty_fence() {
let md = to_string_with_format(&values::page(), Format::Json).expect("serialize");
assert!(md.starts_with("```json\n"), "{md}");
assert!(md.contains("{\n \"field1\": \"foo\""), "{md}");
assert!(md.contains(" \"field2\": \"bar\""), "{md}");
assert!(md.contains(" \"field3\": 1"), "{md}");
assert!(md.contains("```\nText1 bla bla bla\n---\n"), "{md}");
assert!(!md.contains("```\n\n"), "{md}");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(values::page(), back);
}
#[cfg(feature = "toml")]
#[test]
fn serialize_toml_is_labeled_pretty_fence() {
let md = to_string_with_format(&values::page(), Format::Toml).expect("serialize");
assert!(md.starts_with("```toml\n"), "{md}");
assert!(md.contains("field1 = \"foo\""), "{md}");
assert!(md.contains("field2 = \"bar\""), "{md}");
assert!(md.contains("field3 = 1"), "{md}");
assert!(md.contains("```\nText1 bla bla bla\n---\n"), "{md}");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(values::page(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn serialize_bare_yaml_has_no_fence() {
let md =
to_string_with(&values::page(), Format::Yaml, FieldsLayout::Bare).expect("serialize");
assert!(!md.contains("```"), "{md}");
assert!(!md.starts_with('\n'), "{md}");
assert!(
md.ends_with("---\nText1 bla bla bla\n---\nText2 bal bla bla"),
"{md}"
);
assert!(md.contains("field1: foo"), "{md}");
assert!(md.contains("field2: bar"), "{md}");
assert!(md.contains("field3: 1"), "{md}");
let trimmed = md.trim_end();
assert!(!trimmed.ends_with("---"), "{md}");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(values::page(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn bare_yaml_with_a_fenced_string_after_a_list_reads_back() {
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
struct Artifact {
path: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, serde::Deserialize)]
struct Reason {
text: String,
}
#[derive(
Debug,
Clone,
PartialEq,
Eq,
Serialize,
serde::Deserialize,
serde_markdown_derive::Markdown,
)]
struct Note {
artifacts: Vec<Artifact>,
reason: Reason,
#[markdown(body)]
claim: String,
}
let note = Note {
artifacts: vec![Artifact {
path: "rules/code-first.md".into(),
}],
reason: Reason {
text: "Behaviour ships first.\n\n```text\n# BAD\n# GOOD\n```".into(),
},
claim: "Keep one pass.".into(),
};
let md = to_string_with(¬e, Format::Yaml, FieldsLayout::Bare).expect("serialize");
assert!(md.contains("```text"), "{md}");
let back: Note = crate::from_str(&md).expect("deserialize");
assert_eq!(note, back);
}
#[cfg(feature = "json")]
#[test]
fn serialize_bare_json_separator_before_body() {
let md =
to_string_with(&values::page(), Format::Json, FieldsLayout::Bare).expect("serialize");
assert!(!md.contains("```"), "{md}");
assert!(md.contains("{\n \"field1\": \"foo\""), "{md}");
assert!(md.contains("}\n---\nText1 bla bla bla\n---\n"), "{md}");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(values::page(), back);
}
#[cfg(feature = "toml")]
#[test]
fn serialize_bare_toml_separator_before_body() {
let md =
to_string_with(&values::page(), Format::Toml, FieldsLayout::Bare).expect("serialize");
assert!(!md.contains("```"), "{md}");
assert!(md.contains("field1 = \"foo\""), "{md}");
assert!(md.contains("field3 = 1"), "{md}");
assert!(md.contains("---\nText1 bla bla bla\n---\n"), "{md}");
let back: types::Page = crate::from_str(&md).expect("deserialize");
assert_eq!(values::page(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn serialize_bare_fields_only_omits_separator() {
let md = to_string_with(&values::fields_only(), Format::Yaml, FieldsLayout::Bare)
.expect("serialize");
assert!(!md.contains("```"), "{md}");
assert!(md.contains("name: only"), "{md}");
assert!(md.contains("count: 2"), "{md}");
assert!(!md.contains("---"), "{md}");
let back: types::FieldsOnly = crate::from_str(&md).expect("deserialize");
assert_eq!(values::fields_only(), back);
}
#[cfg(feature = "yaml")]
fn assert_optional_round_trip(value: &types::OptionalBody, golden: &str) {
let from_golden: types::OptionalBody = crate::from_str(golden).expect("golden");
assert_eq!(*value, from_golden);
let md = to_string(value).expect("serialize");
let back: types::OptionalBody = crate::from_str(&md).expect("round-trip");
assert_eq!(*value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn optional_middle_none_keeps_empty_slot() {
let value = values::optional_middle_none();
assert_optional_round_trip(&value, goldens::OPTIONAL_MIDDLE_NONE);
let md = to_string(&value).expect("serialize");
assert!(md.ends_with("```\na\n---\n\n---\nc"), "{md}");
}
#[cfg(feature = "yaml")]
#[test]
fn optional_trailing_none_omits_separator() {
let value = values::optional_trailing_none();
assert_optional_round_trip(&value, goldens::OPTIONAL_TRAILING_NONE);
let md = to_string(&value).expect("serialize");
assert!(md.ends_with("```\na"), "{md}");
assert!(!md.contains("---"), "{md}");
let trimmed = md.trim_end();
assert!(!trimmed.ends_with("---"), "{md}");
}
#[cfg(feature = "yaml")]
#[test]
fn optional_leading_none_empty_first_section() {
let value = values::optional_leading_none();
assert_optional_round_trip(&value, goldens::OPTIONAL_LEADING_NONE);
let md = to_string(&value).expect("serialize");
assert!(md.ends_with("```\n---\na"), "{md}");
}
#[cfg(feature = "yaml")]
#[test]
fn optional_empty_string_writes_quotes() {
let value = values::optional_empty_string();
assert_optional_round_trip(&value, goldens::OPTIONAL_EMPTY_STRING);
let md = to_string(&value).expect("serialize");
assert!(md.ends_with("```\na\n---\n\"\""), "{md}");
}
#[cfg(feature = "yaml")]
#[test]
fn to_string_stays_fenced_yaml() {
let md = to_string(&values::page()).expect("serialize");
assert!(md.starts_with("```yaml\n"), "{md}");
let with_format = to_string_with_format(&values::page(), Format::Yaml).expect("yaml");
assert_eq!(md, with_format);
}
#[cfg(not(feature = "json"))]
#[test]
fn json_without_feature_is_format_disabled() {
let err = to_string_with_format(&values::page(), Format::Json).expect_err("disabled");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(not(feature = "toml"))]
#[test]
fn toml_without_feature_is_format_disabled() {
let err = to_string_with_format(&values::page(), Format::Toml).expect_err("disabled");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
}
#[test]
fn root_tuple_struct_is_type() {
let err = to_string(&TupleRoot(1)).expect_err("tuple root");
assert_eq!(err.kind(), ErrorKind::Type);
}
#[test]
fn root_unit_struct_is_type() {
let err = to_string(&UnitRoot).expect_err("unit root");
assert_eq!(err.kind(), ErrorKind::Type);
assert_eq!(err.to_string(), "root must be a named struct");
}
#[cfg(feature = "yaml")]
#[test]
fn nested_round_trip() {
let value = values::nested();
let from_golden: types::NestedFields =
crate::from_str(goldens::NESTED_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
assert!(md.contains("author: Ada"), "{md}");
assert!(md.contains("draft: true"), "{md}");
let back: types::NestedFields = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn structured_body_is_fence_format_dump() {
let value = values::article();
let from_golden: types::Article =
crate::from_str(goldens::STRUCTURED_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
assert!(md.contains("```yaml\n"), "{md}");
assert!(md.contains("title: Hello"), "{md}");
assert!(md.contains("heading: Intro"), "{md}");
assert!(md.contains("pages: 3"), "{md}");
let back: types::Article = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "json")]
#[test]
fn structured_json_round_trip() {
let value = values::article();
let md = to_string_with_format(&value, Format::Json).expect("serialize");
assert!(md.contains("```json\n"), "{md}");
assert!(md.contains("\"heading\": \"Intro\""), "{md}");
let back: types::Article = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn serde_rename_keys_in_markdown() {
let value = values::json_names();
let from_golden: types::JsonNames =
crate::from_str(goldens::NAMES_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
assert!(md.contains("publishedAt:"), "{md}");
assert!(!md.contains("published_at"), "{md}");
assert!(md.contains("A body note."), "{md}");
assert!(!md.contains("body_note"), "{md}");
assert!(
!md.contains("bodyNote:"),
"body field name must not appear as a key, got {md}"
);
let back: types::JsonNames = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn whitespace_round_trip_keeps_interiors() {
let value = values::whitespace_page();
let md = to_string(&value).expect("serialize");
assert!(md.contains("spaces \n"), "{md}");
assert!(md.contains("🦀"), "{md}");
let back: types::Page = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn well_known_round_trip() {
let value = values::well_known();
let from_golden: types::WellKnown =
crate::from_str(goldens::WELL_KNOWN_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
assert!(md.contains("published:"), "{md}");
assert!(md.contains(values::PUBLISHED_RFC3339), "{md}");
assert!(
md.contains("1.5s") || md.contains("1.500s"),
"Duration proto3 JSON (1.5s or 1.500s), got {md}"
);
assert!(md.contains("meta:"), "{md}");
assert!(md.contains("{}"), "empty Empty object, got {md}");
assert!(md.contains("lang:"), "{md}");
assert!(md.contains("a,b.c"), "{md}");
assert!(md.contains("count:"), "{md}");
assert!(md.contains("3"), "{md}");
assert!(
md.contains("```\n2026-09-17T02:42:00Z\n---\n"),
"Timestamp body must be raw RFC 3339 after the fence, got {md}"
);
assert!(
md.ends_with("---\n1.5s") || md.ends_with("---\n1.500s"),
"Duration body must be raw proto3 JSON, got {md}"
);
assert!(
!md.contains("\"2026-09-17T02:42:00Z\"\n---"),
"body Timestamp must not be a quoted JSON string, got {md}"
);
let back: types::WellKnown = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn well_known_generated_round_trip() {
let value = values::well_known_generated();
let from_golden: serde_markdown_proto::markdown::testdata::WellKnown =
crate::from_str(goldens::WELL_KNOWN_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::WellKnown =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn page_published_round_trip() {
let value = values::page_with_published();
let from_golden: types::Page =
crate::from_str(goldens::PAGE_PUBLISHED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
assert!(md.contains("published:"), "{md}");
assert!(md.contains(values::PUBLISHED_RFC3339), "{md}");
let back: types::Page = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn page_published_generated_round_trip() {
let value = values::page_generated_with_published();
let from_golden: serde_markdown_proto::markdown::testdata::Page =
crate::from_str(goldens::PAGE_PUBLISHED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::Page =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn page_generated_round_trip() {
let value = values::page_generated();
let from_golden: serde_markdown_proto::markdown::testdata::Page =
crate::from_str(goldens::PAGE_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::Page =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn proto3_page_generated_round_trip() {
let value = values::proto3_page_generated();
let from_golden: serde_markdown_proto::markdown::testdata::Proto3Page =
crate::from_str(goldens::PROTO3_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::Proto3Page =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn json_names_generated_round_trip() {
let value = values::json_names_generated();
let from_golden: serde_markdown_proto::markdown::testdata::JsonNames =
crate::from_str(goldens::NAMES_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::JsonNames =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn optional_body_generated_round_trip() {
let value = values::optional_middle_none_generated();
let from_golden: serde_markdown_proto::markdown::testdata::OptionalBody =
crate::from_str(goldens::OPTIONAL_MIDDLE_NONE).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::OptionalBody =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
let trailing = values::optional_trailing_none_generated();
let from_trailing: serde_markdown_proto::markdown::testdata::OptionalBody =
crate::from_str(goldens::OPTIONAL_TRAILING_NONE).expect("trailing");
assert_eq!(trailing, from_trailing);
}
#[cfg(feature = "yaml")]
#[test]
fn article_generated_round_trip() {
let value = values::article_generated();
let from_golden: serde_markdown_proto::markdown::testdata::Article =
crate::from_str(goldens::STRUCTURED_FENCED_YAML).expect("golden");
assert_eq!(value, from_golden);
let md = to_string(&value).expect("serialize");
let back: serde_markdown_proto::markdown::testdata::Article =
crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn to_vec_matches_to_string() {
let md = to_string(&values::page()).expect("serialize");
let bytes = to_vec(&values::page()).expect("to_vec");
assert_eq!(bytes, md.as_bytes());
let back: types::Page = crate::from_slice(&bytes).expect("from_slice");
assert_eq!(values::page(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn to_writer_matches_to_string() {
let md = to_string(&values::page()).expect("serialize");
let mut buf = Vec::new();
to_writer(&mut buf, &values::page()).expect("to_writer");
assert_eq!(buf, md.as_bytes());
let back: types::Page = crate::from_reader(buf.as_slice()).expect("from_reader");
assert_eq!(values::page(), back);
}
#[cfg(feature = "yaml")]
#[test]
fn to_writer_io_error_is_io() {
struct Boom;
impl std::io::Write for Boom {
fn write(&mut self, _buf: &[u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("boom"))
}
fn flush(&mut self) -> std::io::Result<()> {
Ok(())
}
}
let err = to_writer(Boom, &values::page()).expect_err("write");
assert_eq!(err.kind(), ErrorKind::Io);
assert!(std::error::Error::source(&err).is_some());
}
#[cfg(feature = "yaml")]
#[derive(Debug, PartialEq, serde::Deserialize, Serialize, serde_markdown_derive::Markdown)]
struct BytesDoc {
title: String,
#[markdown(body)]
data: Vec<u8>,
}
#[cfg(feature = "yaml")]
#[derive(Debug, PartialEq)]
struct ByteField(Vec<u8>);
#[cfg(feature = "yaml")]
impl Serialize for ByteField {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
serializer.serialize_bytes(&self.0)
}
}
#[cfg(feature = "yaml")]
impl<'de> serde::Deserialize<'de> for ByteField {
fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
struct Vis;
impl<'de> serde::de::Visitor<'de> for Vis {
type Value = ByteField;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("bytes")
}
fn visit_bytes<E: serde::de::Error>(self, v: &[u8]) -> Result<ByteField, E> {
Ok(ByteField(v.to_vec()))
}
fn visit_byte_buf<E: serde::de::Error>(self, v: Vec<u8>) -> Result<ByteField, E> {
Ok(ByteField(v))
}
}
deserializer.deserialize_byte_buf(Vis)
}
}
#[cfg(feature = "yaml")]
#[derive(Debug, PartialEq, serde::Deserialize, Serialize, serde_markdown_derive::Markdown)]
struct ByteBufDoc {
title: String,
#[markdown(body)]
data: ByteField,
}
#[cfg(feature = "yaml")]
#[test]
fn vec_u8_body_is_raw_utf8() {
let value = BytesDoc {
title: "bin".into(),
data: b"caf\xc3\xa9".to_vec(),
};
let md = to_string(&value).expect("serialize");
assert!(md.contains("```yaml\n"), "{md}");
assert!(md.contains("title: bin"), "{md}");
assert!(
md.ends_with("```\ncafé"),
"Vec<u8> body must be raw UTF-8, got {md}"
);
assert!(
!md.contains("99") && !md.contains("- 99"),
"Vec<u8> must not dump as a number array, got {md}"
);
let back: BytesDoc = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn serialize_bytes_body_is_raw_utf8() {
let value = ByteBufDoc {
title: "bin".into(),
data: ByteField(b"hello".to_vec()),
};
let md = to_string(&value).expect("serialize");
assert!(md.ends_with("```\nhello"), "{md}");
let back: ByteBufDoc = crate::from_str(&md).expect("round-trip");
assert_eq!(value, back);
}
#[cfg(feature = "yaml")]
#[test]
fn invalid_utf8_byte_body_is_body() {
let value = BytesDoc {
title: "bin".into(),
data: vec![0xff, b'a'],
};
let err = to_string(&value).expect_err("utf8");
assert_eq!(err.kind(), ErrorKind::Body);
assert!(std::error::Error::source(&err).is_none());
assert!(err.to_string().contains("UTF-8"), "{err}");
let via_bytes = ByteBufDoc {
title: "bin".into(),
data: ByteField(vec![0xff]),
};
let err = to_string(&via_bytes).expect_err("serialize_bytes utf8");
assert_eq!(err.kind(), ErrorKind::Body);
}
#[cfg(not(feature = "yaml"))]
#[test]
fn yaml_without_feature_is_format_disabled() {
let err = to_string(&values::page()).expect_err("disabled");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
let err = to_vec(&values::page()).expect_err("to_vec");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
let err = to_writer(std::io::sink(), &values::page()).expect_err("to_writer");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
}
#[cfg(all(not(feature = "yaml"), feature = "json"))]
#[test]
fn without_yaml_a_body_only_document_needs_no_fields_block() {
let md = to_string_with(&values::body_only(), Format::Json, FieldsLayout::Fenced)
.expect("serialize");
assert_eq!(md, crate::testdata::goldens::BODY_ONLY_TWO_SECTIONS);
}
#[cfg(not(all(feature = "yaml", feature = "json", feature = "toml")))]
#[derive(Serialize)]
struct OneBody {
text: String,
}
#[cfg(not(all(feature = "yaml", feature = "json", feature = "toml")))]
impl Markdown for OneBody {
const BODY_FIELDS: &'static [&'static str] = &["text"];
}
#[cfg(not(feature = "yaml"))]
#[test]
fn without_yaml_an_empty_yaml_fields_block_is_format_disabled() {
let value = OneBody {
text: "a: b".into(),
};
let err = to_string(&value).expect_err("the reader could not read the block");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
}
#[cfg(not(feature = "json"))]
#[test]
fn without_json_an_empty_json_fields_block_is_format_disabled() {
let value = OneBody {
text: "{ not json".into(),
};
let err = to_string_with_format(&value, Format::Json).expect_err("disabled");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
}
#[cfg(not(feature = "toml"))]
#[test]
fn without_toml_an_empty_toml_fields_block_is_format_disabled() {
let value = OneBody {
text: "a = 1".into(),
};
let err = to_string_with_format(&value, Format::Toml).expect_err("disabled");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
}
}