use reqwest::StatusCode;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum Error {
#[error("transport error: {0}")]
Transport(#[from] reqwest::Error),
#[error("OANDA API error (HTTP {status}): {}", body.error_message)]
Api {
status: StatusCode,
request_id: Option<String>,
body: ApiErrorBody,
},
#[error("failed to decode response body: {source}")]
Decode {
source: serde_json::Error,
body: String,
},
#[error("stream protocol error: {0}")]
Stream(String),
#[error("invalid client configuration: {0}")]
Config(String),
}
impl Error {
pub fn status(&self) -> Option<StatusCode> {
match self {
Error::Api { status, .. } => Some(*status),
Error::Transport(e) => e.status(),
_ => None,
}
}
pub fn request_id(&self) -> Option<&str> {
match self {
Error::Api { request_id, .. } => request_id.as_deref(),
_ => None,
}
}
pub fn is_rate_limited(&self) -> bool {
self.status() == Some(StatusCode::TOO_MANY_REQUESTS)
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[non_exhaustive]
pub struct ApiErrorBody {
#[serde(rename = "errorMessage")]
pub error_message: String,
#[serde(rename = "errorCode", skip_serializing_if = "Option::is_none")]
pub error_code: Option<String>,
#[serde(rename = "rejectReason", skip_serializing_if = "Option::is_none")]
pub reject_reason: Option<String>,
#[serde(flatten)]
pub extra: serde_json::Map<String, serde_json::Value>,
}
impl ApiErrorBody {
pub(crate) fn from_text(text: String) -> Self {
ApiErrorBody {
error_message: text,
error_code: None,
reject_reason: None,
extra: serde_json::Map::new(),
}
}
pub fn details<T: DeserializeOwned>(&self) -> Option<T> {
serde_json::to_value(self)
.ok()
.and_then(|v| serde_json::from_value(v).ok())
}
}