1use crate::prelude::*;
6
7use core::fmt;
8
9#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
13pub struct Code(pub &'static str);
14
15impl fmt::Display for Code {
16 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
17 f.write_str(self.0)
18 }
19}
20
21pub mod codes {
24 use super::Code;
25
26 pub const PARSE: Code = Code("SH2001");
28 pub const NOT_TEXT: Code = Code("SH2002");
30 pub const MISSING_FILE: Code = Code("SH2003");
32 pub const AMBIGUOUS_FILE: Code = Code("SH2004");
34 pub const NO_FORMAT: Code = Code("SH2005");
36 pub const TOO_BIG: Code = Code("SH2006");
38 pub const PACK_DIFFERS: Code = Code("SH2007");
40 pub const UNKNOWN_NAME: Code = Code("SH2008");
43 pub const DUPLICATE: Code = Code("SH2009");
45 pub const OUT_OF_RANGE: Code = Code("SH2010");
47}
48
49#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
52#[non_exhaustive]
53pub struct ContentError {
54 pub file: String,
56 pub line: Option<u32>,
58 pub column: Option<u32>,
60 pub message: String,
62 pub code: Option<Code>,
64}
65
66impl ContentError {
67 pub fn new(file: impl Into<String>, message: impl Into<String>) -> Self {
69 Self {
70 file: file.into(),
71 line: None,
72 column: None,
73 message: message.into(),
74 code: None,
75 }
76 }
77
78 pub fn not_found(file: impl Into<String>) -> Self {
81 Self::new(file, crate::format::NOT_FOUND).with_code(codes::MISSING_FILE)
82 }
83
84 #[must_use]
86 pub fn at(mut self, line: u32, column: Option<u32>) -> Self {
87 self.line = Some(line);
88 self.column = column;
89 self
90 }
91
92 #[must_use]
94 pub fn with_code(mut self, code: Code) -> Self {
95 self.code = Some(code);
96 self
97 }
98}
99
100impl fmt::Display for ContentError {
101 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
102 write!(f, "{}", self.file)?;
103 if let Some(line) = self.line {
104 write!(f, ":{line}")?;
105 if let Some(column) = self.column {
106 write!(f, ":{column}")?;
107 }
108 }
109 write!(f, ": {}", self.message)
110 }
111}
112
113#[derive(Debug, Clone, PartialEq, Eq, Default, thiserror::Error)]
115pub struct ContentErrors(pub Vec<ContentError>);
116
117impl fmt::Display for ContentErrors {
118 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119 let n = self.0.len();
120 write!(f, "{n} content error{}:", if n == 1 { "" } else { "s" })?;
121 for e in &self.0 {
122 write!(f, "\n {e}")?;
123 }
124 Ok(())
125 }
126}
127
128pub fn line_of(source: &str, needle: &str) -> Option<u32> {
130 let index = source.lines().position(|l| l.contains(needle))?;
131 u32::try_from(index + 1).ok()
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn display_with_line_and_column() {
140 let e =
141 ContentError::new("base/data/colors.ron", "unknown colour \"plyer\"").at(12, Some(5));
142 assert_eq!(
143 e.to_string(),
144 "base/data/colors.ron:12:5: unknown colour \"plyer\""
145 );
146 }
147
148 #[test]
149 fn display_with_line_only() {
150 let e = ContentError::new("a.ron", "bad").at(3, None);
151 assert_eq!(e.to_string(), "a.ron:3: bad");
152 }
153
154 #[test]
155 fn display_without_position() {
156 let e = ContentError::new("a.ron", "missing file");
157 assert_eq!(e.to_string(), "a.ron: missing file");
158 }
159
160 #[test]
161 fn display_lists_all_errors() {
162 let errs = ContentErrors(vec![
163 ContentError::new("a.ron", "one").at(1, Some(2)),
164 ContentError::new("b.ron", "two"),
165 ]);
166 assert_eq!(
167 errs.to_string(),
168 "2 content errors:\n a.ron:1:2: one\n b.ron: two"
169 );
170 let single = ContentErrors(vec![ContentError::new("a.ron", "one")]);
171 assert_eq!(single.to_string(), "1 content error:\n a.ron: one");
172 }
173
174 #[test]
175 fn a_code_leaves_the_text_as_it_is() {
176 let e = ContentError::new("a.ron", "bad").at(1, Some(2));
177 assert_eq!(e.code, None);
178 let coded = e.clone().with_code(codes::PARSE);
179 assert_eq!(coded.code, Some(Code("SH2001")));
180 assert_eq!(coded.to_string(), e.to_string());
181 assert_eq!(codes::PARSE.to_string(), "SH2001");
182 let missing = ContentError::not_found("b.png");
183 assert_eq!(missing.to_string(), "b.png: file not found in asset bundle");
184 assert_eq!(missing.code, Some(codes::MISSING_FILE));
185 }
186
187 #[test]
188 fn line_of_finds_first_match() {
189 assert_eq!(line_of("a\nbb\nb", "b"), Some(2));
190 assert_eq!(line_of("a", "z"), None);
191 }
192}