use std::borrow::Cow;
use std::fmt;
use crate::Name;
#[derive(Clone, Debug, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("{path}: {reason}")]
Invalid {
path: Path,
reason: Cow<'static, str>,
},
#[error("unsupported: {0}")]
Unsupported(Cow<'static, str>),
#[error("over limit: {0}")]
Limit(Cow<'static, str>),
#[error("io: {0}")]
Io(Cow<'static, str>),
#[error("operation timed out")]
Timeout,
#[error("GPU device lost")]
DeviceLost,
#[error("operation cancelled")]
Cancelled,
}
pub type Result<T, E = Error> = std::result::Result<T, E>;
impl Error {
pub fn invalid(reason: impl Into<Cow<'static, str>>) -> Error {
Error::Invalid { path: Path::default(), reason: reason.into() }
}
pub fn code(&self) -> &'static str {
match self {
Error::Invalid { .. } => "invalid",
Error::Unsupported(_) => "unsupported",
Error::Limit(_) => "limit",
Error::Io(_) => "io",
Error::DeviceLost => "device-lost",
Error::Cancelled => "cancelled",
Error::Timeout => "timeout",
}
}
#[must_use]
pub fn at(self, step: impl Into<Step>) -> Error {
match self {
Error::Invalid { mut path, reason } => {
path.0.insert(0, step.into());
Error::Invalid { path, reason }
}
other => other,
}
}
}
impl serde::Serialize for Error {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
use serde::ser::SerializeMap;
let mut map = serializer.serialize_map(None)?;
map.serialize_entry("code", self.code())?;
map.serialize_entry("message", &self.to_string())?;
if let Error::Invalid { path, .. } = self {
map.serialize_entry("path", &path.to_string())?;
}
map.end()
}
}
impl schemars::JsonSchema for Error {
fn schema_name() -> Cow<'static, str> {
"Failure".into()
}
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
schemars::json_schema!({
"description": "What failed: its `code`, the message, and for invalid input the `path` to the part, such as `marks[2].vertices.color`.",
"type": "object",
"properties": {
"code": { "enum": ["invalid", "unsupported", "limit", "io", "device-lost", "cancelled", "timeout"] },
"message": { "type": "string" },
"path": { "type": "string" },
},
"required": ["code", "message"],
})
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct Path(pub Vec<Step>);
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum Step {
Name(Name),
Index(usize),
}
impl From<&str> for Step {
fn from(name: &str) -> Step {
Step::Name(name.into())
}
}
impl From<&Name> for Step {
fn from(name: &Name) -> Step {
Step::Name(name.clone())
}
}
impl From<Name> for Step {
fn from(name: Name) -> Step {
Step::Name(name)
}
}
impl From<usize> for Step {
fn from(index: usize) -> Step {
Step::Index(index)
}
}
impl fmt::Display for Path {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if self.0.is_empty() {
return f.write_str("input");
}
for (i, step) in self.0.iter().enumerate() {
match step {
Step::Name(name) if i == 0 => write!(f, "{name}")?,
Step::Name(name) => write!(f, ".{name}")?,
Step::Index(index) => write!(f, "[{index}]")?,
}
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn paths_read_like_access_expressions() {
let error = Error::invalid("not a number").at("color").at(2).at("marks");
assert_eq!(error.to_string(), "marks[2].color: not a number");
}
#[test]
fn only_invalid_errors_take_steps() {
assert_eq!(Error::DeviceLost.at("marks"), Error::DeviceLost);
}
}