use crate::types::*;
use serde::de::DeserializeOwned;
use serde::Serialize;
use std::fmt;
use std::time::Duration;
fn try_build_http(timeout: Duration) -> Result<reqwest::Client, reqwest::Error> {
reqwest::Client::builder().timeout(timeout).build()
}
fn build_http(timeout: Duration) -> reqwest::Client {
try_build_http(timeout).expect("failed to build HTTP client")
}
#[allow(dead_code, clippy::result_large_err)]
fn decode_body<T: DeserializeOwned>(bytes: &[u8]) -> Result<T, Error> {
if bytes.iter().all(u8::is_ascii_whitespace) {
return Ok(serde_json::from_value(serde_json::Value::Null)?);
}
Ok(serde_json::from_slice(bytes)?)
}
fn should_retry_status(status: u16) -> bool {
matches!(status, 408 | 409 | 429) || status >= 500
}
fn parse_retry_after(response: &reqwest::Response) -> Option<Duration> {
let headers = response.headers();
if let Some(ms) = headers
.get("retry-after-ms")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse::<u64>().ok())
{
return Some(Duration::from_millis(ms));
}
if let Some(secs) = headers
.get("retry-after")
.and_then(|v| v.to_str().ok())
.and_then(|s| s.trim().parse::<u64>().ok())
{
return Some(Duration::from_secs(secs));
}
None
}
fn backoff_duration(attempt: u32, retry_after: Option<Duration>) -> Duration {
if let Some(wait) = retry_after {
return wait.min(Duration::from_secs(60));
}
let exp = 0.5_f64 * 2f64.powi(attempt.saturating_sub(1) as i32);
let capped = exp.min(8.0);
Duration::from_secs_f64(capped * equal_jitter())
}
fn equal_jitter() -> f64 {
use std::time::{SystemTime, UNIX_EPOCH};
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.subsec_nanos())
.unwrap_or(0);
0.5 + (nanos % 500_000) as f64 / 1_000_000.0
}
fn should_retry_response(response: &reqwest::Response) -> bool {
match response
.headers()
.get("x-should-retry")
.and_then(|v| v.to_str().ok())
{
Some("true") => return true,
Some("false") => return false,
_ => {}
}
should_retry_status(response.status().as_u16())
}
fn new_idempotency_key() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static COUNTER: AtomicU64 = AtomicU64::new(0);
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0);
let n = COUNTER.fetch_add(1, Ordering::Relaxed);
format!("{:x}-{:x}", nanos, n)
}
fn write_idempotency_key(method: &reqwest::Method) -> Option<String> {
if *method == reqwest::Method::GET {
None
} else {
Some(new_idempotency_key())
}
}
#[allow(dead_code)]
pub(crate) fn encode_path(value: impl std::fmt::Display) -> String {
const HEX: &[u8; 16] = b"0123456789ABCDEF";
let s = value.to_string();
let mut out = String::with_capacity(s.len());
for &b in s.as_bytes() {
match b {
b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'.' | b'_' | b'~' | b'/' => {
out.push(b as char)
}
_ => {
out.push('%');
out.push(HEX[(b >> 4) as usize] as char);
out.push(HEX[(b & 0x0f) as usize] as char);
}
}
}
out
}
#[derive(Clone)]
pub struct Steel {
http: reqwest::Client,
api_key: String,
base_url: String,
max_retries: u32,
default_headers: Vec<(String, String)>,
default_query: Vec<(String, String)>,
}
impl Steel {
pub fn new(api_key: impl Into<String>) -> Self {
let mut api_key = api_key.into();
if api_key.is_empty() {
if let Ok(v) = std::env::var("STEEL_API_KEY") {
api_key = v;
}
}
let base_url =
std::env::var("STEEL_BASE_URL").unwrap_or_else(|_| "https://api.steel.dev".to_string());
Self::with_base_url(api_key, base_url)
}
pub fn try_new(api_key: impl Into<String>) -> Result<Self, reqwest::Error> {
let mut api_key = api_key.into();
if api_key.is_empty() {
if let Ok(v) = std::env::var("STEEL_API_KEY") {
api_key = v;
}
}
let base_url =
std::env::var("STEEL_BASE_URL").unwrap_or_else(|_| "https://api.steel.dev".to_string());
Self::try_with_base_url(api_key, base_url)
}
pub fn with_base_url(api_key: impl Into<String>, base_url: impl Into<String>) -> Self {
Self {
http: build_http(Duration::from_secs(60)),
api_key: api_key.into(),
base_url: base_url.into(),
max_retries: 2,
default_headers: Vec::new(),
default_query: Vec::new(),
}
}
pub fn try_with_base_url(
api_key: impl Into<String>,
base_url: impl Into<String>,
) -> Result<Self, reqwest::Error> {
Ok(Self {
http: try_build_http(Duration::from_secs(60))?,
api_key: api_key.into(),
base_url: base_url.into(),
max_retries: 2,
default_headers: Vec::new(),
default_query: Vec::new(),
})
}
pub fn with_max_retries(mut self, max_retries: u32) -> Self {
self.max_retries = max_retries;
self
}
pub fn with_timeout(mut self, timeout: Duration) -> Self {
self.http = build_http(timeout);
self
}
pub fn try_with_timeout(mut self, timeout: Duration) -> Result<Self, reqwest::Error> {
self.http = try_build_http(timeout)?;
Ok(self)
}
pub fn with_default_header(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.default_headers.push((key.into(), value.into()));
self
}
pub fn with_default_query(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
self.default_query.push((key.into(), value.into()));
self
}
pub fn credentials(&self) -> crate::resources::Credentials<'_> {
crate::resources::Credentials::new(self)
}
pub fn extensions(&self) -> crate::resources::Extensions<'_> {
crate::resources::Extensions::new(self)
}
pub fn files(&self) -> crate::resources::Files<'_> {
crate::resources::Files::new(self)
}
pub fn profiles(&self) -> crate::resources::Profiles<'_> {
crate::resources::Profiles::new(self)
}
pub fn sessions(&self) -> crate::resources::Sessions<'_> {
crate::resources::Sessions::new(self)
}
pub fn pdf(
&self,
body: ClientPdfParams,
) -> RequestBuilder<'_, (), ClientPdfParams, PdfResponse> {
let path = "/v1/pdf".to_string();
self.call(reqwest::Method::POST, path, None::<()>, Some(body))
}
pub fn scrape(
&self,
body: ClientScrapeParams,
) -> RequestBuilder<'_, (), ClientScrapeParams, ScrapeResponse> {
let path = "/v1/scrape".to_string();
self.call(reqwest::Method::POST, path, None::<()>, Some(body))
}
pub fn screenshot(
&self,
body: ClientScreenshotParams,
) -> RequestBuilder<'_, (), ClientScreenshotParams, ScreenshotResponse> {
let path = "/v1/screenshot".to_string();
self.call(reqwest::Method::POST, path, None::<()>, Some(body))
}
fn authed(&self, method: reqwest::Method, path: &str) -> reqwest::RequestBuilder {
let url = format!("{}{}", self.base_url.trim_end_matches('/'), path);
let mut req = self.http.request(method, &url);
req = req.header("steel-api-key", &self.api_key);
req = req.header(
"user-agent",
concat!("steel-rust/", env!("CARGO_PKG_VERSION")),
);
req = req.header("x-steel-lang", "rust");
req = req.header("x-steel-package-version", env!("CARGO_PKG_VERSION"));
for (name, value) in &self.default_headers {
req = req.header(name, value);
}
if !self.default_query.is_empty() {
req = req.query(&self.default_query);
}
req
}
pub(crate) fn call<Q, B, T>(
&self,
method: reqwest::Method,
path: String,
query: Option<Q>,
body: Option<B>,
) -> RequestBuilder<'_, Q, B, T> {
RequestBuilder {
client: self,
method,
path,
query,
body,
options: RequestOptions::default(),
_marker: std::marker::PhantomData,
}
}
pub(crate) async fn request_with<Q, B, T>(
&self,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
body: Option<&B>,
options: &RequestOptions,
) -> Result<T, Error>
where
Q: Serialize + ?Sized,
B: Serialize + ?Sized,
T: DeserializeOwned,
{
let response = self
.send_request(method, path, query, body, options)
.await?;
decode_body::<T>(&response.bytes().await?)
}
pub(crate) async fn send_request<Q, B>(
&self,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
body: Option<&B>,
options: &RequestOptions,
) -> Result<reqwest::Response, Error>
where
Q: Serialize + ?Sized,
B: Serialize + ?Sized,
{
let idem_key = options
.idempotency_key
.clone()
.or_else(|| write_idempotency_key(&method));
let max_retries = options.max_retries.unwrap_or(self.max_retries);
let mut attempt: u32 = 0;
loop {
let mut req = self
.authed(method.clone(), path)
.header("accept", "application/json");
if let Some(q) = query {
req = req.query(q);
}
if !options.query.is_empty() {
req = req.query(&options.query);
}
if let Some(b) = body {
req = req.json(b);
}
if let Some(ref k) = idem_key {
req = req.header("idempotency-key", k);
}
for (name, value) in &options.headers {
req = req.header(name, value);
}
if let Some(timeout) = options.timeout {
req = req.timeout(timeout);
}
match req.send().await {
Ok(response) => {
let status = response.status();
if status.is_success() {
return Ok(response);
}
if should_retry_response(&response) && attempt < max_retries {
let wait = backoff_duration(attempt + 1, parse_retry_after(&response));
attempt += 1;
tokio::time::sleep(wait).await;
continue;
}
let code = status.as_u16();
let headers = response.headers().clone();
let message = response.text().await.unwrap_or_default();
return Err(Error::from_status(code, message, headers));
}
Err(e) => {
if (e.is_timeout() || e.is_connect()) && attempt < max_retries {
attempt += 1;
tokio::time::sleep(backoff_duration(attempt, None)).await;
continue;
}
if e.is_timeout() {
return Err(Error::Timeout(e));
}
return Err(Error::Transport(e));
}
}
}
}
pub(crate) async fn request_raw<Q, B>(
&self,
method: reqwest::Method,
path: &str,
query: Option<&Q>,
body: Option<&B>,
options: &RequestOptions,
) -> Result<Vec<u8>, Error>
where
Q: Serialize + ?Sized,
B: Serialize + ?Sized,
{
let idem_key = options
.idempotency_key
.clone()
.or_else(|| write_idempotency_key(&method));
let max_retries = options.max_retries.unwrap_or(self.max_retries);
let mut attempt: u32 = 0;
loop {
let mut req = self.authed(method.clone(), path);
if let Some(q) = query {
req = req.query(q);
}
if !options.query.is_empty() {
req = req.query(&options.query);
}
if let Some(b) = body {
req = req.json(b);
}
if let Some(ref k) = idem_key {
req = req.header("idempotency-key", k);
}
for (name, value) in &options.headers {
req = req.header(name, value);
}
if let Some(timeout) = options.timeout {
req = req.timeout(timeout);
}
match req.send().await {
Ok(response) => {
let status = response.status();
if status.is_success() {
return Ok(response.bytes().await?.to_vec());
}
if should_retry_response(&response) && attempt < max_retries {
let wait = backoff_duration(attempt + 1, parse_retry_after(&response));
attempt += 1;
tokio::time::sleep(wait).await;
continue;
}
let code = status.as_u16();
let headers = response.headers().clone();
let message = response.text().await.unwrap_or_default();
return Err(Error::from_status(code, message, headers));
}
Err(e) => {
if (e.is_timeout() || e.is_connect()) && attempt < max_retries {
attempt += 1;
tokio::time::sleep(backoff_duration(attempt, None)).await;
continue;
}
if e.is_timeout() {
return Err(Error::Timeout(e));
}
return Err(Error::Transport(e));
}
}
}
}
pub(crate) async fn request_multipart<F, Q, T>(
&self,
method: reqwest::Method,
path: &str,
build_form: F,
query: Option<&Q>,
options: &RequestOptions,
) -> Result<T, Error>
where
F: Fn() -> Result<reqwest::multipart::Form, reqwest::Error>,
Q: Serialize + ?Sized,
T: DeserializeOwned,
{
let idem_key = options
.idempotency_key
.clone()
.or_else(|| write_idempotency_key(&method));
let max_retries = options.max_retries.unwrap_or(self.max_retries);
let mut attempt: u32 = 0;
loop {
let form = build_form()?;
let mut req = self
.authed(method.clone(), path)
.header("accept", "application/json")
.multipart(form);
if let Some(q) = query {
req = req.query(q);
}
if !options.query.is_empty() {
req = req.query(&options.query);
}
if let Some(ref k) = idem_key {
req = req.header("idempotency-key", k);
}
for (name, value) in &options.headers {
req = req.header(name, value);
}
if let Some(timeout) = options.timeout {
req = req.timeout(timeout);
}
match req.send().await {
Ok(response) => {
let status = response.status();
if status.is_success() {
return decode_body::<T>(&response.bytes().await?);
}
if should_retry_response(&response) && attempt < max_retries {
let wait = backoff_duration(attempt + 1, parse_retry_after(&response));
attempt += 1;
tokio::time::sleep(wait).await;
continue;
}
let code = status.as_u16();
let headers = response.headers().clone();
let message = response.text().await.unwrap_or_default();
return Err(Error::from_status(code, message, headers));
}
Err(e) => {
if (e.is_timeout() || e.is_connect()) && attempt < max_retries {
attempt += 1;
tokio::time::sleep(backoff_duration(attempt, None)).await;
continue;
}
if e.is_timeout() {
return Err(Error::Timeout(e));
}
return Err(Error::Transport(e));
}
}
}
}
}
macro_rules! request_options_setters {
() => {
pub fn header(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.options.headers.push((name.into(), value.into()));
self
}
pub fn query_param(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
self.options.query.push((name.into(), value.into()));
self
}
pub fn max_retries(mut self, max_retries: u32) -> Self {
self.options.max_retries = Some(max_retries);
self
}
pub fn timeout(mut self, timeout: Duration) -> Self {
self.options.timeout = Some(timeout);
self
}
pub fn idempotency_key(mut self, key: impl Into<String>) -> Self {
self.options.idempotency_key = Some(key.into());
self
}
};
}
#[derive(Clone, Default)]
pub struct RequestOptions {
headers: Vec<(String, String)>,
query: Vec<(String, String)>,
max_retries: Option<u32>,
timeout: Option<Duration>,
idempotency_key: Option<String>,
}
pub struct Response<T> {
pub data: T,
pub status: reqwest::StatusCode,
pub headers: reqwest::header::HeaderMap,
}
pub struct RequestBuilder<'a, Q, B, T> {
client: &'a Steel,
method: reqwest::Method,
path: String,
query: Option<Q>,
body: Option<B>,
options: RequestOptions,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<'a, Q, B, T> RequestBuilder<'a, Q, B, T> {
request_options_setters! {}
}
impl<'a, Q, B, T> RequestBuilder<'a, Q, B, T>
where
Q: Serialize,
B: Serialize,
T: DeserializeOwned,
{
pub async fn send(self) -> Result<T, Error> {
self.client
.request_with(
self.method,
&self.path,
self.query.as_ref(),
self.body.as_ref(),
&self.options,
)
.await
}
pub async fn raw(self) -> Result<reqwest::Response, Error> {
self.client
.send_request(
self.method,
&self.path,
self.query.as_ref(),
self.body.as_ref(),
&self.options,
)
.await
}
pub async fn with_response(self) -> Result<Response<T>, Error> {
let response = self
.client
.send_request(
self.method,
&self.path,
self.query.as_ref(),
self.body.as_ref(),
&self.options,
)
.await?;
let status = response.status();
let headers = response.headers().clone();
let data = decode_body::<T>(&response.bytes().await?)?;
Ok(Response {
data,
status,
headers,
})
}
}
impl<'a, Q, B, T> std::future::IntoFuture for RequestBuilder<'a, Q, B, T>
where
Q: Serialize + Send + Sync + 'a,
B: Serialize + Send + Sync + 'a,
T: DeserializeOwned + 'a,
{
type Output = Result<T, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, Error>> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.send())
}
}
pub struct RawRequestBuilder<'a, Q, B> {
client: &'a Steel,
method: reqwest::Method,
path: String,
query: Option<Q>,
body: Option<B>,
options: RequestOptions,
}
impl<'a, Q, B> RawRequestBuilder<'a, Q, B> {
request_options_setters! {}
}
impl<'a, Q, B> RawRequestBuilder<'a, Q, B>
where
Q: Serialize,
B: Serialize,
{
pub async fn send(self) -> Result<Vec<u8>, Error> {
self.client
.request_raw(
self.method,
&self.path,
self.query.as_ref(),
self.body.as_ref(),
&self.options,
)
.await
}
}
impl<'a, Q, B> std::future::IntoFuture for RawRequestBuilder<'a, Q, B>
where
Q: Serialize + Send + Sync + 'a,
B: Serialize + Send + Sync + 'a,
{
type Output = Result<Vec<u8>, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<Vec<u8>, Error>> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.send())
}
}
impl Steel {
pub(crate) fn raw_call<Q, B>(
&self,
method: reqwest::Method,
path: String,
query: Option<Q>,
body: Option<B>,
) -> RawRequestBuilder<'_, Q, B> {
RawRequestBuilder {
client: self,
method,
path,
query,
body,
options: RequestOptions::default(),
}
}
}
pub struct MultipartRequestBuilder<'a, F, Q, T> {
client: &'a Steel,
method: reqwest::Method,
path: String,
build_form: F,
query: Option<Q>,
options: RequestOptions,
_marker: std::marker::PhantomData<fn() -> T>,
}
impl<'a, F, Q, T> MultipartRequestBuilder<'a, F, Q, T> {
request_options_setters! {}
}
impl<'a, F, Q, T> MultipartRequestBuilder<'a, F, Q, T>
where
F: Fn() -> Result<reqwest::multipart::Form, reqwest::Error>,
Q: Serialize,
T: DeserializeOwned,
{
pub async fn send(self) -> Result<T, Error> {
self.client
.request_multipart(
self.method,
&self.path,
self.build_form,
self.query.as_ref(),
&self.options,
)
.await
}
}
impl<'a, F, Q, T> std::future::IntoFuture for MultipartRequestBuilder<'a, F, Q, T>
where
F: Fn() -> Result<reqwest::multipart::Form, reqwest::Error> + Send + 'a,
Q: Serialize + Send + Sync + 'a,
T: DeserializeOwned + 'a,
{
type Output = Result<T, Error>;
type IntoFuture =
std::pin::Pin<Box<dyn std::future::Future<Output = Result<T, Error>> + Send + 'a>>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.send())
}
}
impl Steel {
pub(crate) fn multipart_call<F, Q, T>(
&self,
method: reqwest::Method,
path: String,
query: Option<Q>,
build_form: F,
) -> MultipartRequestBuilder<'_, F, Q, T>
where
F: Fn() -> Result<reqwest::multipart::Form, reqwest::Error>,
{
MultipartRequestBuilder {
client: self,
method,
path,
build_form,
query,
options: RequestOptions::default(),
_marker: std::marker::PhantomData,
}
}
}
pub struct PageRequestBuilder<T> {
client: Steel,
path: String,
params: Vec<(String, String)>,
next_cursor: crate::pagination::NextCursor<T>,
items_field: &'static str,
cursor_field: Option<&'static str>,
has_more_field: Option<&'static str>,
cursor_param: &'static str,
options: RequestOptions,
}
impl<T> PageRequestBuilder<T> {
request_options_setters! {}
}
impl<T> PageRequestBuilder<T>
where
T: DeserializeOwned,
{
pub async fn send(self) -> Result<crate::pagination::Page<T>, Error> {
crate::pagination::Page::fetch(
self.client,
self.path,
self.params,
self.next_cursor,
self.items_field,
self.cursor_field,
self.has_more_field,
self.cursor_param,
self.options,
)
.await
}
}
impl<T> std::future::IntoFuture for PageRequestBuilder<T>
where
T: DeserializeOwned + 'static,
{
type Output = Result<crate::pagination::Page<T>, Error>;
type IntoFuture = std::pin::Pin<
Box<dyn std::future::Future<Output = Result<crate::pagination::Page<T>, Error>> + Send>,
>;
fn into_future(self) -> Self::IntoFuture {
Box::pin(self.send())
}
}
impl Steel {
#[allow(clippy::too_many_arguments)]
pub(crate) fn page_call<T>(
&self,
path: String,
params: Vec<(String, String)>,
next_cursor: crate::pagination::NextCursor<T>,
items_field: &'static str,
cursor_field: Option<&'static str>,
has_more_field: Option<&'static str>,
cursor_param: &'static str,
) -> PageRequestBuilder<T> {
PageRequestBuilder {
client: self.clone(),
path,
params,
next_cursor,
items_field,
cursor_field,
has_more_field,
cursor_param,
options: RequestOptions::default(),
}
}
}
#[derive(Debug, Clone)]
pub struct ApiError {
pub status: u16,
pub message: String,
pub request_id: Option<String>,
pub headers: reqwest::header::HeaderMap,
}
#[derive(Debug)]
#[non_exhaustive]
pub enum Error {
Transport(reqwest::Error),
Timeout(reqwest::Error),
Decode(serde_json::Error),
BadRequest(ApiError),
Authentication(ApiError),
PermissionDenied(ApiError),
NotFound(ApiError),
Conflict(ApiError),
UnprocessableEntity(ApiError),
RateLimit(ApiError),
InternalServer(ApiError),
Api(ApiError),
}
fn extract_error_message(v: &serde_json::Value) -> Option<String> {
let obj = v.as_object()?;
for key in [
"message",
"detail",
"error_message",
"display_message",
"title",
] {
if let Some(s) = obj.get(key).and_then(|x| x.as_str()) {
return Some(s.to_string());
}
}
if let Some(err) = obj.get("error") {
if let Some(s) = err.get("message").and_then(|x| x.as_str()) {
return Some(s.to_string());
}
if let Some(s) = err.as_str() {
return Some(s.to_string());
}
}
if let Some(arr) = obj.get("errors").and_then(|x| x.as_array()) {
if let Some(s) = arr
.first()
.and_then(|e| e.get("message"))
.and_then(|x| x.as_str())
{
return Some(s.to_string());
}
}
None
}
impl Error {
fn from_status(status: u16, body: String, headers: reqwest::header::HeaderMap) -> Self {
let parsed: Option<serde_json::Value> = serde_json::from_str(&body).ok();
let message = parsed
.as_ref()
.and_then(extract_error_message)
.unwrap_or_else(|| body.clone());
let request_id = headers
.get("x-request-id")
.and_then(|v| v.to_str().ok())
.map(|s| s.to_string());
let detail = ApiError {
status,
message,
request_id,
headers,
};
match status {
400 => Error::BadRequest(detail),
401 => Error::Authentication(detail),
403 => Error::PermissionDenied(detail),
404 => Error::NotFound(detail),
409 => Error::Conflict(detail),
422 => Error::UnprocessableEntity(detail),
429 => Error::RateLimit(detail),
500..=599 => Error::InternalServer(detail),
_ => Error::Api(detail),
}
}
pub fn status(&self) -> Option<u16> {
match self {
Error::Transport(_) | Error::Timeout(_) | Error::Decode(_) => None,
Error::BadRequest(e)
| Error::Authentication(e)
| Error::PermissionDenied(e)
| Error::NotFound(e)
| Error::Conflict(e)
| Error::UnprocessableEntity(e)
| Error::RateLimit(e)
| Error::InternalServer(e)
| Error::Api(e) => Some(e.status),
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Error::Transport(e) => write!(f, "transport error: {}", e),
Error::Timeout(e) => write!(f, "request timed out: {}", e),
Error::Decode(e) => write!(f, "decode error: {}", e),
Error::BadRequest(e)
| Error::Authentication(e)
| Error::PermissionDenied(e)
| Error::NotFound(e)
| Error::Conflict(e)
| Error::UnprocessableEntity(e)
| Error::RateLimit(e)
| Error::InternalServer(e)
| Error::Api(e) => write!(f, "api error {}: {}", e.status, e.message),
}
}
}
impl std::error::Error for Error {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
Error::Transport(e) => Some(e),
Error::Timeout(e) => Some(e),
Error::Decode(e) => Some(e),
_ => None,
}
}
}
impl From<reqwest::Error> for Error {
fn from(e: reqwest::Error) -> Self {
Error::Transport(e)
}
}
impl From<serde_json::Error> for Error {
fn from(e: serde_json::Error) -> Self {
Error::Decode(e)
}
}