use std::fmt;
#[derive(Debug)]
pub struct Error {
pub kind: ErrorKind,
pub context: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ErrorKind {
TypeMismatch {
expected: &'static str,
found: Option<String>,
},
MissingField {
field: String,
},
MissingValue,
InvalidValue {
reason: String,
},
Automerge {
message: String,
},
UnknownField {
field: String,
},
}
impl Error {
#[must_use]
pub fn type_mismatch(expected: &'static str, found: Option<String>) -> Self {
Self {
kind: ErrorKind::TypeMismatch { expected, found },
context: None,
}
}
#[must_use]
pub fn missing_field(field: impl Into<String>) -> Self {
Self {
kind: ErrorKind::MissingField {
field: field.into(),
},
context: None,
}
}
#[must_use]
pub fn missing_value() -> Self {
Self {
kind: ErrorKind::MissingValue,
context: None,
}
}
#[must_use]
pub fn invalid_value(reason: impl Into<String>) -> Self {
Self {
kind: ErrorKind::InvalidValue {
reason: reason.into(),
},
context: None,
}
}
#[must_use]
pub fn expected_type(expected: &'static str, type_name: &str) -> Self {
Self {
kind: ErrorKind::TypeMismatch {
expected,
found: Some(format!("while loading {}", type_name)),
},
context: None,
}
}
#[must_use]
pub fn unknown_field(field: impl Into<String>) -> Self {
Self {
kind: ErrorKind::UnknownField {
field: field.into(),
},
context: None,
}
}
#[must_use]
pub fn with_context(mut self, context: impl Into<String>) -> Self {
self.context = Some(context.into());
self
}
#[must_use]
pub fn with_field(mut self, field: impl Into<String>) -> Self {
let field = field.into();
self.context = Some(match self.context {
Some(existing) => {
if existing.starts_with('[') {
format!("{}{}", field, existing)
} else {
format!("{}.{}", field, existing)
}
}
None => field,
});
self
}
#[must_use]
pub fn with_index(mut self, index: usize) -> Self {
let index_str = format!("[{}]", index);
self.context = Some(match self.context {
Some(existing) => {
if existing.starts_with('[') {
format!("{}{}", index_str, existing)
} else {
format!("{}.{}", index_str, existing)
}
}
None => index_str,
});
self
}
#[must_use]
pub fn is_missing_value(&self) -> bool {
matches!(self.kind, ErrorKind::MissingValue)
}
#[must_use]
pub fn is_missing_field(&self) -> bool {
matches!(self.kind, ErrorKind::MissingField { .. })
}
#[must_use]
pub fn is_type_mismatch(&self) -> bool {
matches!(self.kind, ErrorKind::TypeMismatch { .. })
}
#[must_use]
pub fn is_unknown_field(&self) -> bool {
matches!(self.kind, ErrorKind::UnknownField { .. })
}
#[must_use]
pub fn is_invalid_value(&self) -> bool {
matches!(self.kind, ErrorKind::InvalidValue { .. })
}
#[must_use]
pub fn is_automerge(&self) -> bool {
matches!(self.kind, ErrorKind::Automerge { .. })
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.kind {
ErrorKind::TypeMismatch { expected, found } => {
write!(f, "type mismatch: expected {expected}")?;
if let Some(found) = found {
write!(f, ", found {found}")?;
}
}
ErrorKind::MissingField { field } => {
write!(f, "missing required field: {field}")?;
}
ErrorKind::MissingValue => {
write!(f, "value not found")?;
}
ErrorKind::InvalidValue { reason } => {
write!(f, "invalid value: {reason}")?;
}
ErrorKind::Automerge { message } => {
write!(f, "automerge error: {message}")?;
}
ErrorKind::UnknownField { field } => {
write!(f, "unknown field: {field}")?;
}
}
if let Some(ctx) = &self.context {
write!(f, " (at {ctx})")?;
}
Ok(())
}
}
impl std::error::Error for Error {}
impl From<automerge::AutomergeError> for Error {
fn from(err: automerge::AutomergeError) -> Self {
Self {
kind: ErrorKind::Automerge {
message: err.to_string(),
},
context: None,
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_error_display() {
let err = Error::type_mismatch("String", Some("Int".to_string()));
assert!(err.to_string().contains("type mismatch"));
assert!(err.to_string().contains("String"));
assert!(err.to_string().contains("Int"));
}
#[test]
fn test_error_with_context() {
let err = Error::missing_field("name").with_context("person.name");
assert!(err.to_string().contains("person.name"));
}
#[test]
fn test_missing_value_error() {
let err = Error::missing_value();
assert!(err.to_string().contains("not found"));
}
#[test]
fn test_invalid_value_error() {
let err = Error::invalid_value("out of range");
assert!(err.to_string().contains("out of range"));
}
#[test]
fn test_error_with_field_path() {
let err = Error::missing_value()
.with_field("city")
.with_field("address")
.with_field("person");
let msg = err.to_string();
assert!(msg.contains("person.address.city"), "got: {}", msg);
}
#[test]
fn test_error_with_index() {
let err = Error::missing_value().with_index(2).with_field("items");
let msg = err.to_string();
assert!(msg.contains("items[2]"), "got: {}", msg);
}
#[test]
fn test_error_with_nested_index() {
let err = Error::type_mismatch("String", None)
.with_field("name")
.with_index(5)
.with_field("users");
let msg = err.to_string();
assert!(msg.contains("users[5].name"), "got: {}", msg);
}
#[test]
fn test_error_predicates() {
assert!(Error::missing_value().is_missing_value());
assert!(Error::missing_field("x").is_missing_field());
assert!(Error::type_mismatch("Int", None).is_type_mismatch());
assert!(Error::unknown_field("x").is_unknown_field());
assert!(Error::invalid_value("bad").is_invalid_value());
let automerge_err = Error {
kind: ErrorKind::Automerge {
message: "test error".to_string(),
},
context: None,
};
assert!(automerge_err.is_automerge());
}
}