Skip to main content

polyoxide_clob/
error.rs

1use polyoxide_core::ApiError;
2use thiserror::Error;
3
4use crate::types::ParseTickSizeError;
5
6/// Error types for CLOB API operations
7#[derive(Error, Debug)]
8pub enum ClobError {
9    /// Core API error
10    #[error(transparent)]
11    Api(#[from] ApiError),
12
13    /// Cryptographic operation failed
14    #[error("Crypto error: {0}")]
15    Crypto(String),
16
17    /// Alloy (Ethereum library) error
18    #[error("Alloy error: {0}")]
19    Alloy(String),
20
21    /// Invalid tick size
22    #[error(transparent)]
23    InvalidTickSize(#[from] ParseTickSizeError),
24}
25
26impl ClobError {
27    /// Create error from HTTP response
28    pub(crate) async fn from_response(response: reqwest::Response) -> Self {
29        Self::Api(ApiError::from_response(response).await)
30    }
31
32    /// Create validation error
33    pub(crate) fn validation(msg: impl Into<String>) -> Self {
34        Self::Api(ApiError::Validation(msg.into()))
35    }
36
37    /// Create service error (external dependency failure)
38    #[cfg_attr(not(feature = "gamma"), allow(dead_code))]
39    pub(crate) fn service(msg: impl Into<String>) -> Self {
40        Self::Api(ApiError::Api {
41            status: 0,
42            message: msg.into(),
43        })
44    }
45}
46
47impl From<alloy::signers::Error> for ClobError {
48    fn from(err: alloy::signers::Error) -> Self {
49        Self::Alloy(err.to_string())
50    }
51}
52
53impl From<alloy::hex::FromHexError> for ClobError {
54    fn from(err: alloy::hex::FromHexError) -> Self {
55        Self::Alloy(err.to_string())
56    }
57}
58
59impl From<reqwest::Error> for ClobError {
60    fn from(err: reqwest::Error) -> Self {
61        Self::Api(ApiError::Network(err))
62    }
63}
64
65impl From<url::ParseError> for ClobError {
66    fn from(err: url::ParseError) -> Self {
67        Self::Api(ApiError::Url(err))
68    }
69}
70
71impl From<serde_json::Error> for ClobError {
72    fn from(err: serde_json::Error) -> Self {
73        Self::Api(ApiError::Serialization(err))
74    }
75}
76
77#[cfg(test)]
78mod tests {
79    use super::*;
80
81    #[test]
82    fn test_service_error_is_api_not_validation() {
83        let err = ClobError::service("Gamma client failed");
84        match &err {
85            ClobError::Api(ApiError::Api { status, message }) => {
86                assert_eq!(*status, 0);
87                assert_eq!(message, "Gamma client failed");
88            }
89            other => panic!("Expected ApiError::Api, got {:?}", other),
90        }
91    }
92
93    #[test]
94    fn test_validation_error() {
95        let err = ClobError::validation("bad input");
96        match &err {
97            ClobError::Api(ApiError::Validation(msg)) => {
98                assert_eq!(msg, "bad input");
99            }
100            other => panic!("Expected ApiError::Validation, got {:?}", other),
101        }
102    }
103
104    #[test]
105    fn test_service_and_validation_are_distinct() {
106        let service = ClobError::service("service failure");
107        let validation = ClobError::validation("validation failure");
108
109        let service_msg = format!("{}", service);
110        let validation_msg = format!("{}", validation);
111
112        // They should produce different Display output
113        assert_ne!(service_msg, validation_msg);
114        assert!(service_msg.contains("service failure"));
115        assert!(validation_msg.contains("validation failure"));
116    }
117
118    #[test]
119    fn test_crypto_error() {
120        let err = ClobError::Crypto("signing failed".into());
121        assert!(err.to_string().contains("signing failed"));
122        assert!(matches!(err, ClobError::Crypto(_)));
123    }
124
125    #[test]
126    fn test_alloy_error() {
127        let err = ClobError::Alloy("hex decode failed".into());
128        assert!(err.to_string().contains("hex decode failed"));
129        assert!(matches!(err, ClobError::Alloy(_)));
130    }
131
132    #[test]
133    fn test_invalid_tick_size_from_str() {
134        let err: Result<crate::types::TickSize, _> = "0.5".try_into();
135        let clob_err = ClobError::from(err.unwrap_err());
136        assert!(matches!(clob_err, ClobError::InvalidTickSize(_)));
137        assert!(clob_err.to_string().contains("0.5"));
138    }
139
140    #[test]
141    fn test_from_serde_json_error() {
142        let json_err = serde_json::from_str::<String>("not valid json").unwrap_err();
143        let clob_err = ClobError::from(json_err);
144        assert!(matches!(
145            clob_err,
146            ClobError::Api(ApiError::Serialization(_))
147        ));
148    }
149
150    #[test]
151    fn test_from_url_parse_error() {
152        let url_err = url::Url::parse("://bad").unwrap_err();
153        let clob_err = ClobError::from(url_err);
154        assert!(matches!(clob_err, ClobError::Api(ApiError::Url(_))));
155    }
156}