use std::fmt;
use thiserror::Error;
#[derive(Debug, Error, Clone, Copy, PartialEq, Eq)]
pub enum CruxiError {
#[error("cruxi: nil service")]
NilService,
#[error("cruxi: nil provider")]
NilProvider,
#[error("cruxi: unauthorized")]
Unauthorized,
}
#[derive(Debug, Clone)]
pub struct CodedError {
code: String,
instance: Option<String>,
title: Option<String>,
reason: Option<String>,
source: Option<Box<CodedError>>,
}
impl CodedError {
#[must_use]
pub fn new(code: impl Into<String>) -> Self {
Self {
code: code.into(),
instance: None,
title: None,
reason: None,
source: None,
}
}
#[must_use]
pub fn with_instance(mut self, instance: impl Into<String>) -> Self {
self.instance = Some(instance.into());
self
}
#[must_use]
pub fn with_title(mut self, title: impl Into<String>) -> Self {
self.title = Some(title.into());
self
}
#[must_use]
pub fn with_reason(mut self, reason: impl Into<String>) -> Self {
self.reason = Some(reason.into());
self
}
#[must_use]
pub fn with_source(mut self, source: CodedError) -> Self {
self.source = Some(Box::new(source));
self
}
#[must_use]
pub fn code(&self) -> &str {
&self.code
}
#[must_use]
pub fn instance(&self) -> Option<&str> {
self.instance.as_deref()
}
#[must_use]
pub fn title(&self) -> Option<&str> {
self.title.as_deref()
}
#[must_use]
pub fn reason(&self) -> Option<&str> {
self.reason.as_deref()
}
}
impl fmt::Display for CodedError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match (&self.reason, &self.title) {
(Some(reason), _) => write!(f, "{reason}"),
(None, Some(title)) => write!(f, "{title}"),
(None, None) => write!(f, "cruxi: coded error [{}]", self.code),
}
}
}
impl std::error::Error for CodedError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
self.source
.as_ref()
.map(|e| e.as_ref() as &(dyn std::error::Error + 'static))
}
}
#[derive(Debug, Clone, Error)]
#[error("cruxi: validation error: {field}: {message}")]
pub struct ValidationError {
field: String,
message: String,
}
impl ValidationError {
#[must_use]
pub fn new(field: impl Into<String>, message: impl Into<String>) -> Self {
Self {
field: field.into(),
message: message.into(),
}
}
#[must_use]
pub fn field(&self) -> &str {
&self.field
}
#[must_use]
pub fn message(&self) -> &str {
&self.message
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::error::Error;
#[test]
fn coded_error_display_with_reason() {
let err = CodedError::new("TEST")
.with_reason("detailed reason")
.with_title("Title");
assert_eq!(err.to_string(), "detailed reason");
}
#[test]
fn coded_error_display_with_title_only() {
let err = CodedError::new("TEST").with_title("Title Only");
assert_eq!(err.to_string(), "Title Only");
}
#[test]
fn coded_error_display_fallback() {
let err = CodedError::new("TEST_CODE");
assert_eq!(err.to_string(), "cruxi: coded error [TEST_CODE]");
}
#[test]
fn coded_error_chain() {
let inner = CodedError::new("INNER").with_reason("inner cause");
let outer = CodedError::new("OUTER")
.with_reason("outer reason")
.with_source(inner);
assert!(outer.source().is_some());
}
#[test]
fn validation_error_display() {
let err = ValidationError::new("email", "required");
assert_eq!(err.to_string(), "cruxi: validation error: email: required");
}
}