1use noyalib::Error as YamlError;
41use serde_json::Error as JsonError;
42use std::sync::Arc;
43use thiserror::Error;
44
45#[derive(Debug, Clone)]
47pub struct Context {
48 pub line: Option<usize>,
50 pub column: Option<usize>,
52 pub snippet: Option<String>,
54}
55
56impl std::fmt::Display for Context {
57 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
58 match &self.snippet {
59 Some(snippet) => write!(
60 f,
61 "at {}:{} near '{}'",
62 self.line.unwrap_or(0),
63 self.column.unwrap_or(0),
64 snippet
65 ),
66 None => write!(
67 f,
68 "at {}:{}",
69 self.line.unwrap_or(0),
70 self.column.unwrap_or(0)
71 ),
72 }
73 }
74}
75
76#[derive(Error, Debug)]
85#[non_exhaustive]
86pub enum Error {
87 #[error("Your front matter contains too many fields ({size}). The maximum allowed is {max}.")]
97 ContentTooLarge {
98 size: usize,
100 max: usize,
102 },
103
104 #[error(
109 "Your front matter is nested too deeply ({depth} levels). The maximum allowed nesting depth is {max}."
110 )]
111 NestingTooDeep {
112 depth: usize,
114 max: usize,
116 },
117
118 #[error("Failed to parse YAML: {source}")]
123 YamlParseError {
124 source: Arc<YamlError>,
126 },
127
128 #[error("Failed to parse TOML: {0}")]
133 TomlParseError(#[from] toml::de::Error),
134
135 #[error("Failed to parse JSON: {0}")]
140 JsonParseError(Arc<JsonError>),
141
142 #[error("Invalid front matter format")]
147 InvalidFormat,
148
149 #[error("Failed to convert front matter: {0}")]
154 ConversionError(String),
155
156 #[error("Failed to parse front matter: {0}")]
160 ParseError(String),
161
162 #[error("Unsupported front matter format detected at line {line}")]
167 UnsupportedFormat {
168 line: usize,
170 },
171
172 #[error("No front matter found in the content")]
177 NoFrontmatterFound,
178
179 #[error(
183 "Invalid JSON front matter: malformed or invalid structure."
184 )]
185 InvalidJson,
186
187 #[error(
191 "Invalid URL: {0}. Ensure the URL is well-formed and valid."
192 )]
193 InvalidUrl(String),
194
195 #[error(
199 "Invalid TOML front matter: malformed or invalid structure."
200 )]
201 InvalidToml,
202
203 #[error(
207 "Invalid YAML front matter: malformed or invalid structure."
208 )]
209 InvalidYaml,
210
211 #[error("Invalid language code: {0}")]
216 InvalidLanguage(String),
217
218 #[error("JSON front matter exceeds maximum nesting depth")]
223 JsonDepthLimitExceeded,
224
225 #[error("Extraction error: {0}")]
230 ExtractionError(String),
231
232 #[error("Serialization or deserialization error: {source}")]
237 SerdeError {
238 source: Arc<serde_json::Error>,
240 },
241
242 #[error("Input validation error: {0}")]
246 ValidationError(String),
247
248 #[error("Generic error: {0}")]
252 Other(String),
253}
254
255impl Clone for Error {
256 fn clone(&self) -> Self {
257 match self {
258 Self::ContentTooLarge { size, max } => {
259 Self::ContentTooLarge {
260 size: *size,
261 max: *max,
262 }
263 }
264 Self::NestingTooDeep { depth, max } => {
265 Self::NestingTooDeep {
266 depth: *depth,
267 max: *max,
268 }
269 }
270 Self::YamlParseError { source } => Self::YamlParseError {
271 source: Arc::clone(source),
272 },
273 Self::JsonParseError(err) => {
274 Self::JsonParseError(Arc::<serde_json::Error>::clone(
275 err,
276 ))
277 }
278 Self::TomlParseError(err) => {
279 Self::TomlParseError(err.clone())
280 }
281 Self::SerdeError { source } => Self::SerdeError {
282 source: Arc::clone(source),
283 },
284 Self::ConversionError(msg) => {
285 Self::ConversionError(msg.clone())
286 }
287 Self::ParseError(msg) => Self::ParseError(msg.clone()),
288 Self::UnsupportedFormat { line } => {
289 Self::UnsupportedFormat { line: *line }
290 }
291 Self::NoFrontmatterFound => Self::NoFrontmatterFound,
292 Self::InvalidJson => Self::InvalidJson,
293 Self::InvalidToml => Self::InvalidToml,
294 Self::InvalidYaml => Self::InvalidYaml,
295 Self::JsonDepthLimitExceeded => {
296 Self::JsonDepthLimitExceeded
297 }
298 Self::ExtractionError(msg) => {
299 Self::ExtractionError(msg.clone())
300 }
301 Self::ValidationError(msg) => {
302 Self::ValidationError(msg.clone())
303 }
304 Self::InvalidUrl(msg) => Self::InvalidUrl(msg.clone()),
305 Self::InvalidLanguage(msg) => {
306 Self::InvalidLanguage(msg.clone())
307 }
308 Self::Other(msg) => Self::Other(msg.clone()),
309 Self::InvalidFormat => Self::InvalidFormat,
310 }
311 }
312}
313
314#[derive(Debug, Copy, Clone, Eq, PartialEq)]
319pub enum Category {
320 Parsing,
322 Validation,
324 Conversion,
326 Configuration,
328}
329
330impl Error {
331 #[must_use]
337 pub const fn category(&self) -> Category {
338 match self {
339 Self::YamlParseError { .. }
340 | Self::TomlParseError(_)
341 | Self::JsonParseError(_)
342 | Self::SerdeError { .. }
343 | Self::ParseError(_)
344 | Self::InvalidFormat
345 | Self::UnsupportedFormat { .. }
346 | Self::NoFrontmatterFound
347 | Self::InvalidJson
348 | Self::InvalidToml
349 | Self::InvalidYaml
350 | Self::JsonDepthLimitExceeded
351 | Self::ExtractionError(_)
352 | Self::InvalidUrl(_)
353 | Self::InvalidLanguage(_) => Category::Parsing,
354 Self::ValidationError(_) => Category::Validation,
355 Self::ConversionError(_) => Category::Conversion,
356 Self::ContentTooLarge { .. }
357 | Self::NestingTooDeep { .. }
358 | Self::Other(_) => Category::Configuration,
359 }
360 }
361
362 #[must_use]
377 pub fn generic_parse_error(message: &str) -> Self {
378 Self::ParseError(message.to_string())
379 }
380
381 #[must_use]
396 pub const fn unsupported_format(line: usize) -> Self {
397 Self::UnsupportedFormat { line }
398 }
399
400 #[must_use]
415 pub fn validation_error(message: &str) -> Self {
416 Self::ValidationError(message.to_string())
417 }
418
419 #[must_use]
440 pub fn with_context(self, context: &Context) -> Self {
441 let context_info = format!(
442 " (line: {}, column: {})",
443 context.line.unwrap_or(0),
444 context.column.unwrap_or(0)
445 );
446 let snippet_info = context
447 .snippet
448 .as_ref()
449 .map(|s| format!(" near '{}'", s))
450 .unwrap_or_default();
451
452 match self {
453 Self::ParseError(mut msg) => {
454 msg.push_str(&context_info);
455 msg.push_str(&snippet_info);
456 Self::ParseError(msg)
457 }
458 Self::YamlParseError { source } => {
459 Self::YamlParseError { source }
460 }
461 _ => self, }
463 }
464}
465
466#[derive(Error, Debug)]
472pub enum EngineError {
473 #[error("Content processing error: {0}")]
475 ContentError(String),
476
477 #[error("Template processing error: {0}")]
479 TemplateError(String),
480
481 #[error("Asset processing error: {0}")]
483 AssetError(String),
484
485 #[error("File system error: {source}")]
487 FileSystemError {
488 source: std::io::Error,
490 context: String,
492 },
493
494 #[error("Metadata error: {0}")]
496 MetadataError(String),
497}
498
499impl Clone for EngineError {
501 fn clone(&self) -> Self {
502 match self {
503 Self::ContentError(msg) => Self::ContentError(msg.clone()),
504 Self::TemplateError(msg) => {
505 Self::TemplateError(msg.clone())
506 }
507 Self::AssetError(msg) => Self::AssetError(msg.clone()),
508 Self::FileSystemError { source, context } => {
509 Self::FileSystemError {
510 source: std::io::Error::new(
511 source.kind(),
512 source.to_string(),
513 ),
514 context: context.clone(),
515 }
516 }
517 Self::MetadataError(msg) => {
518 Self::MetadataError(msg.clone())
519 }
520 }
521 }
522}
523
524impl From<EngineError> for Error {
543 fn from(err: EngineError) -> Self {
544 match err {
545 EngineError::ContentError(msg) => {
546 Self::ParseError(format!("Content error: {}", msg))
547 }
548 EngineError::TemplateError(msg) => {
549 Self::ParseError(format!("Template error: {}", msg))
550 }
551 EngineError::AssetError(msg) => {
552 Self::ParseError(format!("Asset error: {}", msg))
553 }
554 EngineError::FileSystemError { source, context } => {
555 Self::ParseError(format!(
556 "File system error: {} ({})",
557 source, context
558 ))
559 }
560 EngineError::MetadataError(msg) => {
561 Self::ParseError(format!("Metadata error: {}", msg))
562 }
563 }
564 }
565}
566
567impl From<std::io::Error> for Error {
569 fn from(err: std::io::Error) -> Self {
570 Self::ParseError(err.to_string())
571 }
572}
573
574impl From<Error> for String {
578 fn from(err: Error) -> Self {
579 err.to_string()
580 }
581}
582
583#[cfg(test)]
584mod tests {
585 mod error_tests {
587 use super::super::*;
588
589 #[test]
591 fn test_content_too_large_error() {
592 let error = Error::ContentTooLarge {
593 size: 1000,
594 max: 500,
595 };
596 assert_eq!(
597 error.to_string(),
598 "Your front matter contains too many fields (1000). The maximum allowed is 500."
599 );
600 }
601
602 #[test]
604 fn test_nesting_too_deep_error() {
605 let error = Error::NestingTooDeep { depth: 10, max: 5 };
606 assert_eq!(
607 error.to_string(),
608 "Your front matter is nested too deeply (10 levels). The maximum allowed nesting depth is 5."
609 );
610 }
611
612 #[test]
614 fn test_json_parse_error() {
615 let json_data = r#"{"key": invalid}"#;
616 let result: Result<serde_json::Value, _> =
617 serde_json::from_str(json_data);
618 assert!(result.is_err());
619 let error =
620 Error::JsonParseError(Arc::new(result.unwrap_err()));
621 assert!(matches!(error, Error::JsonParseError(_)));
622 }
623
624 #[test]
626 fn test_invalid_format_error() {
627 let error = Error::InvalidFormat;
628 assert_eq!(
629 error.to_string(),
630 "Invalid front matter format"
631 );
632 }
633
634 #[test]
636 fn test_conversion_error() {
637 let error =
638 Error::ConversionError("Conversion failed".to_string());
639 assert_eq!(
640 error.to_string(),
641 "Failed to convert front matter: Conversion failed"
642 );
643 }
644
645 #[test]
647 fn test_unsupported_format() {
648 let error = Error::unsupported_format(42);
649 assert!(matches!(
650 error,
651 Error::UnsupportedFormat { line: 42 }
652 ));
653 assert_eq!(
654 error.to_string(),
655 "Unsupported front matter format detected at line 42"
656 );
657 }
658
659 #[test]
661 fn test_no_frontmatter_found() {
662 let error = Error::NoFrontmatterFound;
663 assert_eq!(
664 error.to_string(),
665 "No front matter found in the content"
666 );
667 }
668
669 #[test]
671 fn test_invalid_json_error() {
672 let error = Error::InvalidJson;
673 assert_eq!(error.to_string(), "Invalid JSON front matter: malformed or invalid structure.");
674 }
675
676 #[test]
678 fn test_invalid_url_error() {
679 let error =
680 Error::InvalidUrl("http:// invalid.url".to_string());
681 assert_eq!(
682 error.to_string(),
683 "Invalid URL: http:// invalid.url. Ensure the URL is well-formed and valid."
684 );
685 }
686
687 #[test]
689 fn test_invalid_yaml_error() {
690 let error = Error::InvalidYaml;
691 assert_eq!(
692 error.to_string(),
693 "Invalid YAML front matter: malformed or invalid structure."
694 );
695 }
696
697 #[test]
699 fn test_validation_error() {
700 let error =
701 Error::ValidationError("Invalid title".to_string());
702 assert_eq!(
703 error.to_string(),
704 "Input validation error: Invalid title"
705 );
706 }
707
708 #[test]
710 fn test_json_depth_limit_exceeded() {
711 let error = Error::JsonDepthLimitExceeded;
712 assert_eq!(
713 error.to_string(),
714 "JSON front matter exceeds maximum nesting depth"
715 );
716 }
717
718 #[test]
720 fn test_category_method() {
721 let validation_error =
722 Error::ValidationError("Invalid field".to_string());
723 assert_eq!(
724 validation_error.category(),
725 Category::Validation
726 );
727
728 let conversion_error =
729 Error::ConversionError("Conversion failed".to_string());
730 assert_eq!(
731 conversion_error.category(),
732 Category::Conversion
733 );
734
735 let config_error =
736 Error::ContentTooLarge { size: 100, max: 50 };
737 assert_eq!(
738 config_error.category(),
739 Category::Configuration
740 );
741 }
742
743 #[test]
745 fn test_error_clone() {
746 let original = Error::ContentTooLarge {
747 size: 200,
748 max: 100,
749 };
750 let cloned = original.clone();
751 assert!(
752 matches!(cloned, Error::ContentTooLarge { size, max } if size == 200 && max == 100)
753 );
754 }
755 }
756
757 mod engine_error_tests {
759 use super::super::*;
760 use std::io;
761
762 #[test]
764 fn test_content_error() {
765 let error = EngineError::ContentError(
766 "Processing failed".to_string(),
767 );
768 assert_eq!(
769 error.to_string(),
770 "Content processing error: Processing failed"
771 );
772 }
773
774 #[test]
776 fn test_engine_error_to_error_conversion() {
777 let io_error = io::Error::other("disk full");
778 let engine_error = EngineError::FileSystemError {
779 source: io_error,
780 context: "Saving file".to_string(),
781 };
782 let converted: Error = engine_error.into();
783 assert!(converted.to_string().contains("disk full"));
784 assert!(converted.to_string().contains("Saving file"));
785 }
786 }
787
788 mod context_tests {
790 use super::super::*;
791
792 #[test]
794 fn test_context_display() {
795 let context = Context {
796 line: Some(42),
797 column: Some(10),
798 snippet: Some("invalid key".to_string()),
799 };
800 assert_eq!(
801 context.to_string(),
802 "at 42:10 near 'invalid key'"
803 );
804 }
805
806 #[test]
808 fn test_context_missing_fields() {
809 let context = Context {
810 line: None,
811 column: None,
812 snippet: Some("example snippet".to_string()),
813 };
814 assert_eq!(
815 context.to_string(),
816 "at 0:0 near 'example snippet'"
817 );
818 }
819 }
820
821 mod conversion_tests {
823 use super::super::*;
824 use std::io;
825
826 #[test]
828 fn test_io_error_conversion() {
829 let io_error =
830 io::Error::new(io::ErrorKind::NotFound, "file missing");
831 let error: Error = io_error.into();
832 assert!(matches!(error, Error::ParseError(_)));
833 assert!(error.to_string().contains("file missing"));
834 }
835 }
836
837 #[test]
839 fn test_engine_error_conversion() {
840 let engine_error = crate::error::EngineError::ContentError(
841 "content failed".to_string(),
842 );
843 let error: crate::Error = engine_error.into();
844 assert!(matches!(error, crate::Error::ParseError(_)));
845 assert!(error.to_string().contains("content failed"));
846 }
847}
848
849#[cfg(test)]
850mod exhaustive_error_tests {
851 use super::*;
862 use std::sync::Arc;
863
864 fn one_of_every_error() -> Vec<Error> {
865 let json_err = serde_json::from_str::<serde_json::Value>("{")
866 .expect_err("invalid JSON");
867 let toml_err = toml::from_str::<toml::Value>("a = ")
868 .expect_err("invalid TOML");
869 let yaml_err = noyalib::from_str::<noyalib::Value>("a: [")
870 .expect_err("invalid YAML");
871 let serde_err = serde_json::from_str::<serde_json::Value>("]")
872 .expect_err("invalid JSON");
873
874 vec![
875 Error::ContentTooLarge { size: 10, max: 5 },
876 Error::NestingTooDeep { depth: 9, max: 3 },
877 Error::YamlParseError {
878 source: Arc::new(yaml_err),
879 },
880 Error::TomlParseError(toml_err),
881 Error::JsonParseError(Arc::new(json_err)),
882 Error::SerdeError {
883 source: Arc::new(serde_err),
884 },
885 Error::InvalidFormat,
886 Error::ConversionError("conversion".into()),
887 Error::ParseError("parse".into()),
888 Error::UnsupportedFormat { line: 42 },
889 Error::NoFrontmatterFound,
890 Error::InvalidJson,
891 Error::InvalidToml,
892 Error::InvalidYaml,
893 Error::InvalidUrl("url".into()),
894 Error::InvalidLanguage("lang".into()),
895 Error::JsonDepthLimitExceeded,
896 Error::ExtractionError("extract".into()),
897 Error::ValidationError("validate".into()),
898 Error::Other("other".into()),
899 ]
900 }
901
902 fn variant_of(e: &Error) -> &'static str {
905 match e {
906 Error::ContentTooLarge { .. } => "ContentTooLarge",
907 Error::NestingTooDeep { .. } => "NestingTooDeep",
908 Error::YamlParseError { .. } => "YamlParseError",
909 Error::TomlParseError(_) => "TomlParseError",
910 Error::JsonParseError(_) => "JsonParseError",
911 Error::SerdeError { .. } => "SerdeError",
912 Error::InvalidFormat => "InvalidFormat",
913 Error::ConversionError(_) => "ConversionError",
914 Error::ParseError(_) => "ParseError",
915 Error::UnsupportedFormat { .. } => "UnsupportedFormat",
916 Error::NoFrontmatterFound => "NoFrontmatterFound",
917 Error::InvalidJson => "InvalidJson",
918 Error::InvalidToml => "InvalidToml",
919 Error::InvalidYaml => "InvalidYaml",
920 Error::InvalidUrl(_) => "InvalidUrl",
921 Error::InvalidLanguage(_) => "InvalidLanguage",
922 Error::JsonDepthLimitExceeded => "JsonDepthLimitExceeded",
923 Error::ExtractionError(_) => "ExtractionError",
924 Error::ValidationError(_) => "ValidationError",
925 Error::Other(_) => "Other",
926 }
927 }
928
929 #[test]
930 fn clone_round_trips_every_variant() {
931 for error in one_of_every_error() {
932 let cloned = error.clone();
933 assert_eq!(
934 variant_of(&error),
935 variant_of(&cloned),
936 "clone changed the variant of {error:?}"
937 );
938 assert_eq!(
939 error.to_string(),
940 cloned.to_string(),
941 "clone changed the message of {error:?}"
942 );
943 }
944 }
945
946 #[test]
947 fn display_is_non_empty_and_names_its_payload() {
948 for error in one_of_every_error() {
949 let text = error.to_string();
950 let variant = variant_of(&error);
951 assert!(!text.is_empty(), "{variant} has no message");
952 }
953 assert!(Error::ContentTooLarge { size: 10, max: 5 }
954 .to_string()
955 .contains("10"));
956 assert!(Error::NestingTooDeep { depth: 9, max: 3 }
957 .to_string()
958 .contains('9'));
959 assert!(Error::UnsupportedFormat { line: 42 }
960 .to_string()
961 .contains("42"));
962 }
963
964 #[test]
965 fn every_variant_has_a_category() {
966 for error in one_of_every_error() {
967 let _ = error.category();
971 }
972 assert_eq!(
973 Error::ValidationError("x".into()).category(),
974 Category::Validation
975 );
976 assert_eq!(
977 Error::ConversionError("x".into()).category(),
978 Category::Conversion
979 );
980 assert_eq!(
981 Error::NoFrontmatterFound.category(),
982 Category::Parsing
983 );
984 assert_eq!(
985 Error::ContentTooLarge { size: 1, max: 0 }.category(),
986 Category::Configuration
987 );
988 }
989
990 #[test]
991 fn context_is_added_to_parse_errors_and_ignored_elsewhere() {
992 let context = Context {
993 line: Some(7),
994 column: Some(3),
995 snippet: Some("bad: [".into()),
996 };
997
998 let with =
999 Error::ParseError("broken".into()).with_context(&context);
1000 let text = with.to_string();
1001 assert!(text.contains("line: 7"), "{text}");
1002 assert!(text.contains("column: 3"), "{text}");
1003 assert!(text.contains("near 'bad: ['"), "{text}");
1004
1005 let bare = Context {
1007 line: None,
1008 column: None,
1009 snippet: None,
1010 };
1011 let text = Error::ParseError("broken".into())
1012 .with_context(&bare)
1013 .to_string();
1014 assert!(text.contains("line: 0"), "{text}");
1015 assert!(!text.contains("near"), "{text}");
1016
1017 let untouched = Error::NoFrontmatterFound
1019 .with_context(&context)
1020 .to_string();
1021 assert_eq!(untouched, Error::NoFrontmatterFound.to_string());
1022 }
1023
1024 #[test]
1025 fn helper_constructors_build_the_variant_they_name() {
1026 assert!(matches!(
1027 Error::generic_parse_error("x"),
1028 Error::ParseError(_)
1029 ));
1030 assert!(matches!(
1031 Error::validation_error("x"),
1032 Error::ValidationError(_)
1033 ));
1034 }
1035
1036 #[test]
1037 fn context_displays_all_three_fields() {
1038 let full = Context {
1039 line: Some(1),
1040 column: Some(2),
1041 snippet: Some("s".into()),
1042 };
1043 assert!(!full.to_string().is_empty());
1044 let empty = Context {
1045 line: None,
1046 column: None,
1047 snippet: None,
1048 };
1049 assert!(!empty.to_string().is_empty());
1050 }
1051
1052 fn one_of_every_engine_error() -> Vec<EngineError> {
1053 vec![
1054 EngineError::ContentError("content".into()),
1055 EngineError::TemplateError("template".into()),
1056 EngineError::AssetError("asset".into()),
1057 EngineError::FileSystemError {
1058 source: std::io::Error::new(
1059 std::io::ErrorKind::NotFound,
1060 "gone",
1061 ),
1062 context: "reading a template".into(),
1063 },
1064 EngineError::MetadataError("metadata".into()),
1065 ]
1066 }
1067
1068 #[test]
1069 fn engine_error_clones_and_converts_every_variant() {
1070 for error in one_of_every_engine_error() {
1071 let cloned = error.clone();
1072 assert_eq!(error.to_string(), cloned.to_string());
1073
1074 let converted: Error = error.into();
1075 assert!(
1076 matches!(converted, Error::ParseError(_)),
1077 "every EngineError converts to a ParseError: {converted:?}"
1078 );
1079 assert!(!converted.to_string().is_empty());
1080 }
1081 }
1082
1083 #[test]
1084 fn filesystem_engine_error_keeps_its_kind_and_context() {
1085 let error = EngineError::FileSystemError {
1086 source: std::io::Error::new(
1087 std::io::ErrorKind::PermissionDenied,
1088 "nope",
1089 ),
1090 context: "writing output".into(),
1091 };
1092 let cloned = error.clone();
1093 assert!(matches!(
1094 &cloned,
1095 EngineError::FileSystemError { source, context }
1096 if source.kind() == std::io::ErrorKind::PermissionDenied
1097 && context == "writing output"
1098 ));
1099
1100 let yaml_err = noyalib::from_str::<noyalib::Value>(":\n - [")
1101 .expect_err("invalid yaml");
1102 let wrapped = Error::YamlParseError {
1103 source: yaml_err.into(),
1104 }
1105 .with_context(&Context {
1106 line: Some(1),
1107 column: Some(2),
1108 snippet: None,
1109 });
1110 assert!(matches!(wrapped, Error::YamlParseError { .. }));
1111 let as_str: String = Error::InvalidFormat.into();
1112 assert!(!as_str.is_empty());
1113 }
1114}