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 let trimmed = raw_front_matter.trim();
127
128 let front_matter = match format {
129 Format::Yaml => parse_yaml_checked(trimmed)?,
130 Format::Toml => parse_toml_checked(trimmed)?,
131 Format::Json => parse_json_checked(trimmed)?,
132 Format::Unsupported => parse_unsupported(format)?,
133 };
134
135 if options.validate {
137 let max_depth = options.max_depth;
138 let max_keys = options.max_keys;
139 log::debug!("Validating front matter: maximum allowed nesting depth is {max_depth}, maximum allowed number of keys is {max_keys}");
140
141 validate_frontmatter(&front_matter, max_depth, max_keys)
142 .map_err(|e| {
143 log::error!("Front matter validation failed: {}", e);
144 e
145 })?;
146 }
147
148 Ok(front_matter)
149}
150
151fn parse_yaml_checked(trimmed: &str) -> Result<Frontmatter, Error> {
153 if !trimmed.starts_with("---") {
154 log::debug!("YAML front matter validation: Content structure appears non-standard");
155 }
156 parse_yaml(trimmed).map_err(|e| {
157 log::error!("YAML parsing failed: {}", e);
158 e
159 })
160}
161
162const ERR_TOML_MISSING_EQ: &str =
163 "Format set to TOML but input does not contain '=' signs.";
164const ERR_JSON_MISSING_BRACE: &str =
165 "Format set to JSON but input does not start with '{'.";
166
167fn parse_toml_checked(trimmed: &str) -> Result<Frontmatter, Error> {
169 if !trimmed.contains('=') {
170 let msg = ERR_TOML_MISSING_EQ.to_string();
171 return Err(Error::ConversionError(msg));
172 }
173 parse_toml(trimmed).map_err(|e| {
174 log::error!("TOML parsing failed: {}", e);
175 e
176 })
177}
178
179fn parse_json_checked(trimmed: &str) -> Result<Frontmatter, Error> {
181 if !trimmed.starts_with('{') {
182 let msg = ERR_JSON_MISSING_BRACE.to_string();
183 return Err(Error::ConversionError(msg));
184 }
185 parse_json(trimmed).map_err(|e| {
186 log::error!("JSON parsing failed: {}", e);
187 e
188 })
189}
190
191fn parse_unsupported(format: Format) -> Result<Frontmatter, Error> {
193 let err_msg = format!("Unsupported format: {format:?}. Supported formats are YAML, TOML, and JSON.");
194 log::error!("{err_msg}");
195 Err(Error::ConversionError(err_msg))
196}
197
198pub fn parse(
216 raw_front_matter: &str,
217 format: Format,
218) -> Result<Frontmatter, Error> {
219 parse_with_options(raw_front_matter, format, None)
220}
221
222pub fn to_string(
239 front_matter: &Frontmatter,
240 format: Format,
241) -> Result<String, Error> {
242 match format {
243 Format::Yaml => to_yaml(front_matter),
244 Format::Toml => to_toml(front_matter),
245 Format::Json => to_json_optimised(front_matter),
246 Format::Unsupported => Err(Error::ConversionError(
247 "Unsupported format".to_string(),
248 )),
249 }
250}
251
252fn parse_yaml(raw: &str) -> Result<Frontmatter, Error> {
265 let yaml_value: YamlValue = noyalib::from_str(raw)
267 .map_err(|e| Error::YamlParseError { source: e.into() })?;
268
269 let capacity =
271 yaml_value.as_mapping().map_or(0, noyalib::Mapping::len);
272 let mut front_matter =
273 Frontmatter(HashMap::with_capacity(capacity));
274
275 if let YamlValue::Mapping(mapping) = yaml_value {
281 for (key, value) in mapping {
282 let _ = front_matter.insert(key, yaml_into_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_into_value(yaml: YamlValue) -> Value {
310 match yaml {
311 YamlValue::String(s) => Value::String(s),
312 YamlValue::Sequence(seq) => {
313 Value::Array(seq.into_iter().map(yaml_into_value).collect())
314 }
315 YamlValue::Mapping(map) => {
316 let mut inner = HashMap::with_capacity(map.len());
317 for (k, v) in map {
318 let _ = inner.insert(k, yaml_into_value(v));
319 }
320 Value::Object(Box::new(Frontmatter(inner)))
321 }
322 other => yaml_to_value(&other),
325 }
326}
327
328fn yaml_to_value(yaml: &YamlValue) -> Value {
330 match yaml {
331 YamlValue::Null => Value::Null,
332 YamlValue::Bool(b) => Value::Boolean(*b),
333 YamlValue::Number(n) => {
334 n.as_i64().map_or_else(
340 || Value::Number(n.as_f64()),
341 |i| {
342 if i.abs() < (1_i64 << 52) {
343 Value::Number(i as f64)
344 } else {
345 log::warn!(
346 "Integer {} exceeds precision of f64. Defaulting to 0.0",
347 i
348 );
349 Value::Number(0.0) }
351 },
352 )
353 }
354 YamlValue::String(s) => Value::String(optimise_string(s)),
355 YamlValue::Sequence(seq) => {
356 let mut vec = Vec::with_capacity(seq.len());
357 vec.extend(seq.iter().map(yaml_to_value));
358 Value::Array(vec)
359 }
360 YamlValue::Mapping(map) => {
361 let mut result =
365 Frontmatter(HashMap::with_capacity(map.len()));
366 for (k, v) in map {
367 let key = optimise_string(k);
368 let _ = result.insert(key, yaml_to_value(v));
369 }
370 Value::Object(Box::new(result))
371 }
372 YamlValue::Tagged(tagged) => Value::Tagged(
373 optimise_string(tagged.tag().as_ref()),
374 Box::new(yaml_to_value(tagged.value())),
375 ),
376 }
377}
378
379fn to_yaml(front_matter: &Frontmatter) -> Result<String, Error> {
389 noyalib::to_string(&front_matter.0)
390 .map_err(|e| Error::ConversionError(e.to_string()))
391}
392
393fn parse_toml(raw: &str) -> Result<Frontmatter, Error> {
406 let table: toml::Table =
409 raw.parse().map_err(Error::TomlParseError)?;
410
411 let mut front_matter =
412 Frontmatter(HashMap::with_capacity(table.len()));
413
414 for (key, value) in table {
415 let _ = front_matter.0.insert(key, toml_to_value(&value));
416 }
417
418 Ok(front_matter)
419}
420
421fn toml_to_value(toml: &TomlValue) -> Value {
423 match toml {
424 TomlValue::String(s) => Value::String(optimise_string(s)),
425 TomlValue::Integer(i) => Value::Number(*i as f64),
426 TomlValue::Float(f) => Value::Number(*f),
427 TomlValue::Boolean(b) => Value::Boolean(*b),
428 TomlValue::Array(arr) => {
429 let mut vec = Vec::with_capacity(arr.len());
430 vec.extend(arr.iter().map(toml_to_value));
431 Value::Array(vec)
432 }
433 TomlValue::Table(table) => {
434 let mut result =
435 Frontmatter(HashMap::with_capacity(table.len()));
436 for (k, v) in table {
437 let key = optimise_string(k);
438 let _ = result.insert(key, toml_to_value(v));
439 }
440 Value::Object(Box::new(result))
441 }
442 TomlValue::Datetime(dt) => Value::String(dt.to_string()),
443 }
444}
445
446fn to_toml(front_matter: &Frontmatter) -> Result<String, Error> {
456 toml::to_string(&front_matter.0)
457 .map_err(|e| Error::ConversionError(e.to_string()))
458}
459
460fn parse_json(raw: &str) -> Result<Frontmatter, Error> {
473 let json_value: JsonValue = serde_json::from_str(raw)
474 .map_err(|e| Error::JsonParseError(Arc::new(e)))?;
475
476 let capacity = match &json_value {
477 JsonValue::Object(obj) => obj.len(),
478 _ => 0,
479 };
480
481 let mut front_matter =
482 Frontmatter(HashMap::with_capacity(capacity));
483
484 if let JsonValue::Object(obj) = json_value {
485 for (key, value) in obj {
486 let _ = front_matter.0.insert(key, json_to_value(&value));
487 }
488 }
489
490 Ok(front_matter)
491}
492
493fn json_to_value(json: &JsonValue) -> Value {
495 match json {
496 JsonValue::Null => Value::Null,
497 JsonValue::Bool(b) => Value::Boolean(*b),
498 JsonValue::Number(n) => n.as_i64().map_or_else(
499 || Value::Number(n.as_f64().unwrap_or(0.0)),
500 |i| Value::Number(i as f64),
501 ),
502 JsonValue::String(s) => Value::String(optimise_string(s)),
503 JsonValue::Array(arr) => {
504 let mut vec = Vec::with_capacity(arr.len());
505 vec.extend(arr.iter().map(json_to_value));
506 Value::Array(vec)
507 }
508 JsonValue::Object(obj) => {
509 let mut result =
510 Frontmatter(HashMap::with_capacity(obj.len()));
511 for (k, v) in obj {
512 let key = optimise_string(k);
513 let _ = result.insert(key, json_to_value(v));
514 }
515 Value::Object(Box::new(result))
516 }
517 }
518}
519
520fn to_json_optimised(
530 front_matter: &Frontmatter,
531) -> Result<String, Error> {
532 let estimated_size = estimate_json_size(front_matter);
533 let buf = Vec::with_capacity(estimated_size);
534 let mut ser = serde_json::Serializer::new(buf);
535
536 let _ = front_matter.0.serialize(&mut ser);
537
538 Ok(String::from_utf8(ser.into_inner()).unwrap_or_default())
539}
540
541pub fn validate_frontmatter(
567 fm: &Frontmatter,
568 max_depth: usize,
569 max: usize,
570) -> Result<(), Error> {
571 let size = fm.0.len();
572 if size > max {
573 return Err(Error::ContentTooLarge { size, max });
574 }
575
576 for value in fm.0.values() {
578 check_depth(value, 1, max_depth)?;
579 }
580
581 Ok(())
582}
583
584fn check_depth(
596 value: &Value,
597 depth: usize,
598 max: usize,
599) -> Result<(), Error> {
600 if depth > max {
601 return Err(Error::NestingTooDeep { depth, max });
602 }
603
604 if let Value::Array(arr) = value {
605 for item in arr {
606 check_depth(item, depth + 1, max)?;
607 }
608 } else if let Value::Object(obj) = value {
609 for v in obj.0.values() {
610 check_depth(v, depth + 1, max)?;
611 }
612 }
613
614 Ok(())
615}
616
617fn estimate_json_size(fm: &Frontmatter) -> usize {
629 let mut size = 2; for (k, v) in &fm.0 {
631 size += k.len() + 3; size += estimate_value_size(v);
633 size += 1; }
635 size
636}
637
638fn estimate_value_size(value: &Value) -> usize {
648 match value {
649 Value::Null => 4, Value::String(s) => s.len() + 2, Value::Number(_) => 8, Value::Boolean(_) => 5, Value::Array(arr) => {
654 2 + arr.iter().map(estimate_value_size).sum::<usize>() }
656 Value::Object(obj) => estimate_json_size(obj),
657 Value::Tagged(tag, val) => {
658 tag.len() + 2 + estimate_value_size(val)
659 }
660 }
661}
662
663#[cfg(test)]
664mod tests {
665 #[test]
674 fn yaml_to_value_borrowed_handles_every_scalar_arm() {
675 let doc = "nothing: ~\nratio: 2.5\nhuge: 9007199254740993\ntagged: !custom text\n";
676 let root: YamlValue = noyalib::from_str(doc).expect("yaml");
677
678 assert_eq!(
679 yaml_to_value(root.get("nothing").expect("nothing")),
680 Value::Null
681 );
682 assert_eq!(
683 yaml_to_value(root.get("ratio").expect("ratio")),
684 Value::Number(2.5)
685 );
686 assert_eq!(
689 yaml_to_value(root.get("huge").expect("huge")),
690 Value::Number(0.0)
691 );
692 let tagged_val =
693 yaml_to_value(root.get("tagged").expect("tagged"));
694 let (tag, inner) =
695 tagged_val.as_tagged().expect("expected tagged");
696 assert!(tag.contains("custom"), "tag was {tag:?}");
697 assert_eq!(*inner, Value::String("text".into()));
698 }
699
700 #[test]
707 fn yaml_to_value_borrowed_handles_compound_values() {
708 let doc = "list:\n - one\n - two\nmap:\n k: v\n";
709 let root: YamlValue = noyalib::from_str(doc).expect("yaml");
710 let list = root.get("list").expect("list");
711 let map = root.get("map").expect("map");
712
713 let list_val = yaml_to_value(list);
714 let items = list_val.as_array().expect("expected array");
715 assert_eq!(items.len(), 2);
716 assert_eq!(items[0], Value::String("one".into()));
717
718 let map_val = yaml_to_value(map);
719 let obj = map_val.as_object().expect("expected object");
720 assert_eq!(obj.get("k"), Some(&Value::String("v".into())));
721 }
722
723 #[test]
725 fn yaml_into_value_matches_yaml_to_value() {
726 let doc =
727 "s: text\nn: 3\nb: true\nlist:\n - a\nmap:\n x: y\n";
728 let root: YamlValue = noyalib::from_str(doc).expect("yaml");
729 let m = root.as_mapping().cloned().unwrap();
730 for (k, v) in m {
731 let borrowed = yaml_to_value(&v);
732 let moved = yaml_into_value(v);
733 assert_eq!(borrowed, moved, "key {k}");
734 }
735 }
736
737 #[test]
742 fn readme_install_snippet_matches_cargo_version() {
743 let readme = include_str!("../README.md");
744 let version = env!("CARGO_PKG_VERSION");
745 let expected = format!("frontmatter-gen = \"{version}\"");
746 assert!(
747 readme.contains(&expected),
748 "README does not contain `{expected}`; update the install \
749 snippet when bumping Cargo.toml"
750 );
751 }
752
753 use super::*;
754 use std::f64::consts::PI;
755
756 fn create_test_frontmatter() -> Frontmatter {
758 let mut fm = Frontmatter::new();
759 let _ = fm.insert(
760 "title".to_string(),
761 Value::String("Test".to_string()),
762 );
763 let _ = fm.insert("number".to_string(), Value::Number(PI));
764 let _ = fm.insert("boolean".to_string(), Value::Boolean(true));
765 let _ = fm.insert(
766 "array".to_string(),
767 Value::Array(vec![
768 Value::Number(1.0),
769 Value::Number(2.0),
770 Value::Number(3.0),
771 ]),
772 );
773 fm
774 }
775
776 mod parse_options_tests {
778 use super::*;
779
780 #[test]
781 fn test_parse_options_default() {
782 let default_options = ParseOptions::default();
783 assert_eq!(default_options.max_depth, MAX_NESTING_DEPTH);
784 assert_eq!(default_options.max_keys, MAX_KEYS);
785 assert!(default_options.validate);
786 }
787 }
788
789 mod optimise_string_tests {
791 use super::*;
792
793 #[test]
794 fn test_optimise_string_short() {
795 let short_string = "short";
796 let optimised = optimise_string(short_string);
797 assert_eq!(optimised, short_string);
798 assert_eq!(optimised.capacity(), short_string.len());
799 }
800
801 #[test]
802 fn test_optimise_string_long() {
803 let long_string = "a".repeat(SMALL_STRING_SIZE + 1);
804 let optimised = optimise_string(&long_string);
805 assert_eq!(optimised, long_string);
806 assert!(optimised.capacity() >= long_string.len());
807 }
808 }
809
810 mod parsing_tests {
812 use super::*;
813
814 #[test]
815 fn test_parse_yaml() {
816 let yaml = "key: value";
817 let result = parse_yaml(yaml);
818 assert!(result.is_ok());
819 let fm = result.unwrap();
820 assert_eq!(
821 fm.0.get("key"),
822 Some(&Value::String("value".to_string()))
823 );
824 }
825
826 #[test]
827 fn test_parse_toml() {
828 let toml = "key = \"value\"";
829 let result = parse_toml(toml);
830 assert!(result.is_ok());
831 let fm = result.unwrap();
832 assert_eq!(
833 fm.0.get("key"),
834 Some(&Value::String("value".to_string()))
835 );
836 }
837
838 #[test]
839 fn test_parse_json() {
840 let json = r#"{"key": "value"}"#;
841 let result = parse_json(json);
842 assert!(result.is_ok());
843 let fm = result.unwrap();
844 assert_eq!(
845 fm.0.get("key"),
846 Some(&Value::String("value".to_string()))
847 );
848 }
849
850 #[test]
851 fn test_parse_with_options() {
852 let yaml = "key: value";
853 let result = parse_with_options(yaml, Format::Yaml, None);
854 assert!(result.is_ok());
855 let fm = result.unwrap();
856 assert_eq!(
857 fm.0.get("key"),
858 Some(&Value::String("value".to_string()))
859 );
860 }
861
862 #[test]
863 fn test_parse_with_invalid_format() {
864 let yaml = "key: value";
865 let result =
866 parse_with_options(yaml, Format::Unsupported, None);
867 assert!(matches!(result, Err(Error::ConversionError(_))));
868 }
869 }
870
871 mod serialization_tests {
873 use super::*;
874
875 #[test]
876 fn test_to_yaml() {
877 let fm = create_test_frontmatter();
878 let yaml = to_yaml(&fm).unwrap();
879 assert!(yaml.contains("title:"));
880 assert!(yaml.contains("Test"));
881 }
882
883 #[test]
884 fn test_to_toml() {
885 let fm = create_test_frontmatter();
886 let toml = to_toml(&fm).unwrap();
887 assert!(toml.contains("title = \"Test\""));
888 }
889
890 #[test]
891 fn test_to_json_optimised() {
892 let fm = create_test_frontmatter();
893 let json = to_json_optimised(&fm).unwrap();
894 assert!(json.contains("\"title\":\"Test\""));
895 }
896
897 #[test]
898 fn test_to_string() {
899 let fm = create_test_frontmatter();
900
901 let yaml = to_string(&fm, Format::Yaml).unwrap();
903 assert!(yaml.contains("title: Test"));
904
905 let toml = to_string(&fm, Format::Toml).unwrap();
907 assert!(toml.contains("title = \"Test\""));
908
909 let json = to_string(&fm, Format::Json).unwrap();
911 assert!(json.contains("\"title\":\"Test\""));
912 }
913 }
914
915 mod validation_tests {
917 use super::*;
918
919 #[test]
920 fn test_validate_frontmatter_valid() {
921 let fm = create_test_frontmatter();
922 assert!(validate_frontmatter(
923 &fm,
924 MAX_NESTING_DEPTH,
925 MAX_KEYS
926 )
927 .is_ok());
928 }
929
930 #[test]
931 fn test_validate_frontmatter_exceeds_keys() {
932 let mut fm = Frontmatter::new();
933 for i in 0..MAX_KEYS + 1 {
934 let _ = fm.insert(
935 i.to_string(),
936 Value::String("value".to_string()),
937 );
938 }
939 let result =
940 validate_frontmatter(&fm, MAX_NESTING_DEPTH, MAX_KEYS);
941 assert!(matches!(
942 result,
943 Err(Error::ContentTooLarge { .. })
944 ));
945 }
946
947 #[test]
948 fn test_validate_frontmatter_exceeds_depth() {
949 let mut current = Value::Null;
950 for _ in 0..MAX_NESTING_DEPTH + 1 {
951 current = Value::Object(Box::new(Frontmatter(
952 [("nested".to_string(), current)]
953 .into_iter()
954 .collect(),
955 )));
956 }
957 let mut fm = Frontmatter::new();
958 let _ = fm.insert("deep".to_string(), current);
959 let result =
960 validate_frontmatter(&fm, MAX_NESTING_DEPTH, MAX_KEYS);
961 assert!(matches!(
962 result,
963 Err(Error::NestingTooDeep { .. })
964 ));
965 }
966 }
967
968 mod utility_tests {
970 use super::*;
971
972 #[test]
973 fn test_estimate_json_size() {
974 let fm = create_test_frontmatter();
975 let estimated_size = estimate_json_size(&fm);
976 let actual_json = to_string(&fm, Format::Json).unwrap();
977 assert!(estimated_size >= actual_json.len());
978 }
979
980 #[test]
981 fn test_check_depth_valid() {
982 let value =
983 Value::Object(Box::new(create_test_frontmatter()));
984 assert!(check_depth(&value, 1, MAX_NESTING_DEPTH).is_ok());
985 }
986
987 #[test]
988 fn test_check_depth_exceeds() {
989 let mut current = Value::Null;
990 for _ in 0..MAX_NESTING_DEPTH + 1 {
991 current = Value::Object(Box::new(Frontmatter(
992 [("nested".to_string(), current)]
993 .into_iter()
994 .collect(),
995 )));
996 }
997 let result = check_depth(¤t, 1, MAX_NESTING_DEPTH);
998 assert!(matches!(
999 result,
1000 Err(Error::NestingTooDeep { .. })
1001 ));
1002 }
1003 }
1004}
1005
1006#[cfg(test)]
1007mod exhaustive_parser_tests {
1008 use super::*;
1017
1018 fn get<'a>(fm: &'a Frontmatter, key: &str) -> &'a Value {
1019 fm.get(key).unwrap()
1020 }
1021
1022 #[test]
1023 fn toml_converts_every_scalar_and_container_kind() {
1024 let raw = "\
1025s = \"text\"
1026i = 7
1027f = 2.5
1028b = true
1029when = 2024-01-02
1030arr = [1, 2]
1031nested = [[1], [2]]
1032
1033[table]
1034inner = \"x\"
1035";
1036 let fm = parse_toml(raw).expect("valid TOML");
1037 assert_eq!(get(&fm, "s"), &Value::String("text".into()));
1038 assert_eq!(get(&fm, "i"), &Value::Number(7.0));
1039 assert_eq!(get(&fm, "f"), &Value::Number(2.5));
1040 assert_eq!(get(&fm, "b"), &Value::Boolean(true));
1041 assert!(
1043 matches!(get(&fm, "when"), Value::String(s) if s.contains("2024"))
1044 );
1045 assert_eq!(get(&fm, "arr").array_len(), Some(2));
1046 assert_eq!(get(&fm, "nested").array_len(), Some(2));
1047 let table = get(&fm, "table")
1048 .as_object()
1049 .expect("table should convert to an object");
1050 assert_eq!(
1051 table.get("inner"),
1052 Some(&Value::String("x".into()))
1053 );
1054 }
1055
1056 #[test]
1057 fn json_converts_every_scalar_and_container_kind() {
1058 let raw = r#"{
1059 "null": null,
1060 "bool": false,
1061 "int": 7,
1062 "float": 2.5,
1063 "big": 12345678901234567890,
1064 "str": "text",
1065 "arr": [1, "a", null],
1066 "obj": {"inner": {"deep": 1}}
1067 }"#;
1068 let fm = parse_json(raw).expect("valid JSON");
1069 assert_eq!(get(&fm, "null"), &Value::Null);
1070 assert_eq!(get(&fm, "bool"), &Value::Boolean(false));
1071 assert_eq!(get(&fm, "int"), &Value::Number(7.0));
1072 assert_eq!(get(&fm, "float"), &Value::Number(2.5));
1073 assert!(
1075 matches!(get(&fm, "big"), Value::Number(n) if *n > 1e19)
1076 );
1077 assert_eq!(get(&fm, "str"), &Value::String("text".into()));
1078 assert_eq!(get(&fm, "arr").array_len(), Some(3));
1079 let obj = get(&fm, "obj").as_object().expect("nested object");
1080 assert!(matches!(obj.get("inner"), Some(Value::Object(_))));
1081 }
1082
1083 #[test]
1084 fn yaml_converts_every_scalar_and_container_kind() {
1085 let raw = "\
1086s: text
1087i: 7
1088f: 2.5
1089b: true
1090n: null
1091arr:
1092 - 1
1093 - a
1094obj:
1095 inner: x
1096";
1097 let fm = parse_yaml(raw).expect("valid YAML");
1098 assert_eq!(get(&fm, "s"), &Value::String("text".into()));
1099 assert_eq!(get(&fm, "i"), &Value::Number(7.0));
1100 assert_eq!(get(&fm, "b"), &Value::Boolean(true));
1101 assert_eq!(get(&fm, "n"), &Value::Null);
1102 assert_eq!(get(&fm, "arr").array_len(), Some(2));
1103 assert!(matches!(get(&fm, "obj"), Value::Object(_)));
1104 }
1105
1106 #[test]
1107 fn the_three_converters_agree_on_the_same_document() {
1108 let yaml =
1109 parse_yaml("a: 1\nb: text\nc: true\n").expect("yaml");
1110 let toml = parse_toml("a = 1\nb = \"text\"\nc = true\n")
1111 .expect("toml");
1112 let json = parse_json(r#"{"a": 1, "b": "text", "c": true}"#)
1113 .expect("json");
1114 for key in ["a", "b", "c"] {
1115 assert_eq!(
1116 get(&yaml, key),
1117 get(&toml, key),
1118 "yaml vs toml on {key}"
1119 );
1120 assert_eq!(
1121 get(&yaml, key),
1122 get(&json, key),
1123 "yaml vs json on {key}"
1124 );
1125 }
1126 }
1127
1128 #[test]
1129 fn format_mismatch_is_rejected_before_parsing() {
1130 let err =
1131 parse_with_options("no equals here", Format::Toml, None)
1132 .expect_err("TOML without '=' is rejected");
1133 assert!(
1134 matches!(err, Error::ConversionError(ref m) if m.contains("TOML"))
1135 );
1136
1137 let err =
1138 parse_with_options("not an object", Format::Json, None)
1139 .expect_err("JSON not starting with '{' is rejected");
1140 assert!(
1141 matches!(err, Error::ConversionError(ref m) if m.contains("JSON"))
1142 );
1143 }
1144
1145 #[test]
1146 fn validation_limits_are_enforced_only_when_asked() {
1147 let deep = r#"{"a": {"b": {"c": {"d": 1}}}}"#;
1148
1149 let options = ParseOptions {
1151 max_depth: 2,
1152 max_keys: 100,
1153 validate: false,
1154 };
1155 assert!(parse_with_options(deep, Format::Json, Some(options))
1156 .is_ok());
1157
1158 let options = ParseOptions {
1159 max_depth: 2,
1160 max_keys: 100,
1161 validate: true,
1162 };
1163 let err = parse_with_options(deep, Format::Json, Some(options))
1164 .expect_err("nesting beyond max_depth is rejected");
1165 assert!(matches!(err, Error::NestingTooDeep { .. }), "{err:?}");
1166
1167 let wide = r#"{"a": 1, "b": 2, "c": 3}"#;
1168 let options = ParseOptions {
1169 max_depth: 10,
1170 max_keys: 2,
1171 validate: true,
1172 };
1173 assert!(parse_with_options(wide, Format::Json, Some(options))
1174 .is_err());
1175 }
1176
1177 #[test]
1178 fn round_trips_through_every_format() {
1179 let fm = parse_json(r#"{"title": "T", "n": 2, "flag": true}"#)
1180 .expect("json");
1181 for format in [Format::Yaml, Format::Toml, Format::Json] {
1182 let text = to_string(&fm, format).expect("serialise");
1183 let back = parse(&text, format).expect("re-parse");
1184 assert_eq!(
1185 back.get("title"),
1186 Some(&Value::String("T".into())),
1187 "{format} lost the title"
1188 );
1189 assert_eq!(back.get("flag"), Some(&Value::Boolean(true)));
1190 }
1191 }
1192
1193 #[test]
1194 fn non_mapping_yaml_and_non_object_json_and_deep_yaml_serialisation(
1195 ) {
1196 log::set_max_level(log::LevelFilter::Trace);
1197
1198 let with_delim = parse_with_options(
1200 "---\ntitle: test\n",
1201 Format::Yaml,
1202 None,
1203 )
1204 .expect("yaml with ---");
1205 assert_eq!(
1206 with_delim.get("title"),
1207 Some(&Value::String("test".into()))
1208 );
1209
1210 assert!(matches!(
1212 parse_yaml("- item1\n- item2\n"),
1213 Err(Error::ParseError(_))
1214 ));
1215
1216 let empty = parse_json("[1, 2, 3]").expect("json array");
1218 assert!(empty.is_empty());
1219
1220 let mut null_fm = Frontmatter::new();
1222 let _ = null_fm.insert("k".into(), Value::Null);
1223 assert!(matches!(
1224 to_toml(&null_fm),
1225 Err(Error::ConversionError(_))
1226 ));
1227
1228 let mut deep_val = Value::Null;
1230 for _ in 0..150 {
1231 deep_val = Value::Array(vec![deep_val]);
1232 }
1233 let mut deep_fm = Frontmatter::new();
1234 let _ = deep_fm.insert("deep".into(), deep_val);
1235 assert!(matches!(
1236 to_yaml(&deep_fm),
1237 Err(Error::ConversionError(_))
1238 ));
1239 }
1240
1241 #[test]
1242 fn unsupported_format_is_rejected_rather_than_reaching_unreachable()
1243 {
1244 let err =
1247 parse_with_options("anything", Format::Unsupported, None)
1248 .expect_err("Unsupported must be refused up front");
1249 assert!(!err.to_string().is_empty());
1250 }
1251
1252 #[test]
1253 fn size_estimates_grow_with_the_document() {
1254 let empty = Frontmatter::new();
1259 assert!(estimate_json_size(&empty) >= 2, "at least the braces");
1260
1261 let mut fm = Frontmatter::new();
1262 let _ = fm.insert("k".into(), Value::String("value".into()));
1263 let one = estimate_json_size(&fm);
1264 let _ = fm.insert("k2".into(), Value::Number(1.0));
1265 assert!(
1266 estimate_json_size(&fm) > one,
1267 "adding a key must grow the estimate"
1268 );
1269
1270 for value in [
1271 Value::Null,
1272 Value::String("s".into()),
1273 Value::Number(1.0),
1274 Value::Boolean(true),
1275 Value::Array(vec![Value::Null, Value::Number(1.0)]),
1276 Value::Object(Box::new(Frontmatter::new())),
1277 Value::Tagged("t".into(), Box::new(Value::Null)),
1278 ] {
1279 assert!(
1280 estimate_value_size(&value) > 0,
1281 "{value:?} estimated as zero bytes"
1282 );
1283 }
1284
1285 let nested =
1287 Value::Array(vec![Value::Array(vec![Value::String(
1288 "deep".into(),
1289 )])]);
1290 assert!(
1291 estimate_value_size(&nested)
1292 > estimate_value_size(&Value::Array(vec![]))
1293 );
1294 }
1295
1296 #[test]
1297 fn malformed_input_reports_the_formats_own_error() {
1298 assert!(parse_yaml("a: [").is_err());
1299 assert!(parse_toml("a = ").is_err());
1300 assert!(parse_json("{").is_err());
1301 }
1302}