darklua_core/frontend/
error.rs

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}