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