asterdex-sdk 0.1.1

AsterDex Futures SDK v3 — Rust async client for REST and WebSocket APIs
Documentation
// US-001: Error type — fully implemented in this wave (US-003 extends as needed)
use std::time::Duration;
use thiserror::Error;

/// All SDK error variants — see each variant for handling instructions.
#[derive(Error, Debug)]
pub enum AsterDexError {
    /// API returned a structured error `{ "code": int, "msg": string }`
    #[error("API error {code}: {msg}")]
    ApiError {
        /// Numeric error code from the exchange
        code: i64,
        /// Human-readable error message from the exchange
        msg: String,
    },

    /// HTTP 429 — Too Many Requests
    #[error("Rate limited, retry after: {retry_after:?}")]
    RateLimited {
        /// Duration to wait before retrying, parsed from `Retry-After` header
        retry_after: Option<Duration>,
    },

    /// HTTP 418 — IP banned
    #[error("IP banned, retry after: {retry_after:?}")]
    IpBanned {
        /// Duration to wait before retrying, parsed from `Retry-After` header
        retry_after: Option<Duration>,
    },

    /// HTTP 5xx or other non-success status that is not a structured API error
    #[error("HTTP error {status}: {body}")]
    HttpError {
        /// HTTP status code
        status: u16,
        /// Response body or error description (truncated to prevent log spam)
        body: String,
    },

    /// Network-level error (DNS, connect, TLS, read, body decode — no HTTP status)
    #[error("Network error ({kind}): {message}")]
    NetworkError {
        /// Short classifier: "timeout", "connect", "tls", "body", "decode", "other"
        kind: &'static str,
        /// Short description (no body, safe to log)
        message: String,
    },

    /// Request timed out (client-side) — distinguished so callers can retry aggressively
    #[error("Request timeout: {message}")]
    Timeout {
        /// Short description of what timed out
        message: String,
    },

    /// Client-side validation failure (caller-supplied parameters are invalid)
    #[error("Invalid parameters: {message}")]
    InvalidParams {
        /// Description of what is invalid
        message: String,
    },

    /// WebSocket connection or protocol error
    #[error("WebSocket error: {message}")]
    WebSocketError {
        /// Description of the WebSocket error
        message: String,
    },

    /// Configuration error (missing env var, invalid key, etc.)
    #[error("Config error: {message}")]
    ConfigError {
        /// Description of the configuration issue
        message: String,
    },

    /// JSON serialization/deserialization error
    #[error("Serde error: {message}")]
    SerdeError {
        /// Description of the serialization/deserialization failure.
        /// Body preview (first 512 bytes) is included when helpful, never the full body.
        message: String,
    },
}

impl AsterDexError {
    /// Classify a `reqwest::Error` into the most specific variant.
    ///
    /// Produces `Timeout` for client-side timeouts, otherwise `NetworkError`
    /// with a stable `kind` tag for retry logic. Never returns `HttpError`
    /// (that is reserved for non-success HTTP responses).
    pub(crate) fn from_reqwest(e: reqwest::Error) -> Self {
        if e.is_timeout() {
            return AsterDexError::Timeout {
                message: e.to_string(),
            };
        }
        let kind = if e.is_connect() {
            "connect"
        } else if e.is_request() {
            "request"
        } else if e.is_body() {
            "body"
        } else if e.is_decode() {
            "decode"
        } else {
            "other"
        };
        AsterDexError::NetworkError {
            kind,
            message: e.to_string(),
        }
    }
}