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(|work| work.total_required_content() != 0)
162            .filter_map(|work| match &work.status {
163                WorkStatus::NotStarted => None,
164                WorkStatus::InProgress(progress) => {
165                    let mut content: Vec<_> = progress
166                        .required_content()
167                        .filter(|path| source_left.contains(*path))
168                        .map(PathBuf::from)
169                        .collect();
170                    if content.is_empty() {
171                        None
172                    } else {
173                        content.sort();
174                        Some((work.data.clone(), content))
175                    }
176                }
177                WorkStatus::Done(_) => None,
178            })
179            .collect();
180
181        required_work.sort_by(|(a_data, a_content), (b_data, b_content)| {
182            match a_content.len().cmp(&b_content.len()) {
183                Ordering::Equal => a_data.source().cmp(b_data.source()),
184                other => other,
185            }
186        });
187
188        required_work.sort_by_key(|(_, content)| content.len());
189
190        Self::new(ErrorKind::CyclicWork {
191            work: required_work,
192        })
193    }
194
195    pub(crate) fn invalid_resource_path(
196        path: impl Into<String>,
197        message: impl Into<String>,
198    ) -> Self {
199        Self::new(ErrorKind::InvalidResourcePath {
200            location: path.into(),
201            message: message.into(),
202        })
203    }
204
205    pub(crate) fn invalid_resource_extension(path: impl Into<PathBuf>) -> Self {
206        Self::new(ErrorKind::InvalidResourceExtension {
207            location: path.into(),
208        })
209    }
210
211    pub(crate) fn os_string_conversion(os_string: impl Into<OsString>) -> Self {
212        Self::new(ErrorKind::OsStringConversion {
213            os_string: os_string.into(),
214        })
215    }
216
217    pub fn custom(message: impl Into<Cow<'static, str>>) -> Self {
218        Self::new(ErrorKind::Custom {
219            message: message.into(),
220        })
221    }
222}
223
224impl From<ResourceError> for DarkluaError {
225    fn from(err: ResourceError) -> Self {
226        match err {
227            ResourceError::NotFound(path) => DarkluaError::resource_not_found(path),
228            ResourceError::IO { path, error } => DarkluaError::io_error(path, error),
229        }
230    }
231}
232
233impl From<json5::Error> for DarkluaError {
234    fn from(error: json5::Error) -> Self {
235        Self::new(ErrorKind::Deserialization {
236            message: error.to_string(),
237            data_type: "json",
238        })
239    }
240}
241
242impl From<serde_json::Error> for DarkluaError {
243    fn from(error: serde_json::Error) -> Self {
244        Self::new(ErrorKind::Deserialization {
245            message: error.to_string(),
246            data_type: "json",
247        })
248    }
249}
250
251impl From<serde_yaml::Error> for DarkluaError {
252    fn from(error: serde_yaml::Error) -> Self {
253        Self::new(ErrorKind::Deserialization {
254            message: error.to_string(),
255            data_type: "yaml",
256        })
257    }
258}
259
260impl From<toml::de::Error> for DarkluaError {
261    fn from(error: toml::de::Error) -> Self {
262        Self::new(ErrorKind::Deserialization {
263            message: error.to_string(),
264            data_type: "toml",
265        })
266    }
267}
268
269impl From<toml::ser::Error> for DarkluaError {
270    fn from(error: toml::ser::Error) -> Self {
271        Self::new(ErrorKind::Serialization {
272            message: error.to_string(),
273            data_type: "toml",
274        })
275    }
276}
277
278impl From<LuaSerializerError> for DarkluaError {
279    fn from(error: LuaSerializerError) -> Self {
280        Self::new(ErrorKind::Serialization {
281            message: error.to_string(),
282            data_type: "lua",
283        })
284    }
285}
286
287impl Display for DarkluaError {
288    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
289        match &*self.kind {
290            ErrorKind::Parser { path, error } => {
291                write!(f, "unable to parse `{}`: {}", path.display(), error)?;
292            }
293            ErrorKind::ResourceNotFound { path } => {
294                write!(f, "unable to find `{}`", path.display())?;
295            }
296            ErrorKind::InvalidConfiguration { path } => {
297                write!(f, "invalid configuration file at `{}`", path.display())?;
298            }
299            ErrorKind::MultipleConfigurationFound { paths } => {
300                write!(
301                    f,
302                    "multiple default configuration file found: {}",
303                    paths
304                        .iter()
305                        .map(|path| format!("`{}`", path.display()))
306                        .collect::<Vec<_>>()
307                        .join(", ")
308                )?;
309            }
310            ErrorKind::IO { path, error } => {
311                write!(f, "IO error with `{}`: {}", path.display(), error)?;
312            }
313            ErrorKind::UncachedWork { path } => {
314                write!(f, "attempt to obtain work at `{}`", path.display())?;
315            }
316            ErrorKind::RuleError {
317                path,
318                rule_name,
319                rule_number,
320                error,
321            } => {
322                if let Some(rule_number) = rule_number {
323                    write!(
324                        f,
325                        "error processing `{}` ({} [#{}]):{}{}",
326                        path.display(),
327                        rule_name,
328                        rule_number,
329                        if error.contains('\n') { '\n' } else { ' ' },
330                        error,
331                    )?;
332                } else {
333                    write!(
334                        f,
335                        "error processing `{}` ({}):{}{}",
336                        path.display(),
337                        rule_name,
338                        if error.contains('\n') { '\n' } else { ' ' },
339                        error,
340                    )?;
341                }
342            }
343            ErrorKind::CyclicWork { work } => {
344                const MAX_PRINTED_WORK: usize = 12;
345                const MAX_REQUIRED_PATH: usize = 20;
346
347                let total = work.len();
348                let list: Vec<_> = work
349                    .iter()
350                    .take(MAX_PRINTED_WORK)
351                    .map(|(data, required)| {
352                        let required_list: Vec<_> = required
353                            .iter()
354                            .take(MAX_REQUIRED_PATH)
355                            .map(|path| format!("      - {}", path.display()))
356                            .collect();
357
358                        format!(
359                            "    `{}` needs:\n{}",
360                            data.source().display(),
361                            required_list.join("\n")
362                        )
363                    })
364                    .collect();
365
366                write!(
367                    f,
368                    "cyclic work detected:\n{}{}",
369                    list.join("\n"),
370                    if total <= MAX_PRINTED_WORK {
371                        "".to_owned()
372                    } else {
373                        format!("\n    and {} more", total - MAX_PRINTED_WORK)
374                    }
375                )?;
376            }
377            ErrorKind::Deserialization { message, data_type } => {
378                write!(f, "unable to read {} data: {}", data_type, message)?;
379            }
380            ErrorKind::Serialization { message, data_type } => {
381                write!(f, "unable to serialize {} data: {}", data_type, message)?;
382            }
383            ErrorKind::InvalidResourcePath { location, message } => {
384                write!(
385                    f,
386                    "unable to require resource at `{}`: {}",
387                    location, message
388                )?;
389            }
390            ErrorKind::InvalidResourceExtension { location } => {
391                if let Some(extension) = location.extension().map(OsStr::to_string_lossy) {
392                    write!(
393                        f,
394                        "unable to require resource with extension `{}` at `{}`",
395                        extension,
396                        location.display()
397                    )?;
398                } else {
399                    write!(
400                        f,
401                        "unable to require resource without an extension at `{}`",
402                        location.display()
403                    )?;
404                }
405            }
406            ErrorKind::OsStringConversion { os_string } => {
407                write!(
408                    f,
409                    "unable to convert operating system string (`{}`) into a utf-8 string",
410                    os_string.to_string_lossy(),
411                )?;
412            }
413            ErrorKind::Custom { message } => {
414                write!(f, "{}", message)?;
415            }
416        };
417
418        if let Some(context) = &self.context {
419            write!(f, " ({})", context)?;
420        }
421
422        Ok(())
423    }
424}