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