latlng 0.1.0

Rust SDK for the latlng.work geocoding and places API
Documentation
use reqwest::StatusCode;
use serde::Deserialize;
use thiserror::Error as ThisError;

/// Result type returned by this crate.
pub type Result<T> = std::result::Result<T, Error>;

/// Errors returned by the latlng SDK.
#[derive(Debug, ThisError)]
pub enum Error {
    /// The configured API key cannot be used as an HTTP header value.
    #[error("invalid API key header: {0}")]
    InvalidApiKey(#[source] reqwest::header::InvalidHeaderValue),

    /// The HTTP client could not be built.
    #[error("failed to build HTTP client: {0}")]
    ClientBuild(#[source] reqwest::Error),

    /// The request failed before a response was received.
    #[error("HTTP request failed: {0}")]
    Request(#[from] reqwest::Error),

    /// The API key was rejected.
    #[error("authentication failed ({status}): {message}")]
    Auth {
        /// HTTP status code returned by the API.
        status: StatusCode,
        /// Error message returned by the API.
        message: String,
    },

    /// The request exceeded the current rate limit.
    #[error("rate limit exceeded: {message}")]
    RateLimit {
        /// Error message returned by the API.
        message: String,
    },

    /// The upstream geocoding or places service is unavailable.
    #[error("latlng service unavailable: {message}")]
    Unavailable {
        /// Error message returned by the API.
        message: String,
    },

    /// The API returned an unsuccessful response.
    #[error("API error ({status}): {message}")]
    Api {
        /// HTTP status code returned by the API.
        status: StatusCode,
        /// Error message returned by the API.
        message: String,
    },
}

#[derive(Debug, Deserialize)]
struct ErrorBody {
    error: Option<String>,
    message: Option<String>,
}

pub(crate) async fn error_from_response(response: reqwest::Response) -> Error {
    let status = response.status();
    let body = response.text().await.unwrap_or_default();
    let message = parse_error_message(&body).unwrap_or_else(|| {
        status
            .canonical_reason()
            .unwrap_or("request failed")
            .to_string()
    });

    match status {
        StatusCode::UNAUTHORIZED | StatusCode::FORBIDDEN => Error::Auth { status, message },
        StatusCode::TOO_MANY_REQUESTS => Error::RateLimit { message },
        StatusCode::SERVICE_UNAVAILABLE => Error::Unavailable { message },
        _ => Error::Api { status, message },
    }
}

fn parse_error_message(body: &str) -> Option<String> {
    if body.trim().is_empty() {
        return None;
    }

    if let Ok(parsed) = serde_json::from_str::<ErrorBody>(body) {
        return parsed
            .error
            .or(parsed.message)
            .filter(|message| !message.trim().is_empty());
    }

    Some(body.to_string())
}