1use std::path::PathBuf;
2use thiserror::Error;
3
4#[derive(Debug, Error)]
5pub enum Error {
6 #[error("path not found: {0}")]
7 NotFound(PathBuf),
8 #[error("I/O error: {0}")]
9 Io(#[from] std::io::Error),
10 #[error("container error: {0}")]
11 Container(String),
12 #[error("required stream missing: {0}")]
13 MissingStream(String),
14 #[error("decompression failed in stream {stream}: {source}")]
15 Decompress {
16 stream: String,
17 #[source]
18 source: std::io::Error,
19 },
20 #[error("invalid file header: {0}")]
21 InvalidHeader(String),
22 #[error("record parse error: {0}")]
23 Record(String),
24}
25
26#[derive(Debug, Clone, serde::Serialize, PartialEq, Eq)]
27pub struct Warning {
28 pub code: WarningCode,
29 pub message: String,
30 pub location: Option<Location>,
31}
32
33#[derive(Debug, Clone, Copy, serde::Serialize, PartialEq, Eq)]
34#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
35pub enum WarningCode {
36 UnknownRecordTag,
37 TruncatedRecord,
38 UnsupportedContent,
39 ShapeRefMissing,
40}
41
42#[derive(Debug, Clone, Default, serde::Serialize, PartialEq, Eq)]
43pub struct Location {
44 pub section: Option<usize>,
45 pub paragraph: Option<usize>,
46 pub offset: Option<u64>,
47}
48
49#[derive(Debug, Default)]
50pub struct WarningCollector {
51 pub warnings: Vec<Warning>,
52}
53
54impl WarningCollector {
55 pub fn push(
56 &mut self,
57 code: WarningCode,
58 message: impl Into<String>,
59 location: Option<Location>,
60 ) {
61 self.warnings.push(Warning {
62 code,
63 message: message.into(),
64 location,
65 });
66 }
67 pub fn take(&mut self) -> Vec<Warning> {
68 std::mem::take(&mut self.warnings)
69 }
70}
71
72#[cfg(test)]
73mod tests {
74 use super::*;
75
76 #[test]
77 fn collector_accumulates_and_drains() {
78 let mut c = WarningCollector::default();
79 c.push(WarningCode::UnknownRecordTag, "tag=0x999", None);
80 c.push(
81 WarningCode::TruncatedRecord,
82 "ran out at offset 42",
83 Some(Location {
84 section: Some(0),
85 paragraph: Some(3),
86 offset: Some(42),
87 }),
88 );
89 assert_eq!(c.warnings.len(), 2);
90 let taken = c.take();
91 assert_eq!(taken.len(), 2);
92 assert!(c.warnings.is_empty());
93 }
94
95 #[test]
96 fn warning_serializes_with_screaming_code() {
97 let w = Warning {
98 code: WarningCode::UnknownRecordTag,
99 message: "x".into(),
100 location: None,
101 };
102 let s = serde_json::to_string(&w).unwrap();
103 assert!(s.contains("\"UNKNOWN_RECORD_TAG\""));
104 }
105}