1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
use std::{fmt, io};

/// An error that occured.
#[derive(thiserror::Error, Debug)]
#[non_exhaustive]
pub enum Error {
	/// The requested value could not be found.
	#[error("The {0} could not be found.")]
	NotFound(ErrorValue),

	/// The requested value did not confirm to expected behavior/shape.
	#[error("The {0} is invalid: {1}.")]
	Invalid(ErrorValue, String),

	/// An error occured while working with a resource. This is typically IO-related,
	/// stemming from an inability to read or parse various expected structures.
	/// In most circumstances, recovery from this error is not possible without
	/// re-instantiating ironworks and/or the related module.
	#[error("An error occured while working with the provided resource: {0}")]
	Resource(Box<dyn std::error::Error + Send + Sync>),
}

// Common From<> impls for Error::Resource. This is intentionally not a catch-all.
impl From<io::Error> for Error {
	fn from(error: io::Error) -> Self {
		Error::Resource(error.into())
	}
}
impl From<binrw::Error> for Error {
	fn from(error: binrw::Error) -> Self {
		Error::Resource(error.into())
	}
}

// TODO: this could get pretty cluttered with single-purpose values. Is it worth making errorvalue a trait (and making error generic over it?) and letting each feature/file implement its own values? Generic trait might make it really messy to move errors around in the project due to non-matching bounds but hey maybe box dyn?
/// A value associated with an error that occured.
#[derive(Debug)]
#[non_exhaustive]
pub enum ErrorValue {
	/// A path to a file.
	Path(String),

	/// An Excel sheet.
	#[cfg(feature = "excel")]
	Sheet(String),

	/// An Excel row.
	#[cfg(feature = "exd")]
	Row {
		/// Row ID.
		row: u32,
		/// Sub-row ID.
		subrow: u16,
		/// Row's parent sheet, if known.
		sheet: Option<String>,
	},

	/// A SqPack file.
	#[cfg(feature = "sqpack")]
	File(Vec<u8>),

	/// A value not represented by other variants.
	///
	/// `ErrorValue`s of the `Other` type should only be `match`ed on with a wildcard
	/// (`_`) pattern. Values represented by `Other` may be promoted to a new variant
	/// in future versions.
	Other(String),
}

impl fmt::Display for ErrorValue {
	fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
		match self {
			Self::Path(path) => write!(formatter, "path {path:?}"),

			#[cfg(feature = "excel")]
			Self::Sheet(sheet) => write!(formatter, "Excel sheet {sheet:?}"),

			#[cfg(feature = "exd")]
			Self::Row { row, subrow, sheet } => write!(
				formatter,
				"Excel row {}/{row}:{subrow}",
				sheet.as_deref().unwrap_or("(none)"),
			),

			#[cfg(feature = "sqpack")]
			Self::File(file) => write!(formatter, "SqPack file ({} bytes)", file.len()),

			Self::Other(value) => write!(formatter, "{value}"),
		}
	}
}

pub type Result<T, E = Error> = std::result::Result<T, E>;