infobip-sms-sdk 0.1.0

Async Rust SDK for the Infobip SMS API: send messages, manage scheduled bulks, query delivery reports and logs, fetch inbound SMS, and parse webhook payloads.
Documentation
//! Error types returned by the SDK.
//!
//! Most calls surface [`Error`]. When the server returns a non-2xx
//! response, the SDK tries to parse the body into one of the two error
//! schemas the API uses:
//!
//! - The richer `ApiError` shape ([`Error::Api`]) — used by the v3
//!   `/sms/3/*` endpoints. Contains `errorCode`, `description`, `action`,
//!   plus arrays of [`ApiErrorViolation`]s and [`ApiErrorResource`]s.
//! - The legacy `ApiException` shape ([`Error::Exception`]) — used by the
//!   v1 `/sms/1/*` endpoints. Contains a single nested
//!   `messageId` / `text` pair (and sometimes a map of validation
//!   errors).
//!
//! If neither schema parses, the body is preserved verbatim in
//! [`Error::Unexpected`] so you can debug it.

use serde::{Deserialize, Serialize};
use std::collections::HashMap;

/// All errors the SDK can produce.
///
/// See the [`error` module docs](self) for the dispatch logic between
/// [`Error::Api`] and [`Error::Exception`].
#[derive(Debug, thiserror::Error)]
pub enum Error {
    /// Returned when [`Client::builder`](crate::Client::builder) is
    /// missing required state, or when an option is malformed (e.g. an
    /// auth value that can't be put in an HTTP header).
    #[error("invalid configuration: {0}")]
    Config(String),

    /// The configured base URL or a derived endpoint URL failed to
    /// parse.
    #[error("invalid URL: {0}")]
    Url(#[from] url::ParseError),

    /// A network or HTTP-level failure: connection refused, timeout,
    /// TLS error, mid-stream disconnect, etc.
    #[error("HTTP transport error: {0}")]
    Http(#[from] reqwest::Error),

    /// The response body couldn't be (de)serialized as JSON.
    ///
    /// You shouldn't see this for normal operation — open an issue if
    /// you do, since it usually means the API has drifted from the
    /// version of the spec this crate was built against.
    #[error("failed to (de)serialize JSON: {0}")]
    Json(#[from] serde_json::Error),

    /// Structured error returned by the v3 endpoints.
    ///
    /// Endpoints that surface this variant: `/sms/3/messages`,
    /// `/sms/3/reports`, `/sms/3/logs`.
    #[error("API error ({status}): {error:?}")]
    Api {
        /// HTTP status code (e.g. `400`, `401`, `403`, `500`).
        status: u16,
        /// Server-provided structured error.
        error: Box<ApiError>,
    },

    /// Legacy `ApiException` returned by the v1 endpoints.
    ///
    /// Endpoints that surface this variant: `/sms/1/preview`,
    /// `/sms/1/bulks` (and `.../status`), `/sms/1/inbox/reports`,
    /// `/ct/1/log/end/{messageId}`.
    #[error("API exception ({status}): {exception:?}")]
    Exception {
        /// HTTP status code.
        status: u16,
        /// Server-provided exception payload.
        exception: Box<ApiException>,
    },

    /// The server returned a non-2xx response whose body matched
    /// neither error schema. The raw body is preserved verbatim.
    #[error("HTTP {status}: {body}")]
    Unexpected {
        /// HTTP status code.
        status: u16,
        /// Raw response body, decoded as UTF-8 (lossily if needed).
        body: String,
    },
}

/// Structured error envelope used by the v3 endpoints.
///
/// Mirrors the API's `ApiError` schema. All fields are optional because
/// the API does not always populate every field on every error.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
    /// Stable, machine-readable error code (e.g. `BAD_REQUEST`,
    /// `UNAUTHORIZED`, `RESOURCE_NOT_FOUND`).
    pub error_code: Option<String>,
    /// Human-readable description of what went wrong.
    pub description: Option<String>,
    /// Suggested next action to recover from the error.
    pub action: Option<String>,
    /// Per-field validation failures (e.g. "messages\[0\].destinations
    /// must not be empty").
    #[serde(default)]
    pub violations: Vec<ApiErrorViolation>,
    /// Documentation links the API thinks may help you recover.
    #[serde(default)]
    pub resources: Vec<ApiErrorResource>,
}

/// One field-level validation failure inside an [`ApiError`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApiErrorViolation {
    /// Property path that triggered the violation
    /// (e.g. `messages[0].content.text`).
    pub property: Option<String>,
    /// Detailed description of the violation.
    pub violation: Option<String>,
}

/// A documentation resource (name + URL) attached to an [`ApiError`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
pub struct ApiErrorResource {
    /// Friendly name of the resource.
    pub name: Option<String>,
    /// URL of the resource.
    pub url: Option<String>,
}

/// Legacy error envelope used by the v1 endpoints.
///
/// Mirrors the API's `ApiException` schema. Wraps a single nested
/// [`ApiRequestError`] under the `requestError` field.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiException {
    /// The actual error payload, when the server emits one.
    pub request_error: Option<ApiRequestError>,
}

/// Inner wrapper for [`ApiException`].
///
/// The legacy schema double-wraps every error inside two levels of
/// envelope; this is the intermediate level.
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiRequestError {
    /// Service-specific exception details.
    pub service_exception: Option<ApiRequestErrorDetails>,
}

/// Concrete details of a legacy [`ApiException`].
#[derive(Debug, Clone, Default, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiRequestErrorDetails {
    /// Stable, machine-readable error identifier.
    pub message_id: Option<String>,
    /// Human-readable error description.
    pub text: Option<String>,
    /// Per-field validation failures keyed by property name.
    #[serde(default)]
    pub validation_errors: HashMap<String, Vec<String>>,
}