Skip to main content

frontmatter_gen/
parser.rs

1//! # Front Matter Parser and Serialiser Module
2//!
3//! This module provides robust functionality for parsing and serialising front matter
4//! in various formats (YAML, TOML, and JSON). It focuses on:
5//!
6//! - Memory efficiency through pre-allocation and string optimisation
7//! - Type safety with comprehensive error handling
8//! - Performance optimisation with minimal allocations
9//! - Validation of input data
10//! - Consistent cross-format handling
11//!
12//! ## Features
13//!
14//! - Multi-format support (YAML, TOML, JSON)
15//! - Zero-copy parsing where possible
16//! - Efficient memory management
17//! - Comprehensive validation
18//! - Rich error context
19//!
20//! ## Usage Example
21//!
22//! ```rust
23//! use frontmatter_gen::{Format, parser};
24//!
25//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
26//! let yaml = "title: My Post\ndate: 2025-09-09\n";
27//! let front_matter = parser::parse_with_options(
28//!     yaml,
29//!     Format::Yaml,
30//!     None
31//! )?;
32//! # Ok(())
33//! # }
34//! ```
35
36use noyalib::Value as YamlValue;
37use serde::Serialize;
38use serde_json::Value as JsonValue;
39use std::{collections::HashMap, sync::Arc};
40use toml::Value as TomlValue;
41
42use crate::{error::Error, types::Frontmatter, Format, Value};
43
44// Constants for optimisation and validation
45const SMALL_STRING_SIZE: usize = 24;
46const MAX_NESTING_DEPTH: usize = 32;
47const MAX_KEYS: usize = 1000;
48
49/// Options for controlling parsing behaviour.
50///
51/// Provides configuration for maximum allowed nesting depth, maximum number of keys,
52/// and whether to perform validation.
53#[derive(Debug, Clone, Copy)]
54pub struct ParseOptions {
55    /// Maximum allowed nesting depth.
56    pub max_depth: usize,
57    /// Maximum allowed number of keys.
58    pub max_keys: usize,
59    /// Whether to validate the structure.
60    pub validate: bool,
61}
62
63impl Default for ParseOptions {
64    fn default() -> Self {
65        Self {
66            max_depth: MAX_NESTING_DEPTH,
67            max_keys: MAX_KEYS,
68            validate: true,
69        }
70    }
71}
72
73/// Optimises string storage based on length.
74///
75/// For strings shorter than `SMALL_STRING_SIZE`, uses standard allocation.
76/// For longer strings, pre-allocates exact capacity to avoid reallocations.
77///
78/// # Arguments
79///
80/// * `s` - The input string slice to optimise.
81///
82/// # Returns
83///
84/// An optimised owned `String`.
85#[inline]
86fn optimise_string(s: &str) -> String {
87    if s.len() <= SMALL_STRING_SIZE {
88        s.to_string()
89    } else {
90        let mut string = String::with_capacity(s.len());
91        string.push_str(s);
92        string
93    }
94}
95
96/// Parses raw front matter string into a `Frontmatter` object based on the specified format.
97///
98/// This function attempts to parse the provided string into a structured `Frontmatter`
99/// object according to the specified format. It performs validation by default
100/// and optimises memory allocation where possible.
101///
102/// # Arguments
103///
104/// * `raw_front_matter` - A string slice containing the raw front matter content.
105/// * `format` - The `Format` enum specifying the desired format.
106/// * `options` - Optional parsing options for controlling validation and limits.
107///
108/// # Returns
109///
110/// A `Result` containing either the parsed `Frontmatter` object or a `Error`.
111///
112/// # Errors
113///
114/// Returns `Error` if:
115/// - The input is not valid in the specified format
116/// - The structure exceeds configured limits
117/// - The format is unsupported
118pub fn parse_with_options(
119    raw_front_matter: &str,
120    format: Format,
121    options: Option<ParseOptions>,
122) -> Result<Frontmatter, Error> {
123    let options = options.unwrap_or_default();
124
125    // Check for unsupported formats
126    if format == Format::Unsupported {
127        let err_msg = format!(
128            "Unsupported format: {:?}. Supported formats are YAML, TOML, and JSON.",
129            format
130        );
131        log::error!("{}", err_msg);
132        return Err(Error::ConversionError(err_msg));
133    }
134
135    // Trim the input and validate format assumptions
136    let trimmed_content = raw_front_matter.trim();
137
138    // Format-specific validation
139    match format {
140        Format::Yaml => {
141            if !trimmed_content.starts_with("---") {
142                log::debug!("YAML front matter validation: Content structure appears non-standard");
143            }
144        }
145        Format::Toml => {
146            if !trimmed_content.contains('=') {
147                return Err(Error::ConversionError(
148                    "Format set to TOML but input does not contain '=' signs.".to_string(),
149                ));
150            }
151        }
152        Format::Json => {
153            if !trimmed_content.starts_with('{') {
154                return Err(Error::ConversionError(
155                    "Format set to JSON but input does not start with '{'."
156                        .to_string(),
157                ));
158            }
159        }
160        Format::Unsupported => unreachable!(), // We've already handled this case above
161    };
162
163    let front_matter = match format {
164        Format::Yaml => parse_yaml(trimmed_content).map_err(|e| {
165            log::error!("YAML parsing failed: {}", e);
166            e
167        })?,
168        Format::Toml => parse_toml(trimmed_content).map_err(|e| {
169            log::error!("TOML parsing failed: {}", e);
170            e
171        })?,
172        Format::Json => parse_json(trimmed_content).map_err(|e| {
173            log::error!("JSON parsing failed: {}", e);
174            e
175        })?,
176        Format::Unsupported => unreachable!(),
177    };
178
179    // Perform validation if specified in options
180    if options.validate {
181        log::debug!(
182            "Validating front matter: maximum allowed nesting depth is {}, maximum allowed number of keys is {}",
183            options.max_depth,
184            options.max_keys
185        );
186
187        validate_frontmatter(
188            &front_matter,
189            options.max_depth,
190            options.max_keys,
191        )
192        .map_err(|e| {
193            log::error!("Front matter validation failed: {}", e);
194            e
195        })?;
196    }
197
198    Ok(front_matter)
199}
200
201/// Convenience wrapper around `parse_with_options` using default options.
202///
203/// # Arguments
204///
205/// * `raw_front_matter` - A string slice containing the raw front matter content.
206/// * `format` - The `Format` enum specifying the desired format.
207///
208/// # Returns
209///
210/// A `Result` containing either the parsed `Frontmatter` object or a `Error`.
211///
212/// # Errors
213///
214/// Returns an `Error` if:
215/// - The format is invalid or unsupported.
216/// - Parsing fails due to invalid syntax.
217/// - Validation fails if enabled.
218pub fn parse(
219    raw_front_matter: &str,
220    format: Format,
221) -> Result<Frontmatter, Error> {
222    parse_with_options(raw_front_matter, format, None)
223}
224
225/// Converts a `Frontmatter` object to a string representation in the specified format.
226///
227/// # Arguments
228///
229/// * `front_matter` - Reference to the `Frontmatter` object to serialise.
230/// * `format` - The target format for serialisation.
231///
232/// # Returns
233///
234/// A `Result` containing the serialised string or a `Error`.
235///
236/// # Errors
237///
238/// Returns `Error` if:
239/// - Serialisation fails.
240/// - The specified format is unsupported.
241pub fn to_string(
242    front_matter: &Frontmatter,
243    format: Format,
244) -> Result<String, Error> {
245    match format {
246        Format::Yaml => to_yaml(front_matter),
247        Format::Toml => to_toml(front_matter),
248        Format::Json => to_json_optimised(front_matter),
249        Format::Unsupported => Err(Error::ConversionError(
250            "Unsupported format".to_string(),
251        )),
252    }
253}
254
255// YAML Implementation
256// -------------------
257
258/// Parses a YAML string into a `Frontmatter` object.
259///
260/// # Arguments
261///
262/// * `raw` - The raw YAML string.
263///
264/// # Returns
265///
266/// A `Result` containing the parsed `Frontmatter` or a `Error`.
267fn parse_yaml(raw: &str) -> Result<Frontmatter, Error> {
268    // Parse the YAML content into a noyalib::Value
269    let yaml_value: YamlValue = noyalib::from_str(raw)
270        .map_err(|e| Error::YamlParseError { source: e.into() })?;
271
272    // Prepare the front matter container
273    let capacity =
274        yaml_value.as_mapping().map_or(0, noyalib::Mapping::len);
275    let mut front_matter =
276        Frontmatter(HashMap::with_capacity(capacity));
277
278    // Convert the YAML mapping into the front matter structure.
279    //
280    // `noyalib::Mapping` keys are guaranteed `String` (unlike serde_yml's
281    // `Value`-keyed mapping), so we can iterate directly without an extra
282    // type check.
283    if let YamlValue::Mapping(mapping) = yaml_value {
284        for (key, value) in mapping {
285            let _ = front_matter.insert(key, yaml_to_value(&value));
286        }
287    } else {
288        return Err(Error::ParseError(
289            "YAML front matter is not a valid mapping".to_string(),
290        ));
291    }
292
293    Ok(front_matter)
294}
295
296/// Converts a `noyalib::Value` into a `Value`.
297fn yaml_to_value(yaml: &YamlValue) -> Value {
298    match yaml {
299        YamlValue::Null => Value::Null,
300        YamlValue::Bool(b) => Value::Boolean(*b),
301        YamlValue::Number(n) => {
302            // `noyalib::Number::as_f64` returns `f64` directly (not
303            // `Option<f64>` — integers are losslessly widened when
304            // possible). We still prefer the integer path when the value
305            // fits in f64's 52-bit mantissa, falling back to the float
306            // representation otherwise.
307            n.as_i64().map_or_else(
308                || Value::Number(n.as_f64()),
309                |i| {
310                    if i.abs() < (1_i64 << 52) {
311                        Value::Number(i as f64)
312                    } else {
313                        log::warn!(
314                            "Integer {} exceeds precision of f64. Defaulting to 0.0",
315                            i
316                        );
317                        Value::Number(0.0) // Fallback for large values outside f64 precision
318                    }
319                },
320            )
321        }
322        YamlValue::String(s) => Value::String(optimise_string(s)),
323        YamlValue::Sequence(seq) => {
324            let mut vec = Vec::with_capacity(seq.len());
325            vec.extend(seq.iter().map(yaml_to_value));
326            Value::Array(vec)
327        }
328        YamlValue::Mapping(map) => {
329            // `noyalib::Mapping` is keyed by `String` directly — no need
330            // to filter out non-string keys the way we did with the old
331            // `serde_yml::Value`-keyed mapping.
332            let mut result =
333                Frontmatter(HashMap::with_capacity(map.len()));
334            for (k, v) in map {
335                let _ = result
336                    .0
337                    .insert(optimise_string(k), yaml_to_value(v));
338            }
339            Value::Object(Box::new(result))
340        }
341        YamlValue::Tagged(tagged) => Value::Tagged(
342            optimise_string(&tagged.tag().to_string()),
343            Box::new(yaml_to_value(tagged.value())),
344        ),
345    }
346}
347
348/// Serialises a `Frontmatter` object into a YAML string.
349///
350/// # Arguments
351///
352/// * `front_matter` - The `Frontmatter` object to serialise.
353///
354/// # Returns
355///
356/// A `Result` containing the serialised YAML string or a `Error`.
357fn to_yaml(front_matter: &Frontmatter) -> Result<String, Error> {
358    noyalib::to_string(&front_matter.0)
359        .map_err(|e| Error::ConversionError(e.to_string()))
360}
361
362// TOML Implementation
363// -------------------
364
365/// Parses a TOML string into a `Frontmatter` object.
366///
367/// # Arguments
368///
369/// * `raw` - The raw TOML string.
370///
371/// # Returns
372///
373/// A `Result` containing the parsed `Frontmatter` or a `Error`.
374fn parse_toml(raw: &str) -> Result<Frontmatter, Error> {
375    // toml 1.x: `Value::from_str` parses a single TOML *value*, not a
376    // document. Parse into `toml::Table` (i.e. a document) and wrap it.
377    let table: toml::Table =
378        raw.parse().map_err(Error::TomlParseError)?;
379
380    let mut front_matter =
381        Frontmatter(HashMap::with_capacity(table.len()));
382
383    for (key, value) in table {
384        let _ = front_matter.0.insert(key, toml_to_value(&value));
385    }
386
387    Ok(front_matter)
388}
389
390/// Converts a `toml::Value` into a `Value`.
391fn toml_to_value(toml: &TomlValue) -> Value {
392    match toml {
393        TomlValue::String(s) => Value::String(optimise_string(s)),
394        TomlValue::Integer(i) => Value::Number(*i as f64),
395        TomlValue::Float(f) => Value::Number(*f),
396        TomlValue::Boolean(b) => Value::Boolean(*b),
397        TomlValue::Array(arr) => {
398            let mut vec = Vec::with_capacity(arr.len());
399            vec.extend(arr.iter().map(toml_to_value));
400            Value::Array(vec)
401        }
402        TomlValue::Table(table) => {
403            let mut result =
404                Frontmatter(HashMap::with_capacity(table.len()));
405            for (k, v) in table {
406                let _ = result
407                    .0
408                    .insert(optimise_string(k), toml_to_value(v));
409            }
410            Value::Object(Box::new(result))
411        }
412        TomlValue::Datetime(dt) => Value::String(dt.to_string()),
413    }
414}
415
416/// Serialises a `Frontmatter` object into a TOML string.
417///
418/// # Arguments
419///
420/// * `front_matter` - The `Frontmatter` object to serialise.
421///
422/// # Returns
423///
424/// A `Result` containing the serialised TOML string or a `Error`.
425fn to_toml(front_matter: &Frontmatter) -> Result<String, Error> {
426    toml::to_string(&front_matter.0)
427        .map_err(|e| Error::ConversionError(e.to_string()))
428}
429
430// JSON Implementation
431// -------------------
432
433/// Parses a JSON string into a `Frontmatter` object.
434///
435/// # Arguments
436///
437/// * `raw` - The raw JSON string.
438///
439/// # Returns
440///
441/// A `Result` containing the parsed `Frontmatter` or a `Error`.
442fn parse_json(raw: &str) -> Result<Frontmatter, Error> {
443    let json_value: JsonValue = serde_json::from_str(raw)
444        .map_err(|e| Error::JsonParseError(Arc::new(e)))?;
445
446    let capacity = match &json_value {
447        JsonValue::Object(obj) => obj.len(),
448        _ => 0,
449    };
450
451    let mut front_matter =
452        Frontmatter(HashMap::with_capacity(capacity));
453
454    if let JsonValue::Object(obj) = json_value {
455        for (key, value) in obj {
456            let _ = front_matter.0.insert(key, json_to_value(&value));
457        }
458    }
459
460    Ok(front_matter)
461}
462
463/// Converts a `serde_json::Value` into a `Value`.
464fn json_to_value(json: &JsonValue) -> Value {
465    match json {
466        JsonValue::Null => Value::Null,
467        JsonValue::Bool(b) => Value::Boolean(*b),
468        JsonValue::Number(n) => n.as_i64().map_or_else(
469            || {
470                if let Some(f) = n.as_f64() {
471                    Value::Number(f)
472                } else {
473                    Value::Number(0.0)
474                }
475            },
476            |i| Value::Number(i as f64),
477        ),
478        JsonValue::String(s) => Value::String(optimise_string(s)),
479        JsonValue::Array(arr) => {
480            let mut vec = Vec::with_capacity(arr.len());
481            vec.extend(arr.iter().map(json_to_value));
482            Value::Array(vec)
483        }
484        JsonValue::Object(obj) => {
485            let mut result =
486                Frontmatter(HashMap::with_capacity(obj.len()));
487            for (k, v) in obj {
488                let _ = result
489                    .0
490                    .insert(optimise_string(k), json_to_value(v));
491            }
492            Value::Object(Box::new(result))
493        }
494    }
495}
496
497/// Optimised JSON serialisation with pre-allocated buffer.
498///
499/// # Arguments
500///
501/// * `front_matter` - The `Frontmatter` object to serialise.
502///
503/// # Returns
504///
505/// A `Result` containing the serialised JSON string or a `Error`.
506fn to_json_optimised(
507    front_matter: &Frontmatter,
508) -> Result<String, Error> {
509    let estimated_size = estimate_json_size(front_matter);
510    let buf = Vec::with_capacity(estimated_size);
511    let formatter = serde_json::ser::CompactFormatter;
512    let mut ser =
513        serde_json::Serializer::with_formatter(buf, formatter);
514
515    front_matter
516        .0
517        .serialize(&mut ser)
518        .map_err(|e| Error::ConversionError(e.to_string()))?;
519
520    String::from_utf8(ser.into_inner())
521        .map_err(|e| Error::ConversionError(e.to_string()))
522}
523
524// Validation and Utilities
525// ------------------------
526
527/// Validates a front matter structure against configured limits.
528///
529/// Checks:
530/// - Maximum nesting depth.
531/// - Maximum number of keys.
532/// - Structure validity.
533///
534/// # Arguments
535///
536/// * `fm` - Reference to the front matter to validate.
537/// * `max_depth` - Maximum allowed nesting depth.
538/// * `max_keys` - Maximum allowed number of keys.
539///
540/// # Returns
541///
542/// `Ok(())` if validation passes, `Error` otherwise.
543///
544/// # Errors
545///
546/// Returns `Error` if:
547/// - The number of keys exceeds `max_keys`.
548/// - The nesting depth exceeds `max_depth`.
549pub fn validate_frontmatter(
550    fm: &Frontmatter,
551    max_depth: usize,
552    max_keys: usize,
553) -> Result<(), Error> {
554    if fm.0.len() > max_keys {
555        return Err(Error::ContentTooLarge {
556            size: fm.0.len(),
557            max: max_keys,
558        });
559    }
560
561    // Validate nesting depth
562    for value in fm.0.values() {
563        check_depth(value, 1, max_depth)?;
564    }
565
566    Ok(())
567}
568
569/// Recursively checks the nesting depth of a value.
570///
571/// # Arguments
572///
573/// * `value` - The `Value` to check.
574/// * `current_depth` - The current depth of recursion.
575/// * `max_depth` - The maximum allowed depth.
576///
577/// # Returns
578///
579/// `Ok(())` if the depth is within limits, `Error` otherwise.
580fn check_depth(
581    value: &Value,
582    current_depth: usize,
583    max_depth: usize,
584) -> Result<(), Error> {
585    if current_depth > max_depth {
586        return Err(Error::NestingTooDeep {
587            depth: current_depth,
588            max: max_depth,
589        });
590    }
591
592    match value {
593        Value::Array(arr) => {
594            for item in arr {
595                check_depth(item, current_depth + 1, max_depth)?;
596            }
597        }
598        Value::Object(obj) => {
599            for v in obj.0.values() {
600                check_depth(v, current_depth + 1, max_depth)?;
601            }
602        }
603        _ => {}
604    }
605
606    Ok(())
607}
608
609/// Estimates the JSON string size for a front matter object.
610///
611/// Used for pre-allocating buffers in serialisation.
612///
613/// # Arguments
614///
615/// * `fm` - The `Frontmatter` object.
616///
617/// # Returns
618///
619/// An estimated size in bytes.
620fn estimate_json_size(fm: &Frontmatter) -> usize {
621    let mut size = 2; // {}
622    for (k, v) in &fm.0 {
623        size += k.len() + 3; // "key":
624        size += estimate_value_size(v);
625        size += 1; // ,
626    }
627    size
628}
629
630/// Estimates the serialised size of a value.
631///
632/// # Arguments
633///
634/// * `value` - The `Value` to estimate.
635///
636/// # Returns
637///
638/// An estimated size in bytes.
639fn estimate_value_size(value: &Value) -> usize {
640    match value {
641        Value::Null => 4,                // null
642        Value::String(s) => s.len() + 2, // "string"
643        Value::Number(_) => 8,           // average number length
644        Value::Boolean(_) => 5,          // false/true
645        Value::Array(arr) => {
646            2 + arr.iter().map(estimate_value_size).sum::<usize>() // []
647        }
648        Value::Object(obj) => estimate_json_size(obj),
649        Value::Tagged(tag, val) => {
650            tag.len() + 2 + estimate_value_size(val)
651        }
652    }
653}
654
655#[cfg(test)]
656mod tests {
657    use super::*;
658    use std::f64::consts::PI;
659
660    // Helper for creating a test `Frontmatter`
661    fn create_test_frontmatter() -> Frontmatter {
662        let mut fm = Frontmatter::new();
663        let _ = fm.insert(
664            "title".to_string(),
665            Value::String("Test".to_string()),
666        );
667        let _ = fm.insert("number".to_string(), Value::Number(PI));
668        let _ = fm.insert("boolean".to_string(), Value::Boolean(true));
669        let _ = fm.insert(
670            "array".to_string(),
671            Value::Array(vec![
672                Value::Number(1.0),
673                Value::Number(2.0),
674                Value::Number(3.0),
675            ]),
676        );
677        fm
678    }
679
680    /// Tests for `ParseOptions::default`.
681    mod parse_options_tests {
682        use super::*;
683
684        #[test]
685        fn test_parse_options_default() {
686            let default_options = ParseOptions::default();
687            assert_eq!(default_options.max_depth, MAX_NESTING_DEPTH);
688            assert_eq!(default_options.max_keys, MAX_KEYS);
689            assert!(default_options.validate);
690        }
691    }
692
693    /// Tests for `optimise_string`.
694    mod optimise_string_tests {
695        use super::*;
696
697        #[test]
698        fn test_optimise_string_short() {
699            let short_string = "short";
700            let optimised = optimise_string(short_string);
701            assert_eq!(optimised, short_string);
702            assert_eq!(optimised.capacity(), short_string.len());
703        }
704
705        #[test]
706        fn test_optimise_string_long() {
707            let long_string = "a".repeat(SMALL_STRING_SIZE + 1);
708            let optimised = optimise_string(&long_string);
709            assert_eq!(optimised, long_string);
710            assert!(optimised.capacity() >= long_string.len());
711        }
712    }
713
714    /// Tests for parsing functions.
715    mod parsing_tests {
716        use super::*;
717
718        #[test]
719        fn test_parse_yaml() {
720            let yaml = "key: value";
721            let result = parse_yaml(yaml);
722            assert!(result.is_ok());
723            let fm = result.unwrap();
724            assert_eq!(
725                fm.0.get("key"),
726                Some(&Value::String("value".to_string()))
727            );
728        }
729
730        #[test]
731        fn test_parse_toml() {
732            let toml = "key = \"value\"";
733            let result = parse_toml(toml);
734            assert!(result.is_ok());
735            let fm = result.unwrap();
736            assert_eq!(
737                fm.0.get("key"),
738                Some(&Value::String("value".to_string()))
739            );
740        }
741
742        #[test]
743        fn test_parse_json() {
744            let json = r#"{"key": "value"}"#;
745            let result = parse_json(json);
746            assert!(result.is_ok());
747            let fm = result.unwrap();
748            assert_eq!(
749                fm.0.get("key"),
750                Some(&Value::String("value".to_string()))
751            );
752        }
753
754        #[test]
755        fn test_parse_with_options() {
756            let yaml = "key: value";
757            let result = parse_with_options(yaml, Format::Yaml, None);
758            assert!(result.is_ok());
759            let fm = result.unwrap();
760            assert_eq!(
761                fm.0.get("key"),
762                Some(&Value::String("value".to_string()))
763            );
764        }
765
766        #[test]
767        fn test_parse_with_invalid_format() {
768            let yaml = "key: value";
769            let result =
770                parse_with_options(yaml, Format::Unsupported, None);
771            assert!(matches!(result, Err(Error::ConversionError(_))));
772        }
773    }
774
775    /// Tests for serialization functions.
776    mod serialization_tests {
777        use super::*;
778
779        #[test]
780        fn test_to_yaml() {
781            let fm = create_test_frontmatter();
782            let yaml = to_yaml(&fm).unwrap();
783            assert!(yaml.contains("title:"));
784            assert!(yaml.contains("Test"));
785        }
786
787        #[test]
788        fn test_to_toml() {
789            let fm = create_test_frontmatter();
790            let toml = to_toml(&fm).unwrap();
791            assert!(toml.contains("title = \"Test\""));
792        }
793
794        #[test]
795        fn test_to_json_optimised() {
796            let fm = create_test_frontmatter();
797            let json = to_json_optimised(&fm).unwrap();
798            assert!(json.contains("\"title\":\"Test\""));
799        }
800
801        #[test]
802        fn test_to_string() {
803            let fm = create_test_frontmatter();
804
805            // Test YAML format
806            let yaml = to_string(&fm, Format::Yaml).unwrap();
807            assert!(yaml.contains("title: Test"));
808
809            // Test TOML format
810            let toml = to_string(&fm, Format::Toml).unwrap();
811            assert!(toml.contains("title = \"Test\""));
812
813            // Test JSON format
814            let json = to_string(&fm, Format::Json).unwrap();
815            assert!(json.contains("\"title\":\"Test\""));
816        }
817    }
818
819    /// Tests for validation functions.
820    mod validation_tests {
821        use super::*;
822
823        #[test]
824        fn test_validate_frontmatter_valid() {
825            let fm = create_test_frontmatter();
826            assert!(validate_frontmatter(
827                &fm,
828                MAX_NESTING_DEPTH,
829                MAX_KEYS
830            )
831            .is_ok());
832        }
833
834        #[test]
835        fn test_validate_frontmatter_exceeds_keys() {
836            let mut fm = Frontmatter::new();
837            for i in 0..MAX_KEYS + 1 {
838                let _ = fm.insert(
839                    i.to_string(),
840                    Value::String("value".to_string()),
841                );
842            }
843            let result =
844                validate_frontmatter(&fm, MAX_NESTING_DEPTH, MAX_KEYS);
845            assert!(matches!(
846                result,
847                Err(Error::ContentTooLarge { .. })
848            ));
849        }
850
851        #[test]
852        fn test_validate_frontmatter_exceeds_depth() {
853            let mut current = Value::Null;
854            for _ in 0..MAX_NESTING_DEPTH + 1 {
855                current = Value::Object(Box::new(Frontmatter(
856                    [("nested".to_string(), current)]
857                        .into_iter()
858                        .collect(),
859                )));
860            }
861            let mut fm = Frontmatter::new();
862            let _ = fm.insert("deep".to_string(), current);
863            let result =
864                validate_frontmatter(&fm, MAX_NESTING_DEPTH, MAX_KEYS);
865            assert!(matches!(
866                result,
867                Err(Error::NestingTooDeep { .. })
868            ));
869        }
870    }
871
872    /// Tests for utility functions.
873    mod utility_tests {
874        use super::*;
875
876        #[test]
877        fn test_estimate_json_size() {
878            let fm = create_test_frontmatter();
879            let estimated_size = estimate_json_size(&fm);
880            let actual_json = to_string(&fm, Format::Json).unwrap();
881            assert!(estimated_size >= actual_json.len());
882        }
883
884        #[test]
885        fn test_check_depth_valid() {
886            let value =
887                Value::Object(Box::new(create_test_frontmatter()));
888            assert!(check_depth(&value, 1, MAX_NESTING_DEPTH).is_ok());
889        }
890
891        #[test]
892        fn test_check_depth_exceeds() {
893            let mut current = Value::Null;
894            for _ in 0..MAX_NESTING_DEPTH + 1 {
895                current = Value::Object(Box::new(Frontmatter(
896                    [("nested".to_string(), current)]
897                        .into_iter()
898                        .collect(),
899                )));
900            }
901            let result = check_depth(&current, 1, MAX_NESTING_DEPTH);
902            assert!(matches!(
903                result,
904                Err(Error::NestingTooDeep { .. })
905            ));
906        }
907    }
908}