reputation-core 0.1.0

Core calculation engine for the KnowThat Reputation System with advanced scoring algorithms
Documentation
use thiserror::Error;

/// Main error type for the reputation system.
/// 
/// This enum represents all possible errors that can occur during
/// reputation score calculation and validation.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ReputationError {
    #[error("Invalid DID format: {0}")]
    InvalidDid(String),
    
    #[error("Invalid rating: {0} (must be between 1.0 and 5.0)")]
    InvalidRating(f64),
    
    #[error("Inconsistent review data: positive ({positive}) + negative ({negative}) != total ({total})")]
    InconsistentReviews {
        positive: u64,
        negative: u64,
        total: u64,
    },
    
    #[error("Validation failed: {0}")]
    ValidationError(#[from] ValidationError),
    
    #[error("Calculation error: {0}")]
    CalculationError(String),
    
    #[error("Serialization error: {0}")]
    SerializationError(String),
}

/// Validation-specific errors.
/// 
/// These errors occur when validating agent data or configuration values.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum ValidationError {
    #[error("Invalid DID format: {0}")]
    InvalidDid(String),
    
    #[error("Date cannot be in the future: {0}")]
    FutureDate(String),
    
    #[error("Invalid rating value: {0}")]
    InvalidRating(f64),
    
    #[error("Review counts are inconsistent")]
    InconsistentReviews,
    
    #[error("Invalid MCP level: {0} (must be 0-3)")]
    InvalidMcpLevel(u8),
    
    #[error("Invalid field value: {field} = {value}")]
    InvalidField {
        field: String,
        value: String,
    },
}

// Type alias for convenience
pub type Result<T> = std::result::Result<T, ReputationError>;

/// Errors that can occur when building or configuring calculators.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum BuilderError {
    #[error("Missing required field: {0}")]
    MissingField(String),
    
    #[error("Invalid configuration: {0}")]
    InvalidConfig(String),
    
    #[error("Validation failed during build")]
    ValidationFailed(#[from] ValidationError),
}

/// Errors specific to reputation score calculations.
#[derive(Error, Debug)]
#[non_exhaustive]
pub enum CalculationError {
    #[error("Score calculation resulted in NaN")]
    NaNResult,
    
    #[error("Score out of bounds: {0} (must be 0-100)")]
    ScoreOutOfBounds(f64),
    
    #[error("Confidence out of bounds: {0} (must be 0-1)")]
    ConfidenceOutOfBounds(f64),
    
    #[error("Invalid confidence target: {0} (must be 0-1)")]
    InvalidConfidence(f64),
}

// Implement conversion from CalculationError to ReputationError
impl From<CalculationError> for ReputationError {
    fn from(err: CalculationError) -> Self {
        ReputationError::CalculationError(err.to_string())
    }
}

// Implement conversion from BuilderError to ReputationError
impl From<BuilderError> for ReputationError {
    fn from(err: BuilderError) -> Self {
        match err {
            BuilderError::ValidationFailed(ve) => ReputationError::ValidationError(ve),
            _ => ReputationError::CalculationError(err.to_string()),
        }
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_reputation_error_display() {
        let error = ReputationError::InvalidDid("bad-did".to_string());
        assert_eq!(error.to_string(), "Invalid DID format: bad-did");

        let error = ReputationError::InvalidRating(6.0);
        assert_eq!(error.to_string(), "Invalid rating: 6 (must be between 1.0 and 5.0)");

        let error = ReputationError::InconsistentReviews {
            positive: 10,
            negative: 5,
            total: 20,
        };
        assert_eq!(
            error.to_string(),
            "Inconsistent review data: positive (10) + negative (5) != total (20)"
        );
    }

    #[test]
    fn test_validation_error_display() {
        let error = ValidationError::InvalidDid("bad-did".to_string());
        assert_eq!(error.to_string(), "Invalid DID format: bad-did");

        let error = ValidationError::FutureDate("2030-01-01".to_string());
        assert_eq!(error.to_string(), "Date cannot be in the future: 2030-01-01");

        let error = ValidationError::InvalidMcpLevel(5);
        assert_eq!(error.to_string(), "Invalid MCP level: 5 (must be 0-3)");

        let error = ValidationError::InvalidField {
            field: "total_interactions".to_string(),
            value: "-10".to_string(),
        };
        assert_eq!(error.to_string(), "Invalid field value: total_interactions = -10");
    }

    #[test]
    fn test_builder_error_display() {
        let error = BuilderError::MissingField("did".to_string());
        assert_eq!(error.to_string(), "Missing required field: did");

        let error = BuilderError::InvalidConfig("confidence_k must be positive".to_string());
        assert_eq!(error.to_string(), "Invalid configuration: confidence_k must be positive");
    }

    #[test]
    fn test_calculation_error_display() {
        let error = CalculationError::NaNResult;
        assert_eq!(error.to_string(), "Score calculation resulted in NaN");

        let error = CalculationError::ScoreOutOfBounds(150.0);
        assert_eq!(error.to_string(), "Score out of bounds: 150 (must be 0-100)");

        let error = CalculationError::ConfidenceOutOfBounds(1.5);
        assert_eq!(error.to_string(), "Confidence out of bounds: 1.5 (must be 0-1)");
    }

    #[test]
    fn test_error_conversions() {
        // Test ValidationError -> ReputationError
        let validation_err = ValidationError::InvalidRating(10.0);
        let reputation_err: ReputationError = validation_err.into();
        match reputation_err {
            ReputationError::ValidationError(ve) => {
                assert!(matches!(ve, ValidationError::InvalidRating(10.0)));
            }
            _ => panic!("Expected ValidationError variant"),
        }

        // Test CalculationError -> ReputationError
        let calc_err = CalculationError::NaNResult;
        let reputation_err: ReputationError = calc_err.into();
        match reputation_err {
            ReputationError::CalculationError(msg) => {
                assert_eq!(msg, "Score calculation resulted in NaN");
            }
            _ => panic!("Expected CalculationError variant"),
        }

        // Test BuilderError -> ReputationError
        let builder_err = BuilderError::MissingField("test".to_string());
        let reputation_err: ReputationError = builder_err.into();
        match reputation_err {
            ReputationError::CalculationError(msg) => {
                assert_eq!(msg, "Missing required field: test");
            }
            _ => panic!("Expected CalculationError variant"),
        }
    }

    #[test]
    fn test_error_downcast() {
        use std::error::Error;

        let error = ReputationError::InvalidRating(10.0);
        assert!(error.source().is_none());

        let validation_err = ValidationError::InvalidRating(10.0);
        let error = ReputationError::ValidationError(validation_err);
        assert!(error.source().is_some());
    }

    #[test]
    fn test_error_debug_format() {
        let error = ReputationError::InvalidDid("test".to_string());
        let debug_str = format!("{:?}", error);
        assert!(debug_str.contains("InvalidDid"));
        assert!(debug_str.contains("test"));
    }

    #[test]
    fn test_unicode_in_error_messages() {
        let error = ReputationError::InvalidDid("did:测试:123".to_string());
        assert_eq!(error.to_string(), "Invalid DID format: did:测试:123");

        let error = ValidationError::InvalidField {
            field: "フィールド".to_string(),
            value: "".to_string(),
        };
        assert_eq!(error.to_string(), "Invalid field value: フィールド = 値");
    }

    #[test]
    fn test_very_long_error_messages() {
        let long_did = "did:".to_string() + &"x".repeat(1000);
        let error = ReputationError::InvalidDid(long_did.clone());
        assert!(error.to_string().contains(&long_did));
    }

    #[test]
    fn test_nested_error_context() {
        let validation_err = ValidationError::InvalidMcpLevel(10);
        let builder_err = BuilderError::ValidationFailed(validation_err);
        let reputation_err: ReputationError = builder_err.into();

        match reputation_err {
            ReputationError::ValidationError(ve) => {
                assert!(matches!(ve, ValidationError::InvalidMcpLevel(10)));
            }
            _ => panic!("Expected ValidationError variant"),
        }
    }
}