omni-schema-core 0.1.1

Core types and traits for omni-schema - Universal Schema Generator for Rust
Documentation

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"));
    }
}