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>;
73
74#[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 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}