nautilus_architect_ax/http/
error.rs

1// -------------------------------------------------------------------------------------------------
2//  Copyright (C) 2015-2026 Nautech Systems Pty Ltd. All rights reserved.
3//  https://nautechsystems.io
4//
5//  Licensed under the GNU Lesser General Public License Version 3.0 (the "License");
6//  You may not use this file except in compliance with the License.
7//  You may obtain a copy of the License at https://www.gnu.org/licenses/lgpl-3.0.en.html
8//
9//  Unless required by applicable law or agreed to in writing, software
10//  distributed under the License is distributed on an "AS IS" BASIS,
11//  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12//  See the License for the specific language governing permissions and
13//  limitations under the License.
14// -------------------------------------------------------------------------------------------------
15
16//! Error structures and enumerations for the AX Exchange HTTP integration.
17
18use nautilus_network::http::HttpClientError;
19use serde::{Deserialize, Serialize};
20use thiserror::Error;
21
22/// Build error for query parameter validation.
23#[derive(Debug, Clone, Error)]
24pub enum AxBuildError {
25    /// Missing required symbol.
26    #[error("Missing required symbol")]
27    MissingSymbol,
28    /// Invalid limit value.
29    #[error("Invalid limit: {0}")]
30    InvalidLimit(String),
31    /// Invalid time range: `start` should be less than `end`.
32    #[error("Invalid time range: start ({start}) must be less than end ({end})")]
33    InvalidTimeRange { start: i64, end: i64 },
34    /// Missing required order identifier.
35    #[error("Missing required order identifier")]
36    MissingOrderId,
37}
38
39/// Represents the JSON structure of an error response returned by the AX Exchange API.
40///
41/// Note: The exact error response format will be updated as we learn more about
42/// the AX Exchange API error structure.
43#[derive(Clone, Debug, Deserialize, Serialize)]
44pub struct AxErrorResponse {
45    /// Error code or type.
46    #[serde(default)]
47    pub error: Option<String>,
48    /// A human-readable explanation of the error condition.
49    #[serde(default)]
50    pub message: Option<String>,
51    /// HTTP status code.
52    #[serde(default)]
53    pub status: Option<u16>,
54}
55
56/// A typed error enumeration for the Ax HTTP client.
57#[derive(Debug, Clone, Error)]
58pub enum AxHttpError {
59    /// Error variant when credentials are missing but the request is authenticated.
60    #[error("Missing credentials for authenticated request")]
61    MissingCredentials,
62    /// Errors returned directly by AX Exchange API.
63    #[error("AX Exchange API error: {message}")]
64    ApiError { message: String },
65    /// Failure during JSON serialization/deserialization.
66    #[error("JSON error: {0}")]
67    JsonError(String),
68    /// Parameter validation error.
69    #[error("Parameter validation error: {0}")]
70    ValidationError(String),
71    /// Build error for query parameters.
72    #[error("Build error: {0}")]
73    BuildError(#[from] AxBuildError),
74    /// Request was canceled, typically due to shutdown or disconnect.
75    #[error("Request canceled: {0}")]
76    Canceled(String),
77    /// Generic network error (for retries, cancellations, etc).
78    #[error("Network error: {0}")]
79    NetworkError(String),
80    /// Any unknown HTTP status or unexpected response from Ax.
81    #[error("Unexpected HTTP status code {status}: {body}")]
82    UnexpectedStatus { status: u16, body: String },
83}
84
85impl From<HttpClientError> for AxHttpError {
86    fn from(error: HttpClientError) -> Self {
87        Self::NetworkError(error.to_string())
88    }
89}
90
91impl From<String> for AxHttpError {
92    fn from(error: String) -> Self {
93        Self::ValidationError(error)
94    }
95}
96
97impl From<serde_json::Error> for AxHttpError {
98    fn from(error: serde_json::Error) -> Self {
99        Self::JsonError(error.to_string())
100    }
101}
102
103impl From<AxErrorResponse> for AxHttpError {
104    fn from(error: AxErrorResponse) -> Self {
105        let message = error
106            .message
107            .or(error.error)
108            .unwrap_or_else(|| "Unknown error".to_string());
109        Self::ApiError { message }
110    }
111}
112
113#[cfg(test)]
114mod tests {
115    use rstest::rstest;
116
117    use super::*;
118
119    #[rstest]
120    fn test_architect_build_error_display() {
121        let error = AxBuildError::MissingSymbol;
122        assert_eq!(error.to_string(), "Missing required symbol");
123
124        let error = AxBuildError::InvalidLimit("must be positive".to_string());
125        assert_eq!(error.to_string(), "Invalid limit: must be positive");
126
127        let error = AxBuildError::InvalidTimeRange {
128            start: 100,
129            end: 50,
130        };
131        assert_eq!(
132            error.to_string(),
133            "Invalid time range: start (100) must be less than end (50)"
134        );
135    }
136
137    #[rstest]
138    fn test_architect_http_error_from_json_error() {
139        let json_err = serde_json::from_str::<serde_json::Value>("invalid json")
140            .expect_err("Should fail to parse");
141        let http_err = AxHttpError::from(json_err);
142
143        assert!(matches!(http_err, AxHttpError::JsonError(_)));
144    }
145
146    #[rstest]
147    fn test_architect_http_error_from_string() {
148        let error = AxHttpError::from("Test validation error".to_string());
149        assert_eq!(
150            error.to_string(),
151            "Parameter validation error: Test validation error"
152        );
153    }
154
155    #[rstest]
156    fn test_architect_error_response_to_http_error() {
157        let error_response = AxErrorResponse {
158            error: Some("INVALID_REQUEST".to_string()),
159            message: Some("Invalid parameter".to_string()),
160            status: Some(400),
161        };
162
163        let http_error = AxHttpError::from(error_response);
164        assert_eq!(
165            http_error.to_string(),
166            "AX Exchange API error: Invalid parameter"
167        );
168    }
169}