reka 0.1.0

Async Rust SDK for the Reka API.
Documentation
use std::collections::BTreeMap;
use std::env::VarError;
use std::num::ParseIntError;

use reqwest::StatusCode;
use reqwest::header::{HeaderMap, InvalidHeaderValue};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use thiserror::Error;
use url::ParseError;

pub type Result<T> = std::result::Result<T, RekaError>;

#[derive(Debug, Error)]
pub enum ConfigError {
    #[error("REKA_API_KEY is not set")]
    MissingApiKey,
    #[error("{name} is not set")]
    MissingEnvVar { name: &'static str },
    #[error("{name} contains invalid unicode")]
    InvalidEnvVar {
        name: &'static str,
        #[source]
        source: VarError,
    },
    #[error("api key must not be empty")]
    EmptyApiKey,
    #[error("invalid base URL for {field}: {value}")]
    InvalidBaseUrl {
        field: &'static str,
        value: String,
        #[source]
        source: ParseError,
    },
    #[error("invalid integer in {name}: {value}")]
    InvalidNumber {
        name: &'static str,
        value: String,
        #[source]
        source: ParseIntError,
    },
    #[error("api key is not a valid HTTP header value")]
    InvalidApiKeyHeader(#[source] InvalidHeaderValue),
}

#[derive(Debug, Clone, Serialize, Deserialize, Default)]
pub struct ApiErrorResponse {
    #[serde(default)]
    pub message: Option<String>,
    #[serde(default)]
    pub detail: Option<Value>,
    #[serde(default)]
    pub error: Option<Value>,
    #[serde(flatten)]
    pub extra: BTreeMap<String, Value>,
}

#[derive(Debug, Error)]
pub enum RekaError {
    #[error(transparent)]
    Config(#[from] ConfigError),
    #[error("invalid request: {0}")]
    InvalidRequest(String),
    #[error("request transport failed")]
    Transport(#[source] reqwest::Error),
    #[error(transparent)]
    HttpStatus(Box<HttpStatusError>),
    #[error(transparent)]
    Decode(Box<DecodeError>),
}

#[derive(Debug, Error)]
#[error("request to {path} failed with HTTP {status}")]
pub struct HttpStatusError {
    pub status: StatusCode,
    pub path: String,
    pub body: String,
    pub api_error: Option<ApiErrorResponse>,
    pub headers: HeaderMap,
}

#[derive(Debug, Error)]
#[error("failed to decode JSON response from {path}")]
pub struct DecodeError {
    pub path: String,
    pub body: String,
    #[source]
    pub source: serde_json::Error,
}

impl RekaError {
    pub(crate) fn http_status(
        status: StatusCode,
        path: impl Into<String>,
        body: String,
        api_error: Option<ApiErrorResponse>,
        headers: HeaderMap,
    ) -> Self {
        Self::HttpStatus(Box::new(HttpStatusError {
            status,
            path: path.into(),
            body,
            api_error,
            headers,
        }))
    }

    pub(crate) fn decode(path: impl Into<String>, body: String, source: serde_json::Error) -> Self {
        Self::Decode(Box::new(DecodeError {
            path: path.into(),
            body,
            source,
        }))
    }
}