Skip to main content

markdown_frontmatter/
lib.rs

1#![cfg_attr(docsrs, feature(doc_cfg))]
2#![doc = include_str!("../README.md")]
3
4/// The format of the frontmatter.
5#[derive(Debug, Clone, Copy, PartialEq)]
6enum FrontmatterFormat {
7    /// JSON frontmatter, denoted by `{...}`.
8    Json,
9    /// TOML frontmatter, denoted by `+++...+++`.
10    Toml,
11    /// YAML frontmatter, denoted by `---...---`.
12    Yaml,
13}
14
15impl From<FrontmatterFormat> for &'static str {
16    fn from(format: FrontmatterFormat) -> Self {
17        match format {
18            FrontmatterFormat::Json => "JSON",
19            FrontmatterFormat::Toml => "TOML",
20            FrontmatterFormat::Yaml => "YAML",
21        }
22    }
23}
24
25/// The crate's error type
26#[derive(Debug, thiserror::Error)]
27pub enum Error {
28    /// Frontmatter format is disabled.
29    #[error("disabled format {0}, enable corresponding cargo feature")]
30    DisabledFormat(&'static str),
31    /// Closing delimiter is absent.
32    #[error("absent closing {0} delimiter")]
33    AbsentClosingDelimiter(&'static str),
34
35    #[cfg(feature = "json")]
36    /// Invalid JSON syntax.
37    #[error("invalid JSON syntax")]
38    InvalidJson(#[source] serde_json::Error),
39    #[cfg(feature = "toml")]
40    /// Invalid TOML syntax.
41    #[error("invalid TOML syntax")]
42    InvalidToml(#[source] toml::de::Error),
43    #[cfg(feature = "yaml")]
44    /// Invalid YAML syntax.
45    #[error("invalid YAML syntax")]
46    InvalidYaml(#[source] serde_yaml::Error),
47
48    #[cfg(feature = "json")]
49    /// Couldn't deserialize JSON into the target type.
50    #[error("couldn't deserialize JSON")]
51    DeserializeJson(#[source] serde_json::Error),
52    #[cfg(feature = "toml")]
53    /// Couldn't deserialize TOML into the target type.
54    #[error("couldn't deserialize TOML")]
55    DeserializeToml(#[source] toml::de::Error),
56    #[cfg(feature = "yaml")]
57    /// Couldn't deserialize YAML into the target type.
58    #[error("couldn't deserialize YAML")]
59    DeserializeYaml(#[source] serde_yaml::Error),
60}
61
62#[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
63/// Parses frontmatter from a markdown string, deserializing it into a given
64/// type and returning the parsed frontmatter and the body of the document.
65///
66/// # Arguments
67///
68/// * `content` - The content of the document to parse.
69///
70/// # Examples
71///
72/// ```
73/// use markdown_frontmatter::parse;
74/// use serde::Deserialize;
75///
76/// #[derive(Deserialize)]
77/// struct MyFrontmatter {
78///     title: String,
79/// }
80///
81/// let doc = r#"---
82/// title: Hello
83/// ---
84/// World
85/// "#;
86///
87/// let (frontmatter, body) = parse::<MyFrontmatter>(doc).unwrap();
88/// assert_eq!(frontmatter.title, "Hello");
89/// assert_eq!(body, "World\n");
90/// ```
91pub fn parse<T: serde::de::DeserializeOwned>(content: &str) -> Result<(T, &str), Error> {
92    let (maybe_frontmatter, body) = split(content)?;
93    let SplitFrontmatter(format, matter_str) = maybe_frontmatter.unwrap_or_default();
94    let frontmatter = format.parse(matter_str)?;
95    Ok((frontmatter, body))
96}
97
98#[derive(Debug, Clone, Copy)]
99struct SplitFrontmatter<'a>(FrontmatterFormat, &'a str);
100
101#[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
102impl Default for SplitFrontmatter<'_> {
103    fn default() -> Self {
104        #[cfg(feature = "json")]
105        {
106            Self(FrontmatterFormat::Json, "{}")
107        }
108        #[cfg(all(not(feature = "json"), feature = "toml"))]
109        {
110            Self(FrontmatterFormat::Toml, "")
111        }
112        #[cfg(all(not(any(feature = "json", feature = "toml")), feature = "yaml"))]
113        {
114            Self(FrontmatterFormat::Yaml, "{}")
115        }
116    }
117}
118
119/// Splits a document into frontmatter and body, returning the raw frontmatter
120/// string and the body of the document.
121fn split(content: &str) -> Result<(Option<SplitFrontmatter<'_>>, &str), Error> {
122    let content = content.trim_start();
123    let mut lines = LineSpan::new(content);
124
125    let Some(span) = lines.next() else {
126        // Empty document
127        return Ok((None, content));
128    };
129
130    let Some(format) = FrontmatterFormat::detect(span.line) else {
131        // No frontmatter
132        return Ok((None, content));
133    };
134
135    let matter_start = match format {
136        FrontmatterFormat::Json => span.start, // include opening curly bracket,
137        FrontmatterFormat::Toml | FrontmatterFormat::Yaml => span.next_start,
138    };
139
140    let closing_delimiter = format.delimiter().1;
141    for span in lines {
142        if span.line != closing_delimiter {
143            continue;
144        }
145        let (matter, body) = match format {
146            FrontmatterFormat::Json => (
147                &content[matter_start..span.next_start], // include closing curly bracket
148                &content[span.next_start..],
149            ),
150            FrontmatterFormat::Toml | FrontmatterFormat::Yaml => (
151                &content[matter_start..span.start], // exclude closing delimiter
152                &content[span.next_start..],
153            ),
154        };
155        return Ok((Some(SplitFrontmatter(format, matter)), body));
156    }
157    Err(Error::AbsentClosingDelimiter(format.into()))
158}
159
160impl FrontmatterFormat {
161    const VARIANTS: [Self; 3] = [Self::Json, Self::Toml, Self::Yaml];
162
163    /// Detects the frontmatter format from the first line of a document.
164    fn detect(first_line: &str) -> Option<Self> {
165        Self::VARIANTS
166            .into_iter()
167            .find(|&variant| first_line == variant.delimiter().0)
168    }
169
170    #[cfg(any(feature = "json", feature = "toml", feature = "yaml"))]
171    fn parse<T: serde::de::DeserializeOwned>(&self, matter_str: &str) -> Result<T, Error> {
172        match self {
173            #[cfg(feature = "json")]
174            Self::Json => {
175                let json: serde_json::Value =
176                    serde_json::from_str(matter_str).map_err(Error::InvalidJson)?;
177                serde_json::from_value(json).map_err(Error::DeserializeJson)
178            }
179            #[cfg(not(feature = "json"))]
180            Self::Json => Err(Error::DisabledFormat(Self::Json.into())),
181
182            #[cfg(feature = "toml")]
183            Self::Toml => {
184                let toml: toml::Value = toml::from_str(matter_str).map_err(Error::InvalidToml)?;
185                toml.try_into().map_err(Error::DeserializeToml)
186            }
187            #[cfg(not(feature = "toml"))]
188            Self::Toml => Err(Error::DisabledFormat(Self::Toml.into())),
189
190            #[cfg(feature = "yaml")]
191            Self::Yaml => {
192                let yaml: serde_yaml::Value =
193                    serde_yaml::from_str(matter_str).map_err(Error::InvalidYaml)?;
194                serde_yaml::from_value(yaml).map_err(Error::DeserializeYaml)
195            }
196            #[cfg(not(feature = "yaml"))]
197            Self::Yaml => Err(Error::DisabledFormat(Self::Yaml.into())),
198        }
199    }
200
201    fn delimiter(&self) -> (&'static str, &'static str) {
202        match self {
203            Self::Json => ("{", "}"),
204            Self::Toml => ("+++", "+++"),
205            Self::Yaml => ("---", "---"),
206        }
207    }
208}
209
210struct LineSpan<'a> {
211    pub start: usize,
212    pub next_start: usize,
213    pub line: &'a str,
214}
215
216impl<'a> LineSpan<'a> {
217    fn new(s: &'a str) -> impl Iterator<Item = LineSpan<'a>> + 'a {
218        let bytes = s.as_bytes();
219        let mut pos = 0;
220        std::iter::from_fn(move || {
221            if pos >= bytes.len() {
222                return None;
223            }
224            let start = pos;
225            let mut i = start;
226            while i < bytes.len() && bytes[i] != b'\n' && bytes[i] != b'\r' {
227                i += 1;
228            }
229            let line_end = i;
230            if i < bytes.len() && bytes[i] == b'\r' {
231                i += 1;
232                if i < bytes.len() && bytes[i] == b'\n' {
233                    i += 1;
234                }
235            } else if i < bytes.len() && bytes[i] == b'\n' {
236                i += 1;
237            }
238            let line = &s[start..line_end];
239            let next_start = i;
240            pos = i;
241            Some(LineSpan {
242                start,
243                next_start,
244                line,
245            })
246        })
247    }
248}
249
250#[cfg(test)]
251mod test_line_span {
252    use super::*;
253
254    #[test]
255    fn line_span() {
256        let input = "line 1\r\nline 2\nline 3";
257        let mut lines = LineSpan::new(input);
258
259        let line1 = lines.next().unwrap();
260        assert_eq!(line1.line, "line 1");
261        assert_eq!(line1.start, 0);
262        assert_eq!(line1.next_start, 8);
263
264        let line2 = lines.next().unwrap();
265        assert_eq!(line2.line, "line 2");
266        assert_eq!(line2.start, 8);
267        assert_eq!(line2.next_start, 15);
268
269        let line3 = lines.next().unwrap();
270        assert_eq!(line3.line, "line 3");
271        assert_eq!(line3.start, 15);
272        assert_eq!(line3.next_start, 21);
273
274        assert!(lines.next().is_none());
275    }
276}
277
278#[cfg(test)]
279mod test_split {
280    use super::*;
281
282    #[test]
283    fn empty_document() {
284        let input = "";
285        let (frontmatter, body) = split(input).unwrap();
286        assert!(frontmatter.is_none());
287        assert_eq!(body, "");
288    }
289
290    #[test]
291    fn no_frontmatter() {
292        let input = "hello world";
293        let (frontmatter, body) = split(input).unwrap();
294        assert!(frontmatter.is_none());
295        assert_eq!(body, "hello world");
296    }
297
298    #[test]
299    fn unclosed_json() {
300        let input = "{\n\t\"foo\": \"bar\"\n";
301        let result = split(input);
302        assert!(matches!(
303            result.unwrap_err(),
304            Error::AbsentClosingDelimiter("JSON")
305        ));
306    }
307
308    #[test]
309    fn unclosed_toml() {
310        let input = "+++\nfoo = \"bar\"";
311        let result = split(input);
312        assert!(matches!(
313            result.unwrap_err(),
314            Error::AbsentClosingDelimiter("TOML")
315        ));
316    }
317
318    #[test]
319    fn unclosed_yaml() {
320        let input = "---\nfoo: bar";
321        let result = split(input);
322        assert!(matches!(
323            result.unwrap_err(),
324            Error::AbsentClosingDelimiter("YAML")
325        ));
326    }
327
328    #[test]
329    fn json_singleline() {
330        let input = "{\n\t\"foo\": \"bar\"\n}\nhello world";
331        let (frontmatter, body) = split(input).unwrap();
332        assert_eq!(frontmatter.unwrap().1, "{\n\t\"foo\": \"bar\"\n}\n");
333        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Json);
334        assert_eq!(body, "hello world");
335    }
336
337    #[test]
338    fn json_multiline() {
339        let input = "{\n\t\"foo\": \"bar\",\n\t\"baz\": 1\n}\nhello world";
340        let (frontmatter, body) = split(input).unwrap();
341        assert_eq!(
342            frontmatter.unwrap().1,
343            "{\n\t\"foo\": \"bar\",\n\t\"baz\": 1\n}\n"
344        );
345        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Json);
346        assert_eq!(body, "hello world");
347    }
348
349    #[test]
350    fn toml_singleline() {
351        let input = "+++\nfoo = \"bar\"\n+++\nhello world";
352        let (frontmatter, body) = split(input).unwrap();
353        assert_eq!(frontmatter.unwrap().1, "foo = \"bar\"\n");
354        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Toml);
355        assert_eq!(body, "hello world");
356    }
357
358    #[test]
359    fn toml_multiline() {
360        let input = "+++\nfoo = \"bar\"\nbaz = 1\n+++\nhello world";
361        let (frontmatter, body) = split(input).unwrap();
362        assert_eq!(frontmatter.unwrap().1, "foo = \"bar\"\nbaz = 1\n");
363        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Toml);
364        assert_eq!(body, "hello world");
365    }
366
367    #[test]
368    fn yaml_singleline() {
369        let input = "---\nfoo: bar\n---\nhello world";
370        let (frontmatter, body) = split(input).unwrap();
371        assert_eq!(frontmatter.unwrap().1, "foo: bar\n");
372        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Yaml);
373        assert_eq!(body, "hello world");
374    }
375
376    #[test]
377    fn yaml_multiline() {
378        let input = "---\nfoo: bar\nbaz: 1\n---\nhello world";
379        let (frontmatter, body) = split(input).unwrap();
380        assert_eq!(frontmatter.unwrap().1, "foo: bar\nbaz: 1\n");
381        assert_eq!(frontmatter.unwrap().0, FrontmatterFormat::Yaml);
382        assert_eq!(body, "hello world");
383    }
384}
385
386#[cfg(all(test, any(feature = "json", feature = "toml", feature = "yaml")))]
387mod test_parse {
388    use serde::Deserialize;
389
390    use super::*;
391
392    #[derive(Debug, PartialEq, Deserialize)]
393    struct OptionalFrontmatter {
394        foo: Option<bool>,
395    }
396
397    #[derive(Debug, PartialEq, Deserialize)]
398    struct RequiredFrontmatter {
399        foo: bool,
400    }
401
402    #[derive(Debug, PartialEq, Deserialize)]
403    struct EmptyFrontmatter {}
404
405    const EMPTY_DOCUMENT: &str = "";
406    const DOCUMENT_WITHOUT_FRONTMATTER: &str = "hello world";
407
408    const EMPTY_FRONTMATTER: EmptyFrontmatter = EmptyFrontmatter {};
409    const OPTIONAL_FRONTMATTER_SOME: OptionalFrontmatter = OptionalFrontmatter { foo: Some(true) };
410    const OPTIONAL_FRONTMATTER_NONE: OptionalFrontmatter = OptionalFrontmatter { foo: None };
411    const REQUIRED_FRONTMATTER: RequiredFrontmatter = RequiredFrontmatter { foo: true };
412
413    #[cfg(feature = "json")]
414    mod json {
415        use super::*;
416
417        const VALID_DOCUMENT: &str = "{\n\t\"foo\": true\n}\nhello world";
418        const INVALID_SYNTAX: &str = "{\n1\n}";
419        const INVALID_TYPE: &str = "{\n\t\"foo\": 0\n}";
420
421        #[test]
422        fn empty_frontmatter_in_empty_document() {
423            let (frontmatter, body) = parse::<EmptyFrontmatter>(EMPTY_DOCUMENT).unwrap();
424            assert_eq!(frontmatter, EmptyFrontmatter {});
425            assert_eq!(body, "");
426        }
427
428        #[test]
429        fn optional_frontmatter_in_empty_document() {
430            let (frontmatter, body) = parse::<OptionalFrontmatter>(EMPTY_DOCUMENT).unwrap();
431            assert_eq!(frontmatter.foo, None);
432            assert_eq!(body, "");
433        }
434
435        #[test]
436        fn required_frontmatter_in_empty_document() {
437            let result = parse::<RequiredFrontmatter>(EMPTY_DOCUMENT);
438            assert!(matches!(result.unwrap_err(), Error::DeserializeJson(..)));
439        }
440
441        #[test]
442        fn empty_frontmatter_in_document_without_frontmatter() {
443            let (frontmatter, body) =
444                parse::<EmptyFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
445            assert_eq!(frontmatter, EMPTY_FRONTMATTER);
446            assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
447        }
448
449        #[test]
450        fn optional_frontmatter_in_document_without_frontmatter() {
451            let (frontmatter, body) =
452                parse::<OptionalFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
453            assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_NONE);
454            assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
455        }
456
457        #[test]
458        fn required_frontmatter_in_document_without_frontmatter() {
459            let result = parse::<RequiredFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER);
460            assert!(matches!(result.unwrap_err(), Error::DeserializeJson(..)));
461        }
462
463        #[test]
464        fn optional_frontmatter_in_valid_document() {
465            let (frontmatter, body) = parse::<OptionalFrontmatter>(VALID_DOCUMENT).unwrap();
466            assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_SOME);
467            assert_eq!(body, "hello world");
468        }
469
470        #[test]
471        fn required_frontmatter_in_valid_document() {
472            let (frontmatter, body) = parse::<RequiredFrontmatter>(VALID_DOCUMENT).unwrap();
473            assert_eq!(frontmatter, REQUIRED_FRONTMATTER);
474            assert_eq!(body, "hello world");
475        }
476
477        #[test]
478        fn optional_frontmatter_invalid_syntax() {
479            let result = parse::<OptionalFrontmatter>(INVALID_SYNTAX);
480            assert!(matches!(result.unwrap_err(), Error::InvalidJson(..)));
481        }
482
483        #[test]
484        fn required_frontmatter_invalid_syntax() {
485            let result = parse::<RequiredFrontmatter>(INVALID_SYNTAX);
486            assert!(matches!(result.unwrap_err(), Error::InvalidJson(..)));
487        }
488
489        #[test]
490        fn optional_frontmatter_invalid_type() {
491            let result = parse::<OptionalFrontmatter>(INVALID_TYPE);
492            assert!(matches!(result.unwrap_err(), Error::DeserializeJson(..)));
493        }
494
495        #[test]
496        fn required_frontmatter_invalid_type() {
497            let result = parse::<RequiredFrontmatter>(INVALID_TYPE);
498            assert!(matches!(result.unwrap_err(), Error::DeserializeJson(..)));
499        }
500    }
501
502    #[cfg(feature = "toml")]
503    mod toml {
504        use super::*;
505
506        const VALID_DOCUMENT: &str = "+++\nfoo = true\n+++\nhello world";
507        const INVALID_SYNTAX: &str = "+++\nfoobar\n+++\n";
508        const INVALID_TYPE: &str = "+++\nfoo = 123\n+++\n";
509
510        #[cfg(not(any(feature = "json", feature = "yaml")))]
511        mod only {
512            use super::*;
513
514            #[test]
515            fn empty_frontmatter_in_empty_document() {
516                let (frontmatter, body) = parse::<EmptyFrontmatter>(EMPTY_DOCUMENT).unwrap();
517                assert_eq!(frontmatter, EmptyFrontmatter {});
518                assert_eq!(body, "");
519            }
520
521            #[test]
522            fn optional_frontmatter_in_empty_document() {
523                let (frontmatter, body) = parse::<OptionalFrontmatter>(EMPTY_DOCUMENT).unwrap();
524                assert_eq!(frontmatter.foo, None);
525                assert_eq!(body, "");
526            }
527
528            #[test]
529            fn required_frontmatter_in_empty_document() {
530                let result = parse::<RequiredFrontmatter>(EMPTY_DOCUMENT);
531                assert!(matches!(result.unwrap_err(), Error::DeserializeToml(..)));
532            }
533
534            #[test]
535            fn empty_frontmatter_in_document_without_frontmatter() {
536                let (frontmatter, body) =
537                    parse::<EmptyFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
538                assert_eq!(frontmatter, EMPTY_FRONTMATTER);
539                assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
540            }
541
542            #[test]
543            fn optional_frontmatter_in_document_without_frontmatter() {
544                let (frontmatter, body) =
545                    parse::<OptionalFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
546                assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_NONE);
547                assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
548            }
549
550            #[test]
551            fn required_frontmatter_in_document_without_frontmatter() {
552                let result = parse::<RequiredFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER);
553                assert!(matches!(result.unwrap_err(), Error::DeserializeToml(..)));
554            }
555        }
556
557        #[test]
558        fn optional_frontmatter_in_valid_document() {
559            let (frontmatter, body) = parse::<OptionalFrontmatter>(VALID_DOCUMENT).unwrap();
560            assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_SOME);
561            assert_eq!(body, "hello world");
562        }
563
564        #[test]
565        fn required_frontmatter_in_valid_document() {
566            let (frontmatter, body) = parse::<RequiredFrontmatter>(VALID_DOCUMENT).unwrap();
567            assert_eq!(frontmatter, REQUIRED_FRONTMATTER);
568            assert_eq!(body, "hello world");
569        }
570
571        #[test]
572        fn optional_frontmatter_invalid_syntax() {
573            let result = parse::<OptionalFrontmatter>(INVALID_SYNTAX);
574            assert!(matches!(result.unwrap_err(), Error::InvalidToml(..)));
575        }
576
577        #[test]
578        fn required_frontmatter_invalid_syntax() {
579            let result = parse::<RequiredFrontmatter>(INVALID_SYNTAX);
580            assert!(matches!(result.unwrap_err(), Error::InvalidToml(..)));
581        }
582
583        #[test]
584        fn optional_frontmatter_invalid_type() {
585            let result = parse::<OptionalFrontmatter>(INVALID_TYPE);
586            assert!(matches!(result.unwrap_err(), Error::DeserializeToml(..)));
587        }
588
589        #[test]
590        fn required_frontmatter_invalid_type() {
591            let result = parse::<RequiredFrontmatter>(INVALID_TYPE);
592            assert!(matches!(result.unwrap_err(), Error::DeserializeToml(..)));
593        }
594    }
595
596    #[cfg(feature = "yaml")]
597    mod yaml {
598        use super::*;
599
600        const VALID_DOCUMENT: &str = "---\nfoo: true\n---\nhello world";
601        const INVALID_SYNTAX: &str = "---\n:\n---\n";
602        const INVALID_TYPE: &str = "---\nfoo: 123\n---\n";
603
604        #[cfg(not(any(feature = "json", feature = "toml")))]
605        mod only {
606            use super::*;
607
608            #[test]
609            fn empty_frontmatter_in_empty_document() {
610                let (frontmatter, body) = parse::<EmptyFrontmatter>(EMPTY_DOCUMENT).unwrap();
611                assert_eq!(frontmatter, EmptyFrontmatter {});
612                assert_eq!(body, "");
613            }
614
615            #[test]
616            fn optional_frontmatter_in_empty_document() {
617                let (frontmatter, body) = parse::<OptionalFrontmatter>(EMPTY_DOCUMENT).unwrap();
618                assert_eq!(frontmatter.foo, None);
619                assert_eq!(body, "");
620            }
621
622            #[test]
623            fn required_frontmatter_in_empty_document() {
624                let result = parse::<RequiredFrontmatter>(EMPTY_DOCUMENT);
625                assert!(matches!(result.unwrap_err(), Error::DeserializeYaml(..)));
626            }
627
628            #[test]
629            fn empty_frontmatter_in_document_without_frontmatter() {
630                let (frontmatter, body) =
631                    parse::<EmptyFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
632                assert_eq!(frontmatter, EMPTY_FRONTMATTER);
633                assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
634            }
635
636            #[test]
637            fn optional_frontmatter_in_document_without_frontmatter() {
638                let (frontmatter, body) =
639                    parse::<OptionalFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER).unwrap();
640                assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_NONE);
641                assert_eq!(body, DOCUMENT_WITHOUT_FRONTMATTER);
642            }
643
644            #[test]
645            fn required_frontmatter_in_document_without_frontmatter() {
646                let result = parse::<RequiredFrontmatter>(DOCUMENT_WITHOUT_FRONTMATTER);
647                assert!(matches!(result.unwrap_err(), Error::DeserializeYaml(..)));
648            }
649        }
650
651        #[test]
652        fn optional_frontmatter_in_valid_document() {
653            let (frontmatter, body) = parse::<OptionalFrontmatter>(VALID_DOCUMENT).unwrap();
654            assert_eq!(frontmatter, OPTIONAL_FRONTMATTER_SOME);
655            assert_eq!(body, "hello world");
656        }
657
658        #[test]
659        fn required_frontmatter_in_valid_document() {
660            let (frontmatter, body) = parse::<RequiredFrontmatter>(VALID_DOCUMENT).unwrap();
661            assert_eq!(frontmatter, REQUIRED_FRONTMATTER);
662            assert_eq!(body, "hello world");
663        }
664
665        #[test]
666        fn optional_frontmatter_invalid_syntax() {
667            let result = parse::<OptionalFrontmatter>(INVALID_SYNTAX);
668            assert!(matches!(result.unwrap_err(), Error::InvalidYaml(..)));
669        }
670
671        #[test]
672        fn required_frontmatter_invalid_syntax() {
673            let result = parse::<RequiredFrontmatter>(INVALID_SYNTAX);
674            assert!(matches!(result.unwrap_err(), Error::InvalidYaml(..)));
675        }
676
677        #[test]
678        fn optional_frontmatter_invalid_type() {
679            let result = parse::<OptionalFrontmatter>(INVALID_TYPE);
680            assert!(matches!(result.unwrap_err(), Error::DeserializeYaml(..)));
681        }
682
683        #[test]
684        fn required_frontmatter_invalid_type() {
685            let result = parse::<RequiredFrontmatter>(INVALID_TYPE);
686            assert!(matches!(result.unwrap_err(), Error::DeserializeYaml(..)));
687        }
688    }
689}