use std::cell::Cell;
use std::io::Read;
use std::marker::PhantomData;
use std::vec;
use serde::de::value::{StrDeserializer, StringDeserializer};
use serde::de::{DeserializeOwned, DeserializeSeed, Deserializer, MapAccess, SeqAccess, Visitor};
use serde_json::{Map, Value};
use crate::drive::{self, Steps};
use crate::error::{Error, ErrorKind};
use crate::format::{self, Format};
use crate::markdown::Markdown;
use crate::parse::{self, Document, EMPTY_STRING_SECTION, Fields};
pub fn from_str<T>(s: &str) -> Result<T, Error>
where
T: DeserializeOwned + Markdown,
{
drive::run(ReadDoc::new(s))
}
pub(crate) struct ReadDoc<'a, T> {
input: &'a str,
body_fields: &'static [&'static str],
stage: ReadStage,
value: PhantomData<fn() -> T>,
}
enum ReadStage {
Split,
Decode(Document),
Deserialize(Decoded),
Done,
}
impl<'a, T: DeserializeOwned + Markdown> ReadDoc<'a, T> {
pub(crate) fn new(input: &'a str) -> Self {
Self {
input,
body_fields: T::BODY_FIELDS,
stage: ReadStage::Split,
value: PhantomData,
}
}
}
impl<T: DeserializeOwned> Steps for ReadDoc<'_, T> {
type Output = T;
fn step(&mut self) -> Result<Option<T>, Error> {
let count = self.body_fields.len();
match std::mem::replace(&mut self.stage, ReadStage::Done) {
ReadStage::Split => {
self.stage = ReadStage::Decode(parse::parse(self.input, count)?);
}
ReadStage::Decode(doc) => {
let decoded = decode_document(doc)?;
if decoded.body.len() > count {
return Err(Error::body(format!(
"expected at most {count} body section(s), found {}",
decoded.body.len()
)));
}
self.stage = ReadStage::Deserialize(decoded);
}
ReadStage::Deserialize(decoded) => {
let slots = section_slots(count, decoded.body);
let finished = Cell::new(false);
let read = serde::de::Deserialize::deserialize(RootDe {
fields: decoded.fields,
body_fields: self.body_fields,
slots,
format: decoded.format,
finished: &finished,
});
return read.map(Some).map_err(|err| {
match err.missing_field() {
Some(name) if finished.get() && self.body_fields.contains(&name) => {
Error::body(format!("missing section for body field `{name}`"))
}
_ => err,
}
});
}
ReadStage::Done => unreachable!("a finished job is not stepped again"),
}
Ok(None)
}
}
pub fn from_slice<T>(bytes: &[u8]) -> Result<T, Error>
where
T: DeserializeOwned + Markdown,
{
match str::from_utf8(bytes) {
Ok(s) => from_str(s),
Err(err) => Err(Error::syntax(err.valid_up_to(), "invalid UTF-8")),
}
}
pub fn from_reader<R, T>(mut reader: R) -> Result<T, Error>
where
R: Read,
T: DeserializeOwned + Markdown,
{
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes)?;
from_slice(&bytes)
}
struct Decoded {
fields: Map<String, Value>,
body: Vec<String>,
format: Format,
}
fn decode_document(doc: Document) -> Result<Decoded, Error> {
let Document { fields, body } = doc;
match fields {
Fields::Fenced {
labeled,
sniff,
inner,
} => {
let format = labeled.unwrap_or(sniff).format();
let map = mapping_or_type_error(format::load(&inner, format)?)?;
Ok(Decoded {
fields: map,
body,
format,
})
}
Fields::Bare { sniff, map, .. } => Ok(Decoded {
fields: map,
body,
format: sniff.format(),
}),
Fields::None => Ok(Decoded {
fields: Map::new(),
body,
format: Format::Yaml,
}),
}
}
fn mapping_or_type_error(value: Value) -> Result<Map<String, Value>, Error> {
match mapping_from_value(value) {
Some(map) => Ok(map),
None => Err(Error::type_error("fields block must be a mapping")),
}
}
fn mapping_from_value(value: Value) -> Option<Map<String, Value>> {
match value {
Value::Object(map) => Some(map),
Value::Null => Some(Map::new()),
_ => None,
}
}
enum SectionSlot {
Absent,
Empty,
QuotedEmpty,
Text(String),
}
fn section_slots(n: usize, sections: Vec<String>) -> Vec<SectionSlot> {
let mut sections = sections.into_iter();
(0..n)
.map(|i| match sections.next() {
None => SectionSlot::Absent,
Some(text) if i + 1 == n => SectionSlot::Text(text),
Some(text) if text.is_empty() => SectionSlot::Empty,
Some(text) if text == EMPTY_STRING_SECTION => SectionSlot::QuotedEmpty,
Some(text) => SectionSlot::Text(text),
})
.collect()
}
fn into_body(err: Error) -> Error {
match err.kind() {
ErrorKind::Body | ErrorKind::FormatDisabled | ErrorKind::Syntax => err,
_ => Error::body(err),
}
}
struct RootDe<'a> {
fields: Map<String, Value>,
body_fields: &'static [&'static str],
slots: Vec<SectionSlot>,
format: Format,
finished: &'a Cell<bool>,
}
impl<'de> Deserializer<'de> for RootDe<'_> {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.deserialize_map(visitor)
}
fn deserialize_map<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
visitor.visit_map(RootMap::new(self)?)
}
fn deserialize_struct<V: Visitor<'de>>(
self,
_name: &'static str,
_fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
self.deserialize_map(visitor)
}
fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
visitor.visit_unit()
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 char str string
bytes byte_buf option unit unit_struct newtype_struct seq tuple
tuple_struct enum identifier
}
}
enum Pending {
Front(String, Value),
Body { name: &'static str, index: usize },
}
enum Current {
Front(Value),
Body(usize),
}
struct RootMap<'a> {
slots: Vec<SectionSlot>,
format: Format,
pending: vec::IntoIter<Pending>,
current: Option<Current>,
finished: &'a Cell<bool>,
}
impl<'a> RootMap<'a> {
fn new(root: RootDe<'a>) -> Result<Self, Error> {
let mut pending = Vec::with_capacity(root.fields.len() + root.body_fields.len());
for (key, value) in root.fields {
if root.body_fields.contains(&key.as_str()) {
return Err(Error::type_error(format!(
"the fields block has the key `{key}`, which is a body field; \
a body field is read from its section of the document"
)));
}
pending.push(Pending::Front(key, value));
}
for (index, name) in root.body_fields.iter().enumerate() {
if !matches!(root.slots.get(index), None | Some(SectionSlot::Absent)) {
pending.push(Pending::Body { name, index });
}
}
Ok(Self {
slots: root.slots,
format: root.format,
pending: pending.into_iter(),
current: None,
finished: root.finished,
})
}
}
impl<'de> MapAccess<'de> for RootMap<'_> {
type Error = Error;
fn next_key_seed<K>(&mut self, seed: K) -> Result<Option<K::Value>, Error>
where
K: DeserializeSeed<'de>,
{
match self.pending.next() {
None => {
self.finished.set(true);
Ok(None)
}
Some(Pending::Front(key, value)) => {
self.current = Some(Current::Front(value));
seed.deserialize(StringDeserializer::<Error>::new(key))
.map(Some)
}
Some(Pending::Body { name, index }) => {
self.current = Some(Current::Body(index));
seed.deserialize(StrDeserializer::<Error>::new(name))
.map(Some)
}
}
}
fn next_value_seed<V>(&mut self, seed: V) -> Result<V::Value, Error>
where
V: DeserializeSeed<'de>,
{
match self.current.take() {
Some(Current::Front(value)) => seed.deserialize(value).map_err(json_err),
Some(Current::Body(index)) => {
let slot = self
.slots
.get_mut(index)
.map_or(SectionSlot::Absent, |slot| {
std::mem::replace(slot, SectionSlot::Absent)
});
seed.deserialize(SectionDeserializer {
slot,
format: self.format,
})
}
None => Err(Error::type_error("value is missing")),
}
}
}
struct SectionDeserializer {
slot: SectionSlot,
format: Format,
}
impl SectionDeserializer {
fn parsed(self) -> Result<Value, Error> {
match self.slot {
SectionSlot::Absent => Err(Error::body("missing section")),
SectionSlot::Empty => Err(Error::body("empty section for required structured field")),
SectionSlot::Text(text) if text.is_empty() => {
Err(Error::body("empty section for required structured field"))
}
SectionSlot::QuotedEmpty => format::load_body(EMPTY_STRING_SECTION, self.format),
SectionSlot::Text(text) => format::load_body(&text, self.format),
}
}
}
impl<'de> Deserializer<'de> for SectionDeserializer {
type Error = Error;
fn deserialize_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.parsed()?
.deserialize_any(visitor)
.map_err(|err| into_body(json_err(err)))
}
fn deserialize_str<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.deserialize_string(visitor)
}
fn deserialize_string<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.slot {
SectionSlot::Absent => Err(Error::body("missing section")),
SectionSlot::Empty => visitor.visit_string(String::new()),
SectionSlot::QuotedEmpty => visitor.visit_string(EMPTY_STRING_SECTION.to_owned()),
SectionSlot::Text(text) => visitor.visit_string(text),
}
}
fn deserialize_bytes<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.deserialize_byte_buf(visitor)
}
fn deserialize_byte_buf<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.slot {
SectionSlot::Absent => Err(Error::body("missing section")),
SectionSlot::Empty => visitor.visit_byte_buf(Vec::new()),
SectionSlot::QuotedEmpty => {
visitor.visit_byte_buf(EMPTY_STRING_SECTION.as_bytes().to_vec())
}
SectionSlot::Text(text) => visitor.visit_byte_buf(text.into_bytes()),
}
}
fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.slot {
SectionSlot::Absent | SectionSlot::Empty => visitor.visit_none(),
SectionSlot::QuotedEmpty => visitor.visit_some(SectionDeserializer {
slot: SectionSlot::Text(String::new()),
format: self.format,
}),
SectionSlot::Text(_) => visitor.visit_some(self),
}
}
fn deserialize_ignored_any<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
visitor.visit_unit()
}
fn deserialize_seq<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
match self.slot {
SectionSlot::Absent => Err(Error::body("missing section")),
SectionSlot::Empty => Value::Array(Vec::new())
.deserialize_seq(visitor)
.map_err(json_err),
SectionSlot::QuotedEmpty => visitor.visit_seq(ByteSeq {
iter: EMPTY_STRING_SECTION.as_bytes().to_vec().into_iter(),
}),
SectionSlot::Text(text) if !parse::may_be_list(&text) => visitor.visit_seq(ByteSeq {
iter: text.into_bytes().into_iter(),
}),
SectionSlot::Text(text) => match format::load_body(&text, self.format) {
Ok(Value::Array(values)) if values.is_empty() => Value::Array(values)
.deserialize_seq(visitor)
.map_err(json_err),
Ok(Value::Array(values)) => visitor.visit_seq(EitherSeq::Undecided {
bytes: text.into_bytes(),
values,
}),
_ => visitor.visit_seq(ByteSeq {
iter: text.into_bytes().into_iter(),
}),
},
}
}
fn deserialize_enum<V: Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
match self.slot {
SectionSlot::Text(text) if variants.contains(&text.as_str()) => {
StringDeserializer::<Error>::new(text).deserialize_enum(name, variants, visitor)
}
slot => SectionDeserializer {
slot,
format: self.format,
}
.parsed()?
.deserialize_enum(name, variants, visitor)
.map_err(|err| into_body(json_err(err))),
}
}
fn deserialize_char<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
self.deserialize_string(visitor)
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
_name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
visitor.visit_newtype_struct(self)
}
serde::forward_to_deserialize_any! {
bool i8 i16 i32 i64 i128 u8 u16 u32 u64 u128 f32 f64 unit unit_struct
tuple tuple_struct map struct identifier
}
}
enum EitherSeq {
Undecided { bytes: Vec<u8>, values: Vec<Value> },
Bytes(vec::IntoIter<u8>),
Values(vec::IntoIter<Value>),
}
enum Choice {
Bytes,
Values,
}
impl<'de> SeqAccess<'de> for EitherSeq {
type Error = Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Error>
where
T: DeserializeSeed<'de>,
{
match self {
EitherSeq::Bytes(iter) => match iter.next() {
Some(b) => seed
.deserialize(Value::Number(u64::from(b).into()))
.map(Some)
.map_err(json_err),
None => Ok(None),
},
EitherSeq::Values(iter) => match iter.next() {
Some(value) => seed.deserialize(value).map(Some).map_err(json_err),
None => Ok(None),
},
EitherSeq::Undecided { bytes, values } => {
let mut bytes = std::mem::take(bytes).into_iter();
let mut values = std::mem::take(values).into_iter();
let (Some(byte), Some(value)) = (bytes.next(), values.next()) else {
*self = EitherSeq::Values(Vec::new().into_iter());
return Ok(None);
};
let mut choice = Choice::Values;
let first = seed.deserialize(FirstElement {
byte,
value,
choice: &mut choice,
})?;
*self = match choice {
Choice::Bytes => EitherSeq::Bytes(bytes),
Choice::Values => EitherSeq::Values(values),
};
Ok(Some(first))
}
}
}
}
struct FirstElement<'a> {
byte: u8,
value: Value,
choice: &'a mut Choice,
}
macro_rules! first_element_reads_values {
($($method:ident),* $(,)?) => {
$(
fn $method<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value.$method(visitor).map_err(json_err)
}
)*
};
}
impl<'de> Deserializer<'de> for FirstElement<'_> {
type Error = Error;
fn deserialize_u8<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value, Error> {
*self.choice = Choice::Bytes;
visitor.visit_u8(self.byte)
}
first_element_reads_values! {
deserialize_any, deserialize_bool, deserialize_i8, deserialize_i16, deserialize_i32,
deserialize_i64, deserialize_i128, deserialize_u16, deserialize_u32, deserialize_u64,
deserialize_u128, deserialize_f32, deserialize_f64, deserialize_char, deserialize_str,
deserialize_string, deserialize_bytes, deserialize_byte_buf, deserialize_option,
deserialize_unit, deserialize_seq, deserialize_map, deserialize_identifier,
deserialize_ignored_any,
}
fn deserialize_unit_struct<V: Visitor<'de>>(
self,
name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value
.deserialize_unit_struct(name, visitor)
.map_err(json_err)
}
fn deserialize_newtype_struct<V: Visitor<'de>>(
self,
name: &'static str,
visitor: V,
) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value
.deserialize_newtype_struct(name, visitor)
.map_err(json_err)
}
fn deserialize_tuple<V: Visitor<'de>>(self, len: usize, visitor: V) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value.deserialize_tuple(len, visitor).map_err(json_err)
}
fn deserialize_tuple_struct<V: Visitor<'de>>(
self,
name: &'static str,
len: usize,
visitor: V,
) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value
.deserialize_tuple_struct(name, len, visitor)
.map_err(json_err)
}
fn deserialize_struct<V: Visitor<'de>>(
self,
name: &'static str,
fields: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value
.deserialize_struct(name, fields, visitor)
.map_err(json_err)
}
fn deserialize_enum<V: Visitor<'de>>(
self,
name: &'static str,
variants: &'static [&'static str],
visitor: V,
) -> Result<V::Value, Error> {
*self.choice = Choice::Values;
self.value
.deserialize_enum(name, variants, visitor)
.map_err(json_err)
}
}
struct ByteSeq {
iter: vec::IntoIter<u8>,
}
impl<'de> SeqAccess<'de> for ByteSeq {
type Error = Error;
fn next_element_seed<T>(&mut self, seed: T) -> Result<Option<T::Value>, Error>
where
T: DeserializeSeed<'de>,
{
match self.iter.next() {
Some(b) => seed
.deserialize(Value::Number(u64::from(b).into()))
.map(Some)
.map_err(json_err),
None => Ok(None),
}
}
}
fn json_err(err: serde_json::Error) -> Error {
Error::type_error(err)
}
#[cfg(test)]
mod tests {
use super::{from_reader, from_slice, from_str};
use crate::error::ErrorKind;
#[cfg(any(feature = "yaml", feature = "json", feature = "toml"))]
use crate::testdata::values;
use crate::testdata::{goldens, types};
#[cfg(feature = "yaml")]
#[test]
fn page_fenced_yaml_golden() {
let page: types::Page = from_str(goldens::PAGE_FENCED_YAML).expect("yaml");
assert_eq!(page, values::page());
}
#[cfg(feature = "json")]
#[test]
fn page_fenced_json_golden() {
let page: types::Page = from_str(goldens::PAGE_FENCED_JSON).expect("json");
assert_eq!(page, values::page());
}
#[cfg(feature = "toml")]
#[test]
fn page_fenced_toml_golden() {
let page: types::Page = from_str(goldens::PAGE_FENCED_TOML).expect("toml");
assert_eq!(page, values::page());
}
#[cfg(feature = "yaml")]
#[test]
fn page_fenced_yml_tag() {
let page: types::Page = from_str(goldens::PAGE_FENCED_YML).expect("yml");
assert_eq!(page, values::page());
}
#[cfg(feature = "yaml")]
#[test]
fn fields_only_fenced_yaml_golden() {
let fields: types::FieldsOnly =
from_str(goldens::FIELDS_ONLY_FENCED_YAML).expect("fields-only");
assert_eq!(fields, values::fields_only());
}
#[cfg(feature = "yaml")]
#[test]
fn page_bare_yaml_golden() {
let yaml: types::Page = from_str(goldens::PAGE_BARE_YAML).expect("bare yaml");
assert_eq!(yaml, values::page());
}
#[cfg(feature = "json")]
#[test]
fn page_bare_json_golden() {
let json: types::Page = from_str(goldens::PAGE_BARE_JSON).expect("bare json");
assert_eq!(json, values::page());
}
#[cfg(feature = "toml")]
#[test]
fn page_bare_toml_golden() {
let toml: types::Page = from_str(goldens::PAGE_BARE_TOML).expect("bare toml");
assert_eq!(toml, values::page());
}
#[cfg(feature = "yaml")]
#[test]
fn page_unlabeled_yaml_sniffs() {
let yaml: types::Page = from_str(goldens::PAGE_UNLABELED_YAML).expect("unlabeled yaml");
assert_eq!(yaml, values::page());
}
#[cfg(feature = "json")]
#[test]
fn page_unlabeled_json_sniffs() {
let json: types::Page = from_str(goldens::PAGE_UNLABELED_JSON).expect("unlabeled json");
assert_eq!(json, values::page());
}
#[cfg(feature = "toml")]
#[test]
fn page_unlabeled_toml_sniffs() {
let toml: types::Page = from_str(goldens::PAGE_UNLABELED_TOML).expect("unlabeled toml");
assert_eq!(toml, values::page());
}
#[cfg(feature = "json")]
#[test]
fn sniffed_json_parse_failure_is_front_matter() {
let unlabeled = "```\n{\n name: only,\n count: 2\n}\n```\n";
let err = from_str::<types::FieldsOnly>(unlabeled).expect_err("unlabeled json");
assert_eq!(err.kind(), ErrorKind::FrontMatter);
assert!(std::error::Error::source(&err).is_some());
let bare = "{\n name: only,\n count: 2\n}\n";
let err = from_str::<types::FieldsOnly>(bare).expect_err("bare json");
assert_eq!(err.kind(), ErrorKind::FrontMatter);
assert!(std::error::Error::source(&err).is_some());
}
#[cfg(feature = "toml")]
#[test]
fn sniffed_toml_parse_failure_is_front_matter() {
let unlabeled = "```\nname = \"only\"\ncount =\n```\n";
let err = from_str::<types::FieldsOnly>(unlabeled).expect_err("unlabeled toml");
assert_eq!(err.kind(), ErrorKind::FrontMatter);
assert!(std::error::Error::source(&err).is_some());
let bare = "name = \"only\"\ncount =\n";
let err = from_str::<types::FieldsOnly>(bare).expect_err("bare toml");
assert_eq!(err.kind(), ErrorKind::FrontMatter);
assert!(std::error::Error::source(&err).is_some());
}
#[cfg(feature = "yaml")]
#[test]
fn leading_prefix_is_ignored() {
let page: types::Page = from_str(goldens::PAGE_LEADING_PREFIX).expect("prefix");
assert_eq!(page, values::page());
}
#[cfg(feature = "yaml")]
#[test]
fn body_only_two_sections_golden() {
let body: types::BodyOnly = from_str(goldens::BODY_ONLY_TWO_SECTIONS).expect("body-only");
assert_eq!(body, values::body_only());
}
#[cfg(feature = "yaml")]
#[test]
fn fence_not_first_is_body_not_fields() {
let err = from_str::<types::Page>(goldens::PAGE_FENCE_NOT_FIRST).expect_err("not fields");
assert_eq!(err.kind(), ErrorKind::Type);
assert!(err.to_string().contains("field1"), "{err}");
let err = from_str::<types::FieldsOnly>(goldens::PAGE_FENCE_NOT_FIRST)
.expect_err("fence is not fields");
assert_eq!(err.kind(), ErrorKind::Body);
}
#[cfg(feature = "yaml")]
#[test]
fn page_split_goldens_are_one_first_section_plus_appendix() {
let fence: types::Page = from_str(goldens::PAGE_SPLIT_FENCE).expect("split fence");
assert_eq!(fence.field1, "foo");
assert_eq!(fence.field2, "bar");
assert_eq!(fence.field3, 1);
assert_eq!(fence.published, None);
assert_eq!(
fence.text1,
"See this fence; the dashes inside must not split sections:\n\n```text\n---\nnot a split\n```"
);
assert_eq!(fence.appendix, values::TEXT2);
let stars: types::Page = from_str(goldens::PAGE_SPLIT_STARS).expect("split stars");
assert_eq!(stars.field1, "foo");
assert_eq!(stars.appendix, values::TEXT2);
assert_eq!(stars.text1, "before\n***\nstill text1\n___\nstill text1");
let list: types::Page = from_str(goldens::PAGE_SPLIT_LIST).expect("split list");
assert_eq!(list.field1, "foo");
assert_eq!(list.appendix, values::TEXT2);
assert_eq!(list.text1, "- keep going\n ---\n- still the same section");
}
#[cfg(not(feature = "json"))]
#[test]
fn fenced_json_without_feature_is_format_disabled() {
let err = from_str::<types::Page>(goldens::PAGE_FENCED_JSON).expect_err("json");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(not(feature = "toml"))]
#[test]
fn fenced_toml_without_feature_is_format_disabled() {
let err = from_str::<types::Page>(goldens::PAGE_FENCED_TOML).expect_err("toml");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(not(feature = "yaml"))]
#[test]
fn fenced_yaml_without_feature_is_format_disabled() {
let err = from_str::<types::Page>(goldens::PAGE_FENCED_YAML).expect_err("yaml");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(feature = "yaml")]
#[test]
fn optional_middle_none_golden() {
let got: types::OptionalBody =
from_str(goldens::OPTIONAL_MIDDLE_NONE).expect("middle none");
assert_eq!(got, values::optional_middle_none());
}
#[cfg(feature = "yaml")]
#[test]
fn optional_trailing_none_golden() {
let got: types::OptionalBody =
from_str(goldens::OPTIONAL_TRAILING_NONE).expect("trailing none");
assert_eq!(got, values::optional_trailing_none());
}
#[cfg(feature = "yaml")]
#[test]
fn optional_leading_none_golden() {
let got: types::OptionalBody =
from_str(goldens::OPTIONAL_LEADING_NONE).expect("leading none");
assert_eq!(got, values::optional_leading_none());
}
#[cfg(feature = "yaml")]
#[test]
fn optional_empty_string_golden() {
let got: types::OptionalBody =
from_str(goldens::OPTIONAL_EMPTY_STRING).expect("empty string");
assert_eq!(got, values::optional_empty_string());
}
#[cfg(feature = "yaml")]
#[test]
fn extra_optional_body_section_is_body() {
let input = concat!(
"```yaml\n",
"title: memo\n",
"```\n",
"a\n",
"---\n",
"\n",
"---\n",
"c\n",
"---\n",
"extra\n"
);
let value = from_str::<types::OptionalBody>(input).expect("the last field takes the rest");
assert_eq!(value.first.as_deref(), Some("a"));
assert_eq!(value.middle, None);
assert_eq!(value.last.as_deref(), Some("c\n---\nextra\n"));
}
#[cfg(feature = "yaml")]
#[test]
fn empty_section_for_required_structured_is_body() {
let input = "```yaml\ntitle: t\n```\n---\n";
let err = from_str::<types::Article>(input).expect_err("empty structured");
assert_eq!(err.kind(), ErrorKind::Body);
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(feature = "yaml")]
#[test]
fn structured_decoder_failure_is_body_with_source() {
let input = "```yaml\ntitle: Hello\n```\n:\n";
let err = from_str::<types::Article>(input).expect_err("bad structured yaml");
assert_eq!(err.kind(), ErrorKind::Body);
assert!(std::error::Error::source(&err).is_some());
}
#[cfg(feature = "yaml")]
#[test]
fn nested_fenced_yaml_golden() {
let got: types::NestedFields = from_str(goldens::NESTED_FENCED_YAML).expect("nested");
assert_eq!(got, values::nested());
}
#[cfg(feature = "yaml")]
#[test]
fn structured_fenced_yaml_golden() {
let got: types::Article = from_str(goldens::STRUCTURED_FENCED_YAML).expect("structured");
assert_eq!(got, values::article());
}
#[cfg(feature = "yaml")]
#[test]
fn names_fenced_yaml_golden() {
let got: types::JsonNames = from_str(goldens::NAMES_FENCED_YAML).expect("names");
assert_eq!(got, values::json_names());
}
#[cfg(feature = "yaml")]
#[test]
fn whitespace_unicode_golden_keeps_interiors() {
let got: types::Page = from_str(goldens::WHITESPACE_UNICODE).expect("whitespace");
assert_eq!(got, values::whitespace_page());
assert!(got.text1.contains("spaces \n"), "{:?}", got.text1);
assert!(got.text1.contains("🦀"), "{:?}", got.text1);
assert!(got.text1.contains("Ещё"), "{:?}", got.text1);
}
#[cfg(feature = "yaml")]
#[test]
fn well_known_fenced_yaml_golden() {
let got: types::WellKnown = from_str(goldens::WELL_KNOWN_FENCED_YAML).expect("well_known");
assert_eq!(got, values::well_known());
}
#[cfg(feature = "yaml")]
#[test]
fn well_known_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::WellKnown =
from_str(goldens::WELL_KNOWN_FENCED_YAML).expect("generated well_known");
assert_eq!(got, values::well_known_generated());
}
#[cfg(feature = "yaml")]
#[test]
fn page_published_yaml_golden() {
let got: types::Page = from_str(goldens::PAGE_PUBLISHED_YAML).expect("published");
assert_eq!(got, values::page_with_published());
assert!(got.published.is_some());
}
#[cfg(feature = "yaml")]
#[test]
fn page_published_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::Page =
from_str(goldens::PAGE_PUBLISHED_YAML).expect("generated published");
assert_eq!(got, values::page_generated_with_published());
assert!(got.published.is_set());
}
#[cfg(feature = "yaml")]
#[test]
fn page_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::Page =
from_str(goldens::PAGE_FENCED_YAML).expect("generated page");
assert_eq!(got, values::page_generated());
}
#[cfg(feature = "yaml")]
#[test]
fn proto3_page_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::Proto3Page =
from_str(goldens::PROTO3_FENCED_YAML).expect("generated proto3");
assert_eq!(got, values::proto3_page_generated());
assert!(got.body.is_some());
}
#[cfg(feature = "yaml")]
#[test]
fn json_names_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::JsonNames =
from_str(goldens::NAMES_FENCED_YAML).expect("generated names");
assert_eq!(got, values::json_names_generated());
}
#[cfg(feature = "yaml")]
#[test]
fn optional_body_generated_deserializes_golden() {
let middle: serde_markdown_proto::markdown::testdata::OptionalBody =
from_str(goldens::OPTIONAL_MIDDLE_NONE).expect("generated middle none");
assert_eq!(middle, values::optional_middle_none_generated());
let trailing: serde_markdown_proto::markdown::testdata::OptionalBody =
from_str(goldens::OPTIONAL_TRAILING_NONE).expect("generated trailing none");
assert_eq!(trailing, values::optional_trailing_none_generated());
}
#[cfg(feature = "yaml")]
#[test]
fn article_generated_deserializes_golden() {
let got: serde_markdown_proto::markdown::testdata::Article =
from_str(goldens::STRUCTURED_FENCED_YAML).expect("generated article");
assert_eq!(got, values::article_generated());
}
#[cfg(feature = "yaml")]
#[test]
fn extra_body_section_is_body() {
let input = concat!(
"```yaml\n",
"field1: foo\n",
"field2: bar\n",
"field3: 1\n",
"```\n",
"Text1 bla bla bla\n",
"---\n",
"Text2 bal bla bla\n",
"---\n",
"extra\n"
);
let value = from_str::<types::Page>(input).expect("the last field takes the rest");
assert_eq!(value.text1, "Text1 bla bla bla");
assert_eq!(value.appendix, "Text2 bal bla bla\n---\nextra\n");
}
#[cfg(feature = "yaml")]
#[test]
fn missing_body_section_is_body() {
let input = concat!(
"```yaml\n",
"field1: foo\n",
"field2: bar\n",
"field3: 1\n",
"```\n",
"only one section\n"
);
let err = from_str::<types::Page>(input).expect_err("missing section");
assert_eq!(err.kind(), ErrorKind::Body);
}
#[cfg(feature = "yaml")]
#[test]
fn invalid_fields_yaml_is_front_matter() {
let input = "```yaml\n:\n```\n";
let err = from_str::<types::FieldsOnly>(input).expect_err("bad yaml");
assert_eq!(err.kind(), ErrorKind::FrontMatter);
assert!(std::error::Error::source(&err).is_some());
}
#[cfg(feature = "yaml")]
#[test]
fn from_slice_matches_from_str() {
let from_str: types::Page = from_str(goldens::PAGE_FENCED_YAML).expect("from_str");
let from_slice: types::Page =
from_slice(goldens::PAGE_FENCED_YAML.as_bytes()).expect("from_slice");
assert_eq!(from_str, from_slice);
assert_eq!(from_slice, values::page());
}
#[test]
fn from_slice_invalid_utf8_is_syntax() {
let err = from_slice::<types::FieldsOnly>(&[0xff, b'a']).expect_err("utf8");
assert_eq!(err.kind(), ErrorKind::Syntax);
assert_eq!(err.offset(), Some(0));
assert!(std::error::Error::source(&err).is_none());
assert!(err.to_string().contains("invalid UTF-8"), "{err}");
}
#[cfg(feature = "yaml")]
#[test]
fn from_reader_matches_from_str() {
let from_str: types::Page = from_str(goldens::PAGE_FENCED_YAML).expect("from_str");
let from_reader: types::Page =
from_reader(goldens::PAGE_FENCED_YAML.as_bytes()).expect("from_reader");
assert_eq!(from_str, from_reader);
assert_eq!(from_reader, values::page());
}
#[test]
fn from_reader_io_error_is_io() {
struct Boom;
impl std::io::Read for Boom {
fn read(&mut self, _buf: &mut [u8]) -> std::io::Result<usize> {
Err(std::io::Error::other("boom"))
}
}
let err = from_reader::<_, types::FieldsOnly>(Boom).expect_err("read");
assert_eq!(err.kind(), ErrorKind::Io);
assert!(std::error::Error::source(&err).is_some());
}
#[test]
fn from_reader_invalid_utf8_is_syntax() {
let err = from_reader::<_, types::FieldsOnly>([0xff, b'a'].as_slice()).expect_err("utf8");
assert_eq!(err.kind(), ErrorKind::Syntax);
assert_eq!(err.offset(), Some(0));
}
#[cfg(feature = "yaml")]
#[test]
fn unclosed_fence_from_str_is_syntax() {
let err = from_str::<types::FieldsOnly>("```yaml\nname: only\n").expect_err("unclosed");
assert_eq!(err.kind(), ErrorKind::Syntax);
assert_eq!(err.offset(), Some(0));
assert!(std::error::Error::source(&err).is_none());
}
#[cfg(feature = "yaml")]
#[test]
fn empty_input_missing_required_body_is_body() {
let err = from_str::<types::BodyOnly>("").expect_err("empty");
assert_eq!(err.kind(), ErrorKind::Body);
assert!(err.to_string().contains("text1"), "{err}");
let err = from_str::<types::Page>("").expect_err("empty");
assert_eq!(err.kind(), ErrorKind::Type);
}
#[cfg(all(not(feature = "yaml"), feature = "json"))]
#[test]
fn without_yaml_prose_is_body_and_a_yaml_mapping_is_format_disabled() {
let got: types::BodyOnly =
from_str(goldens::BODY_ONLY_TWO_SECTIONS).expect("prose is the body");
assert_eq!(got, values::body_only());
let err = from_str::<types::FieldsOnly>("name: only\ncount: 2\n").expect_err("yaml");
assert_eq!(err.kind(), ErrorKind::FormatDisabled);
}
}