use std::fmt;
use thiserror::Error;
#[derive(Debug, Error)]
pub enum SchemaError {
#[error("Type '{type_name}' is not supported for format '{format}'")]
UnsupportedType {
type_name: String,
format: String,
},
#[error("Circular reference detected: {path}")]
CircularReference {
path: String,
},
#[error("Type '{type_name}' not found in registry")]
TypeNotFound {
type_name: String,
},
#[error("Invalid attribute: {message}")]
InvalidAttribute {
message: String,
},
#[error("Conflicting attributes: {attr1} and {attr2}")]
ConflictingAttributes {
attr1: String,
attr2: String,
},
#[error("Invalid constraint: {message}")]
InvalidConstraint {
message: String,
},
#[error("I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(String),
#[error("Deserialization error: {0}")]
Deserialization(String),
#[error("Multiple errors occurred:\n{}", format_errors(.0))]
Multiple(Vec<SchemaError>),
#[error("{0}")]
Custom(String),
}
fn format_errors(errors: &[SchemaError]) -> String {
errors
.iter()
.enumerate()
.map(|(i, e)| format!(" {}. {}", i + 1, e))
.collect::<Vec<_>>()
.join("\n")
}
impl SchemaError {
pub fn unsupported_type(type_name: impl Into<String>, format: impl Into<String>) -> Self {
SchemaError::UnsupportedType {
type_name: type_name.into(),
format: format.into(),
}
}
pub fn circular_reference(path: impl IntoIterator<Item = impl AsRef<str>>) -> Self {
let path_str = path
.into_iter()
.map(|s| s.as_ref().to_string())
.collect::<Vec<_>>()
.join(" -> ");
SchemaError::CircularReference { path: path_str }
}
pub fn type_not_found(type_name: impl Into<String>) -> Self {
SchemaError::TypeNotFound {
type_name: type_name.into(),
}
}
pub fn invalid_attribute(message: impl Into<String>) -> Self {
SchemaError::InvalidAttribute {
message: message.into(),
}
}
pub fn conflicting_attributes(attr1: impl Into<String>, attr2: impl Into<String>) -> Self {
SchemaError::ConflictingAttributes {
attr1: attr1.into(),
attr2: attr2.into(),
}
}
pub fn invalid_constraint(message: impl Into<String>) -> Self {
SchemaError::InvalidConstraint {
message: message.into(),
}
}
pub fn custom(message: impl Into<String>) -> Self {
SchemaError::Custom(message.into())
}
pub fn multiple(errors: Vec<SchemaError>) -> Self {
if errors.len() == 1 {
errors.into_iter().next().unwrap()
} else {
SchemaError::Multiple(errors)
}
}
pub fn is_unsupported_type(&self) -> bool {
matches!(self, SchemaError::UnsupportedType { .. })
}
pub fn is_type_not_found(&self) -> bool {
matches!(self, SchemaError::TypeNotFound { .. })
}
pub fn is_multiple(&self) -> bool {
matches!(self, SchemaError::Multiple(_))
}
pub fn inner_errors(&self) -> Option<&[SchemaError]> {
match self {
SchemaError::Multiple(errors) => Some(errors),
_ => None,
}
}
}
impl From<serde_json::Error> for SchemaError {
fn from(err: serde_json::Error) -> Self {
SchemaError::Serialization(err.to_string())
}
}
pub type SchemaResult<T> = Result<T, SchemaError>;
#[derive(Debug, Default)]
pub struct ErrorCollector {
errors: Vec<SchemaError>,
}
impl ErrorCollector {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, error: SchemaError) {
self.errors.push(error);
}
pub fn collect<T>(&mut self, result: SchemaResult<T>) -> Option<T> {
match result {
Ok(value) => Some(value),
Err(error) => {
self.push(error);
None
}
}
}
pub fn has_errors(&self) -> bool {
!self.errors.is_empty()
}
pub fn len(&self) -> usize {
self.errors.len()
}
pub fn is_empty(&self) -> bool {
self.errors.is_empty()
}
pub fn finish(self) -> SchemaResult<()> {
if self.errors.is_empty() {
Ok(())
} else {
Err(SchemaError::multiple(self.errors))
}
}
pub fn finish_with<T>(self, value: T) -> SchemaResult<T> {
if self.errors.is_empty() {
Ok(value)
} else {
Err(SchemaError::multiple(self.errors))
}
}
pub fn into_errors(self) -> Vec<SchemaError> {
self.errors
}
}
pub trait ResultExt<T> {
fn with_context(self, context: impl FnOnce() -> String) -> SchemaResult<T>;
fn unsupported_for(self, format: &str) -> SchemaResult<T>;
}
impl<T> ResultExt<T> for SchemaResult<T> {
fn with_context(self, context: impl FnOnce() -> String) -> SchemaResult<T> {
self.map_err(|e| SchemaError::Custom(format!("{}: {}", context(), e)))
}
fn unsupported_for(self, format: &str) -> SchemaResult<T> {
self.map_err(|e| match e {
SchemaError::UnsupportedType { type_name, .. } => {
SchemaError::unsupported_type(type_name, format)
}
other => other,
})
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct SchemaWarning {
pub code: WarningCode,
pub message: String,
pub location: Option<String>,
}
impl SchemaWarning {
pub fn new(code: WarningCode, message: impl Into<String>) -> Self {
Self {
code,
message: message.into(),
location: None,
}
}
pub fn at(mut self, location: impl Into<String>) -> Self {
self.location = Some(location.into());
self
}
}
impl fmt::Display for SchemaWarning {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(ref location) = self.location {
write!(f, "[{}] at {}: {}", self.code, location, self.message)
} else {
write!(f, "[{}] {}", self.code, self.message)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum WarningCode {
DeprecatedUsage,
PrecisionLoss,
PartialSupport,
ConstraintNotExpressible,
NonPortableDefault,
IgnoredAttribute,
RecursiveType,
}
impl fmt::Display for WarningCode {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let code = match self {
WarningCode::DeprecatedUsage => "W001",
WarningCode::PrecisionLoss => "W002",
WarningCode::PartialSupport => "W003",
WarningCode::ConstraintNotExpressible => "W004",
WarningCode::NonPortableDefault => "W005",
WarningCode::IgnoredAttribute => "W006",
WarningCode::RecursiveType => "W007",
};
write!(f, "{}", code)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_unsupported_type_error() {
let err = SchemaError::unsupported_type("HashMap<i32, String>", "protobuf");
assert!(err.is_unsupported_type());
assert!(err.to_string().contains("HashMap<i32, String>"));
assert!(err.to_string().contains("protobuf"));
}
#[test]
fn test_circular_reference_error() {
let err = SchemaError::circular_reference(["User", "Post", "User"]);
assert!(err.to_string().contains("User -> Post -> User"));
}
#[test]
fn test_multiple_errors() {
let errors = vec![
SchemaError::type_not_found("Foo"),
SchemaError::type_not_found("Bar"),
];
let err = SchemaError::multiple(errors);
assert!(err.is_multiple());
assert_eq!(err.inner_errors().unwrap().len(), 2);
}
#[test]
fn test_single_error_not_wrapped() {
let errors = vec![SchemaError::type_not_found("Foo")];
let err = SchemaError::multiple(errors);
assert!(!err.is_multiple());
assert!(err.is_type_not_found());
}
#[test]
fn test_error_collector() {
let mut collector = ErrorCollector::new();
assert!(!collector.has_errors());
collector.push(SchemaError::type_not_found("Foo"));
assert!(collector.has_errors());
assert_eq!(collector.len(), 1);
let result = collector.finish();
assert!(result.is_err());
}
#[test]
fn test_error_collector_empty() {
let collector = ErrorCollector::new();
let result = collector.finish();
assert!(result.is_ok());
}
#[test]
fn test_error_collector_with_value() {
let collector = ErrorCollector::new();
let result = collector.finish_with(42);
assert_eq!(result.unwrap(), 42);
}
#[test]
fn test_warning_display() {
let warning = SchemaWarning::new(WarningCode::DeprecatedUsage, "Field 'foo' is deprecated")
.at("User.foo");
let display = warning.to_string();
assert!(display.contains("W001"));
assert!(display.contains("User.foo"));
assert!(display.contains("deprecated"));
}
}