1use std::{
2 borrow::Cow,
3 cmp::Ordering,
4 collections::HashSet,
5 ffi::{OsStr, OsString},
6 fmt::{self, Display},
7 path::PathBuf,
8};
9
10use crate::{process::LuaSerializerError, rules::Rule, ParserError};
11
12use super::{
13 resources::ResourceError,
14 work_item::{WorkData, WorkItem, WorkStatus},
15};
16
17#[derive(Debug, Clone)]
18enum ErrorKind {
19 Parser {
20 path: PathBuf,
21 error: ParserError,
22 },
23 ResourceNotFound {
24 path: PathBuf,
25 },
26 InvalidConfiguration {
27 path: PathBuf,
28 },
29 MultipleConfigurationFound {
30 paths: Vec<PathBuf>,
31 },
32 IO {
33 path: PathBuf,
34 error: String,
35 },
36 UncachedWork {
37 path: PathBuf,
38 },
39 RuleError {
40 path: PathBuf,
41 rule_name: String,
42 rule_number: Option<usize>,
43 error: String,
44 },
45 CyclicWork {
46 work: Vec<(WorkData, Vec<PathBuf>)>,
47 },
48 Deserialization {
49 message: String,
50 data_type: &'static str,
51 },
52 Serialization {
53 message: String,
54 data_type: &'static str,
55 },
56 InvalidResourcePath {
57 location: String,
58 message: String,
59 },
60 InvalidResourceExtension {
61 location: PathBuf,
62 },
63 OsStringConversion {
64 os_string: OsString,
65 },
66 Custom {
67 message: Cow<'static, str>,
68 },
69}
70
71pub type DarkluaResult<T> = Result<T, DarkluaError>;
72
73#[derive(Debug, Clone)]
74pub struct DarkluaError {
75 kind: Box<ErrorKind>,
76 context: Option<Cow<'static, str>>,
77}
78
79impl DarkluaError {
80 fn new(kind: ErrorKind) -> Self {
81 Self {
82 kind: kind.into(),
83 context: None,
84 }
85 }
86
87 pub(crate) fn context(mut self, context: impl Into<Cow<'static, str>>) -> Self {
88 self.context = Some(context.into());
89 self
90 }
91
92 pub(crate) fn parser_error(path: impl Into<PathBuf>, error: ParserError) -> Self {
93 Self::new(ErrorKind::Parser {
94 path: path.into(),
95 error,
96 })
97 }
98
99 pub(crate) fn multiple_configuration_found(
100 configuration_files: impl Iterator<Item = PathBuf>,
101 ) -> Self {
102 Self::new(ErrorKind::MultipleConfigurationFound {
103 paths: configuration_files.collect(),
104 })
105 }
106
107 pub(crate) fn io_error(path: impl Into<PathBuf>, error: impl Into<String>) -> Self {
108 Self::new(ErrorKind::IO {
109 path: path.into(),
110 error: error.into(),
111 })
112 }
113
114 pub(crate) fn resource_not_found(path: impl Into<PathBuf>) -> Self {
115 Self::new(ErrorKind::ResourceNotFound { path: path.into() })
116 }
117
118 pub(crate) fn invalid_configuration_file(path: impl Into<PathBuf>) -> Self {
119 Self::new(ErrorKind::InvalidConfiguration { path: path.into() })
120 }
121
122 pub(crate) fn uncached_work(path: impl Into<PathBuf>) -> Self {
123 Self::new(ErrorKind::UncachedWork { path: path.into() })
124 }
125
126 pub(crate) fn rule_error(
127 path: impl Into<PathBuf>,
128 rule: &dyn Rule,
129 rule_index: usize,
130 rule_error: impl Into<String>,
131 ) -> Self {
132 Self::new(ErrorKind::RuleError {
133 path: path.into(),
134 rule_name: rule.get_name().to_owned(),
135 rule_number: Some(rule_index),
136 error: rule_error.into(),
137 })
138 }
139
140 pub(crate) fn orphan_rule_error(
141 path: impl Into<PathBuf>,
142 rule: &dyn Rule,
143 rule_error: impl Into<String>,
144 ) -> Self {
145 Self::new(ErrorKind::RuleError {
146 path: path.into(),
147 rule_name: rule.get_name().to_owned(),
148 rule_number: None,
149 error: rule_error.into(),
150 })
151 }
152
153 pub(crate) fn cyclic_work(work_left: Vec<WorkItem>) -> Self {
154 let source_left: HashSet<PathBuf> = work_left
155 .iter()
156 .map(|work| work.source().to_path_buf())
157 .collect();
158
159 let mut required_work: Vec<_> = work_left
160 .into_iter()
161 .filter_map(|work| {
162 if work.total_required_content() == 0 {
163 None
164 } else {
165 let (status, data) = work.extract();
166
167 match status {
168 WorkStatus::NotStarted => None,
169 WorkStatus::InProgress(progress) => {
170 let mut content: Vec<_> = progress
171 .required_content()
172 .filter(|path| source_left.contains(*path))
173 .map(PathBuf::from)
174 .collect();
175 if content.is_empty() {
176 None
177 } else {
178 content.sort();
179 Some((data, content))
180 }
181 }
182 }
183 }
184 })
185 .collect();
186
187 required_work.sort_by(|(a_data, a_content), (b_data, b_content)| {
188 match a_content.len().cmp(&b_content.len()) {
189 Ordering::Equal => a_data.source().cmp(b_data.source()),
190 other => other,
191 }
192 });
193
194 required_work.sort_by_key(|(_, content)| content.len());
195
196 Self::new(ErrorKind::CyclicWork {
197 work: required_work,
198 })
199 }
200
201 pub(crate) fn invalid_resource_path(
202 path: impl Into<String>,
203 message: impl Into<String>,
204 ) -> Self {
205 Self::new(ErrorKind::InvalidResourcePath {
206 location: path.into(),
207 message: message.into(),
208 })
209 }
210
211 pub(crate) fn invalid_resource_extension(path: impl Into<PathBuf>) -> Self {
212 Self::new(ErrorKind::InvalidResourceExtension {
213 location: path.into(),
214 })
215 }
216
217 pub(crate) fn os_string_conversion(os_string: impl Into<OsString>) -> Self {
218 Self::new(ErrorKind::OsStringConversion {
219 os_string: os_string.into(),
220 })
221 }
222
223 pub fn custom(message: impl Into<Cow<'static, str>>) -> Self {
224 Self::new(ErrorKind::Custom {
225 message: message.into(),
226 })
227 }
228}
229
230impl From<ResourceError> for DarkluaError {
231 fn from(err: ResourceError) -> Self {
232 match err {
233 ResourceError::NotFound(path) => DarkluaError::resource_not_found(path),
234 ResourceError::IO { path, error } => DarkluaError::io_error(path, error),
235 }
236 }
237}
238
239impl From<json5::Error> for DarkluaError {
240 fn from(error: json5::Error) -> Self {
241 Self::new(ErrorKind::Deserialization {
242 message: error.to_string(),
243 data_type: "json",
244 })
245 }
246}
247
248impl From<serde_json::Error> for DarkluaError {
249 fn from(error: serde_json::Error) -> Self {
250 Self::new(ErrorKind::Deserialization {
251 message: error.to_string(),
252 data_type: "json",
253 })
254 }
255}
256
257impl From<serde_yaml::Error> for DarkluaError {
258 fn from(error: serde_yaml::Error) -> Self {
259 Self::new(ErrorKind::Deserialization {
260 message: error.to_string(),
261 data_type: "yaml",
262 })
263 }
264}
265
266impl From<toml::de::Error> for DarkluaError {
267 fn from(error: toml::de::Error) -> Self {
268 Self::new(ErrorKind::Deserialization {
269 message: error.to_string(),
270 data_type: "toml",
271 })
272 }
273}
274
275impl From<toml::ser::Error> for DarkluaError {
276 fn from(error: toml::ser::Error) -> Self {
277 Self::new(ErrorKind::Serialization {
278 message: error.to_string(),
279 data_type: "toml",
280 })
281 }
282}
283
284impl From<LuaSerializerError> for DarkluaError {
285 fn from(error: LuaSerializerError) -> Self {
286 Self::new(ErrorKind::Serialization {
287 message: error.to_string(),
288 data_type: "lua",
289 })
290 }
291}
292
293impl Display for DarkluaError {
294 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
295 match &*self.kind {
296 ErrorKind::Parser { path, error } => {
297 write!(f, "unable to parse `{}`: {}", path.display(), error)?;
298 }
299 ErrorKind::ResourceNotFound { path } => {
300 write!(f, "unable to find `{}`", path.display())?;
301 }
302 ErrorKind::InvalidConfiguration { path } => {
303 write!(f, "invalid configuration file at `{}`", path.display())?;
304 }
305 ErrorKind::MultipleConfigurationFound { paths } => {
306 write!(
307 f,
308 "multiple default configuration file found: {}",
309 paths
310 .iter()
311 .map(|path| format!("`{}`", path.display()))
312 .collect::<Vec<_>>()
313 .join(", ")
314 )?;
315 }
316 ErrorKind::IO { path, error } => {
317 write!(f, "IO error with `{}`: {}", path.display(), error)?;
318 }
319 ErrorKind::UncachedWork { path } => {
320 write!(f, "attempt to obtain work at `{}`", path.display())?;
321 }
322 ErrorKind::RuleError {
323 path,
324 rule_name,
325 rule_number,
326 error,
327 } => {
328 if let Some(rule_number) = rule_number {
329 write!(
330 f,
331 "error processing `{}` ({} [#{}]):{}{}",
332 path.display(),
333 rule_name,
334 rule_number,
335 if error.contains('\n') { '\n' } else { ' ' },
336 error,
337 )?;
338 } else {
339 write!(
340 f,
341 "error processing `{}` ({}):{}{}",
342 path.display(),
343 rule_name,
344 if error.contains('\n') { '\n' } else { ' ' },
345 error,
346 )?;
347 }
348 }
349 ErrorKind::CyclicWork { work } => {
350 const MAX_PRINTED_WORK: usize = 12;
351 const MAX_REQUIRED_PATH: usize = 20;
352
353 let total = work.len();
354 let list: Vec<_> = work
355 .iter()
356 .take(MAX_PRINTED_WORK)
357 .map(|(data, required)| {
358 let required_list: Vec<_> = required
359 .iter()
360 .take(MAX_REQUIRED_PATH)
361 .map(|path| format!(" - {}", path.display()))
362 .collect();
363
364 format!(
365 " `{}` needs:\n{}",
366 data.source().display(),
367 required_list.join("\n")
368 )
369 })
370 .collect();
371
372 write!(
373 f,
374 "cyclic work detected:\n{}{}",
375 list.join("\n"),
376 if total <= MAX_PRINTED_WORK {
377 "".to_owned()
378 } else {
379 format!("\n and {} more", total - MAX_PRINTED_WORK)
380 }
381 )?;
382 }
383 ErrorKind::Deserialization { message, data_type } => {
384 write!(f, "unable to read {} data: {}", data_type, message)?;
385 }
386 ErrorKind::Serialization { message, data_type } => {
387 write!(f, "unable to serialize {} data: {}", data_type, message)?;
388 }
389 ErrorKind::InvalidResourcePath { location, message } => {
390 write!(
391 f,
392 "unable to require resource at `{}`: {}",
393 location, message
394 )?;
395 }
396 ErrorKind::InvalidResourceExtension { location } => {
397 if let Some(extension) = location.extension().map(OsStr::to_string_lossy) {
398 write!(
399 f,
400 "unable to require resource with extension `{}` at `{}`",
401 extension,
402 location.display()
403 )?;
404 } else {
405 write!(
406 f,
407 "unable to require resource without an extension at `{}`",
408 location.display()
409 )?;
410 }
411 }
412 ErrorKind::OsStringConversion { os_string } => {
413 write!(
414 f,
415 "unable to convert operating system string (`{}`) into a utf-8 string",
416 os_string.to_string_lossy(),
417 )?;
418 }
419 ErrorKind::Custom { message } => {
420 write!(f, "{}", message)?;
421 }
422 };
423
424 if let Some(context) = &self.context {
425 write!(f, " ({})", context)?;
426 }
427
428 Ok(())
429 }
430}