1use 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
44const SMALL_STRING_SIZE: usize = 24;
46const MAX_NESTING_DEPTH: usize = 32;
47const MAX_KEYS: usize = 1000;
48
49#[derive(Debug, Clone, Copy)]
54pub struct ParseOptions {
55 pub max_depth: usize,
57 pub max_keys: usize,
59 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#[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
96pub 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 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 let trimmed_content = raw_front_matter.trim();
137
138 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!(), };
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 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
201pub 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
225pub 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
255fn parse_yaml(raw: &str) -> Result<Frontmatter, Error> {
268 let yaml_value: YamlValue = noyalib::from_str(raw)
270 .map_err(|e| Error::YamlParseError { source: e.into() })?;
271
272 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 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
296fn 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 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) }
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 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
348fn 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
362fn parse_toml(raw: &str) -> Result<Frontmatter, Error> {
375 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
390fn 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
416fn 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
430fn 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
463fn 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
497fn 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
524pub 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 for value in fm.0.values() {
563 check_depth(value, 1, max_depth)?;
564 }
565
566 Ok(())
567}
568
569fn 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
609fn estimate_json_size(fm: &Frontmatter) -> usize {
621 let mut size = 2; for (k, v) in &fm.0 {
623 size += k.len() + 3; size += estimate_value_size(v);
625 size += 1; }
627 size
628}
629
630fn estimate_value_size(value: &Value) -> usize {
640 match value {
641 Value::Null => 4, Value::String(s) => s.len() + 2, Value::Number(_) => 8, Value::Boolean(_) => 5, Value::Array(arr) => {
646 2 + arr.iter().map(estimate_value_size).sum::<usize>() }
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 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 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 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 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 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 let yaml = to_string(&fm, Format::Yaml).unwrap();
807 assert!(yaml.contains("title: Test"));
808
809 let toml = to_string(&fm, Format::Toml).unwrap();
811 assert!(toml.contains("title = \"Test\""));
812
813 let json = to_string(&fm, Format::Json).unwrap();
815 assert!(json.contains("\"title\":\"Test\""));
816 }
817 }
818
819 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 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(¤t, 1, MAX_NESTING_DEPTH);
902 assert!(matches!(
903 result,
904 Err(Error::NestingTooDeep { .. })
905 ));
906 }
907 }
908}