optionchain_simulator 0.1.0

OptionChain-Simulator is a lightweight REST API service that simulates an evolving option chain with every request. It is designed for developers building or testing trading systems, backtesters, and visual tools that depend on option data streams but want to avoid relying on live data feeds.
use std::error::Error;
use std::fmt;
use tokio::task::JoinError;

/// `ChainError` is an enumeration that represents various kinds of errors that can occur within the context of the application.
///
/// This enum is marked with the `#[derive(Debug, Clone)]` attribute, enabling the implementation of the `Debug` and `Clone` traits.
///
/// # Usage
/// This enum can be used to differentiate between various error types and handle them appropriately in the application.
///
#[derive(Debug, Clone)]
pub enum ChainError {
    /// - `SessionError(String)`
    ///   Represents an error related to a session. Contains a `String` providing additional information about the session error.
    SessionError(String),
    /// - `StdError(String)`
    ///   Represents an error originating from standard library usage. Accepts a `String` for detailed error information.
    StdError(String),
    /// - `InvalidState(String)`
    ///   Indicates the occurrence of an invalid state during execution. Contains a `String` with details about the invalid state.
    InvalidState(String),
    /// - `Internal(String)`
    ///   Represents an internal error within the application. Additional information is provided via a `String`.
    Internal(String),
    /// - `NotFound(String)`
    ///   Denotes that a requested resource or item was not found. Includes a `String` detailing what was not found.
    NotFound(String),
    /// - `AlreadyExists(String)`
    ///   Denotes that a resource with the given identifier already exists and cannot be
    ///   created again. Raised by `SessionStore::create` when a session id is already
    ///   present, guarding against silently overwriting a live session.
    AlreadyExists(String),
    /// - `Conflict(String)`
    ///   Denotes an optimistic-concurrency conflict: a compare-and-swap save
    ///   (`SessionStore::save_cas`) found the stored session at a different
    ///   revision than the caller expected, meaning another writer advanced or
    ///   modified it in between. Raised so concurrent mutations cannot silently
    ///   overwrite one another; the caller (client) should re-read and retry. Maps
    ///   to HTTP 409 at the REST boundary.
    Conflict(String),
    /// - `SimulatorError(String)`
    ///   Indicates an error specific to a simulation process. Provides a `String` for more context.
    SimulatorError(String),
    /// - `ClickHouseError(String)`
    ///   Represents errors related to operations with the ClickHouse database. Contains a `String` with error details.
    ClickHouseError(String),
    /// - `NotEnoughData(String)`
    ///   Represents errors when there isn't enough data to fulfill the requested steps.
    NotEnoughData(String),
    /// - `Validation { field, reason }`
    ///   Indicates that a client-supplied request parameter failed validation before it
    ///   could be converted into a domain type (e.g. a negative `initial_price`, a
    ///   non-finite volatility, or an out-of-range `steps` count). `field` names the
    ///   offending request field and `reason` explains why it was rejected. This maps to
    ///   an HTTP 400 at the REST boundary instead of panicking during conversion.
    Validation {
        /// The name of the request field that failed validation.
        field: String,
        /// A human-readable explanation of why the value was rejected.
        reason: String,
    },
}

impl<'a> From<&'a str> for ChainError {
    fn from(msg: &'a str) -> Self {
        ChainError::StdError(msg.to_string())
    }
}

impl From<String> for ChainError {
    fn from(msg: String) -> Self {
        ChainError::StdError(msg)
    }
}

impl From<std::io::Error> for ChainError {
    fn from(err: std::io::Error) -> Self {
        ChainError::StdError(err.to_string())
    }
}

impl From<Box<dyn Error>> for ChainError {
    fn from(err: Box<dyn Error>) -> Self {
        ChainError::StdError(err.to_string())
    }
}

impl From<clickhouse::error::Error> for ChainError {
    fn from(err: clickhouse::error::Error) -> Self {
        ChainError::ClickHouseError(err.to_string())
    }
}

impl From<JoinError> for ChainError {
    fn from(err: JoinError) -> Self {
        ChainError::ClickHouseError(err.to_string())
    }
}

impl fmt::Display for ChainError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        match self {
            ChainError::SessionError(msg) => write!(f, "Session Error: {}", msg),
            ChainError::StdError(msg) => write!(f, "Std Error: {}", msg),
            ChainError::InvalidState(msg) => write!(f, "Invalid State: {}", msg),
            ChainError::Internal(msg) => write!(f, "Internal Error: {}", msg),
            ChainError::NotFound(msg) => write!(f, "Not Found: {}", msg),
            ChainError::AlreadyExists(msg) => write!(f, "Already Exists: {}", msg),
            ChainError::Conflict(msg) => write!(f, "Conflict: {}", msg),
            ChainError::SimulatorError(msg) => write!(f, "Simulator Error: {}", msg),
            ChainError::ClickHouseError(msg) => write!(f, "ClickHouse Error: {}", msg),
            ChainError::NotEnoughData(msg) => write!(f, "Not Enough Data: {}", msg),
            ChainError::Validation { field, reason } => {
                write!(f, "Validation Error: {}: {}", field, reason)
            }
        }
    }
}

impl Error for ChainError {}

#[cfg(test)]
mod tests {
    use super::*;
    use std::error::Error;
    use std::io;

    // Test conversion from &str
    #[test]
    fn test_from_str() {
        let error: ChainError = "test error".into();
        assert!(matches!(error, ChainError::StdError(msg) if msg == "test error"));
    }

    // Test conversion from String
    #[test]
    fn test_from_string() {
        let error_msg = "test error".to_string();
        let error: ChainError = error_msg.clone().into();
        assert!(matches!(error, ChainError::StdError(msg) if msg == error_msg));
    }

    // Test conversion from std::io::Error
    #[test]
    fn test_from_io_error() {
        let io_error = io::Error::new(io::ErrorKind::NotFound, "file not found");
        let error: ChainError = io_error.into();
        assert!(matches!(error, ChainError::StdError(msg) if msg == "file not found"));
    }

    // Test conversion from Box<dyn Error>
    #[test]
    fn test_from_boxed_error() {
        let boxed_error: Box<dyn Error> = Box::new(io::Error::other("generic error"));
        let error: ChainError = boxed_error.into();
        assert!(matches!(error, ChainError::StdError(msg) if msg == "generic error"));
    }

    // Test conversion from clickhouse::error::Error
    #[test]
    fn test_from_clickhouse_error() {
        // Note: You'll need to mock a clickhouse::error::Error for this test
        // This is a simplified example
        #[derive(Debug)]
        struct MockClickHouseError;

        impl std::fmt::Display for MockClickHouseError {
            fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
                write!(f, "mock clickhouse error")
            }
        }

        impl std::error::Error for MockClickHouseError {}

        impl From<MockClickHouseError> for ChainError {
            fn from(_err: MockClickHouseError) -> Self {
                ChainError::ClickHouseError("mock clickhouse error".to_string())
            }
        }

        let ch_error = MockClickHouseError;
        let error: ChainError = ch_error.into();
        assert!(
            matches!(error, ChainError::ClickHouseError(msg) if msg == "mock clickhouse error")
        );
    }

    // Test Display trait implementation
    #[test]
    fn test_display_trait() {
        let test_cases = vec![
            (
                ChainError::SessionError("session problem".to_string()),
                "Session Error: session problem",
            ),
            (
                ChainError::StdError("standard error".to_string()),
                "Std Error: standard error",
            ),
            (
                ChainError::InvalidState("invalid state".to_string()),
                "Invalid State: invalid state",
            ),
            (
                ChainError::Internal("internal error".to_string()),
                "Internal Error: internal error",
            ),
            (
                ChainError::NotFound("resource missing".to_string()),
                "Not Found: resource missing",
            ),
            (
                ChainError::AlreadyExists("resource present".to_string()),
                "Already Exists: resource present",
            ),
            (
                ChainError::Conflict("version mismatch".to_string()),
                "Conflict: version mismatch",
            ),
            (
                ChainError::SimulatorError("simulation failed".to_string()),
                "Simulator Error: simulation failed",
            ),
            (
                ChainError::ClickHouseError("database error".to_string()),
                "ClickHouse Error: database error",
            ),
            (
                ChainError::NotEnoughData("insufficient data points".to_string()),
                "Not Enough Data: insufficient data points",
            ),
            (
                ChainError::Validation {
                    field: "initial_price".to_string(),
                    reason: "must be greater than zero".to_string(),
                },
                "Validation Error: initial_price: must be greater than zero",
            ),
        ];

        for (error, expected_str) in test_cases {
            assert_eq!(error.to_string(), expected_str);
        }
    }

    // Test Error trait implementation
    #[test]
    fn test_error_trait() {
        let error = ChainError::SessionError("test error".to_string());

        // Check that it can be used as a standard Error
        let _: &dyn Error = &error;

        // Verify description is non-empty
        assert!(!error.to_string().is_empty());
    }

    // Comprehensive pattern matching test
    #[test]
    fn test_error_variants() {
        let errors = vec![
            ChainError::SessionError("session issue".to_string()),
            ChainError::StdError("standard issue".to_string()),
            ChainError::InvalidState("invalid state".to_string()),
            ChainError::Internal("internal issue".to_string()),
            ChainError::NotFound("not found".to_string()),
            ChainError::AlreadyExists("already exists".to_string()),
            ChainError::Conflict("version conflict".to_string()),
            ChainError::SimulatorError("simulation problem".to_string()),
            ChainError::ClickHouseError("database issue".to_string()),
            ChainError::NotEnoughData("insufficient data".to_string()),
            ChainError::Validation {
                field: "steps".to_string(),
                reason: "out of range".to_string(),
            },
        ];

        for error in errors {
            match error {
                ChainError::SessionError(msg) => assert_eq!(msg, "session issue"),
                ChainError::StdError(msg) => assert_eq!(msg, "standard issue"),
                ChainError::InvalidState(msg) => assert_eq!(msg, "invalid state"),
                ChainError::Internal(msg) => assert_eq!(msg, "internal issue"),
                ChainError::NotFound(msg) => assert_eq!(msg, "not found"),
                ChainError::AlreadyExists(msg) => assert_eq!(msg, "already exists"),
                ChainError::Conflict(msg) => assert_eq!(msg, "version conflict"),
                ChainError::SimulatorError(msg) => assert_eq!(msg, "simulation problem"),
                ChainError::ClickHouseError(msg) => assert_eq!(msg, "database issue"),
                ChainError::NotEnoughData(msg) => assert_eq!(msg, "insufficient data"),
                ChainError::Validation { field, reason } => {
                    assert_eq!(field, "steps");
                    assert_eq!(reason, "out of range");
                }
            }
        }
    }

    // Validation error Display names the field and reason.
    #[test]
    fn test_validation_display_names_field_and_reason() {
        let error = ChainError::Validation {
            field: "volatility".to_string(),
            reason: "must be a finite number".to_string(),
        };
        assert_eq!(
            error.to_string(),
            "Validation Error: volatility: must be a finite number"
        );
    }
}