Skip to main content

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    InvalidGlobPattern {
67        pattern: String,
68        message: String,
69    },
70    Custom {
71        message: Cow<'static, str>,
72    },
73}
74
75/// A type alias for `Result<T, DarkluaError>`.
76pub type DarkluaResult<T> = Result<T, DarkluaError>;
77
78/// The main error type for the darklua library.
79///
80/// This error type represents all possible errors that can occur during
81/// the processing of Lua files. It includes errors from parsing, file I/O,
82/// configuration, and rule application.
83#[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    /// Creates a custom error with the given message.
238    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}