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