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 crate::utils::ChainError;
use actix_web::HttpResponse;

pub(crate) fn map_error(error: ChainError) -> HttpResponse {
    match error {
        ChainError::NotFound(_) => {
            HttpResponse::NotFound().json(serde_json::json!({"error": error.to_string()}))
        }
        ChainError::AlreadyExists(_) => {
            HttpResponse::Conflict().json(serde_json::json!({"error": error.to_string()}))
        }
        // Optimistic-concurrency conflict: another writer advanced/modified the
        // session between our read and our compare-and-swap save. 409 so the
        // client re-reads and retries (same shape as the AlreadyExists arm).
        ChainError::Conflict(_) => {
            HttpResponse::Conflict().json(serde_json::json!({"error": error.to_string()}))
        }
        ChainError::InvalidState(_) => {
            HttpResponse::BadRequest().json(serde_json::json!({"error": error.to_string()}))
        }
        // Validation failures carry the offending field so clients can point the user
        // at the exact parameter; the wire shape is the documented
        // `ValidationErrorResponse` ({"error", "field"}).
        ChainError::Validation { ref field, .. } => {
            HttpResponse::BadRequest().json(crate::api::rest::responses::ValidationErrorResponse {
                error: error.to_string(),
                field: field.clone(),
            })
        }
        ChainError::SimulatorError(err) => {
            HttpResponse::Gone().json(serde_json::json!({"error": err}))
        }
        _ => HttpResponse::InternalServerError()
            .json(serde_json::json!({"error": "Internal server error".to_string()})),
    }
}

/// Tests for the `map_error` function.
#[cfg(test)]
mod tests {
    use super::*;
    use actix_web::body::to_bytes;
    use actix_web::http::StatusCode;
    use serde_json::Value;

    /// NotFound should map to 404 with the error message from `Display`.
    #[actix_web::test]
    async fn test_map_error_not_found() {
        let err = ChainError::NotFound("item_xyz".into());
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::NOT_FOUND);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": err.to_string()}));
    }

    /// AlreadyExists should map to 409 with the error message from `Display`.
    #[actix_web::test]
    async fn test_map_error_already_exists() {
        let err = ChainError::AlreadyExists("session_abc".into());
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::CONFLICT);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": err.to_string()}));
    }

    /// Conflict should map to 409 with the error message from `Display`.
    #[actix_web::test]
    async fn test_map_error_conflict() {
        let err = ChainError::Conflict("session modified concurrently".into());
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::CONFLICT);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": err.to_string()}));
    }

    /// InvalidState should map to 400 with the error message from `Display`.
    #[actix_web::test]
    async fn test_map_error_invalid_state() {
        let err = ChainError::InvalidState("bad_state".into());
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": err.to_string()}));
    }

    /// SimulatorError should map to 410 with the raw inner string.
    #[actix_web::test]
    async fn test_map_error_simulator_error() {
        let err = ChainError::SimulatorError("sim_fail".into());
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::GONE);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": "sim_fail"}));
    }

    /// Validation should map to 400 with the error message and the offending field.
    #[actix_web::test]
    async fn test_map_error_validation() {
        let err = ChainError::Validation {
            field: "initial_price".into(),
            reason: "must be a finite number".into(),
        };
        let resp: HttpResponse = map_error(err.clone());
        assert_eq!(resp.status(), StatusCode::BAD_REQUEST);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(
            json,
            serde_json::json!({"error": err.to_string(), "field": "initial_price"})
        );
    }

    /// Any other variant (e.g., SessionError) should map to 500 with a generic message.
    #[actix_web::test]
    async fn test_map_error_default() {
        let err = ChainError::SessionError("oops".into());
        let resp: HttpResponse = map_error(err);
        assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);

        let body = to_bytes(resp.into_body()).await.unwrap();
        let json: Value = serde_json::from_slice(&body).unwrap();
        assert_eq!(json, serde_json::json!({"error": "Internal server error"}));
    }
}