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}