refactor(api): enhance error handling using error_stack crate
This commit is contained in:
122
src/api.rs
122
src/api.rs
@@ -1,19 +1,8 @@
|
||||
use crate::errors::{Error, Result};
|
||||
use error_stack::{Report, ResultExt};
|
||||
use reqwest::Client;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum ApiError {
|
||||
#[error("HTTP request failed: {0}")]
|
||||
Request(#[from] reqwest::Error),
|
||||
#[error("Deserialization error: {0}")]
|
||||
Deserialization(#[from] serde_path_to_error::Error<serde_json::Error>),
|
||||
#[error("API error: {message}")]
|
||||
Api { message: String },
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, ApiError>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct SonarrClient {
|
||||
@@ -31,86 +20,125 @@ impl SonarrClient {
|
||||
}
|
||||
}
|
||||
|
||||
async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T> {
|
||||
async fn get<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, Error> {
|
||||
let url = format!("{}/api/v3{}", self.base_url, endpoint);
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-Api-Key", &self.api_key)
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to send HTTP request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(ApiError::Api {
|
||||
message: format!("HTTP {}: {}", response.status(), response.text().await?),
|
||||
});
|
||||
let status = response.status();
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read error response body")?;
|
||||
return Err(Report::new(Error::Api)
|
||||
.attach_printable(format!("HTTP {}: {}", status, error_text)));
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read response body")?;
|
||||
let deser = &mut serde_json::Deserializer::from_str(&text);
|
||||
|
||||
serde_path_to_error::deserialize(deser).map_err(ApiError::from)
|
||||
serde_path_to_error::deserialize(deser)
|
||||
.change_context(Error::Serialization)
|
||||
.attach_printable("Failed to deserialize API response")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn get_debug<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T> {
|
||||
async fn get_debug<T: for<'de> Deserialize<'de>>(&self, endpoint: &str) -> Result<T, Error> {
|
||||
let url = format!("{}/api/v3{}", self.base_url, endpoint);
|
||||
let response = self
|
||||
.client
|
||||
.get(&url)
|
||||
.header("X-Api-Key", &self.api_key)
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to send debug HTTP request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(ApiError::Api {
|
||||
message: format!("HTTP {}: {}", response.status(), response.text().await?),
|
||||
});
|
||||
let status = response.status();
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read debug error response body")?;
|
||||
return Err(Report::new(Error::Api)
|
||||
.attach_printable(format!("Debug HTTP {}: {}", status, error_text)));
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read debug response body")?;
|
||||
let _ = std::fs::write(endpoint.replace("/", "_"), &text);
|
||||
let deser = &mut serde_json::Deserializer::from_str(&text);
|
||||
|
||||
serde_path_to_error::deserialize(deser).map_err(ApiError::from)
|
||||
serde_path_to_error::deserialize(deser)
|
||||
.change_context(Error::Serialization)
|
||||
.attach_printable("Failed to deserialize debug API response")
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn post<T: Serialize, R: for<'de> Deserialize<'de>>(
|
||||
&self,
|
||||
endpoint: &str,
|
||||
data: &T,
|
||||
) -> Result<R> {
|
||||
body: &T,
|
||||
) -> Result<R, Error> {
|
||||
let url = format!("{}/api/v3{}", self.base_url, endpoint);
|
||||
let response = self
|
||||
.client
|
||||
.post(&url)
|
||||
.header("X-Api-Key", &self.api_key)
|
||||
.json(data)
|
||||
.json(body)
|
||||
.send()
|
||||
.await?;
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to send POST request")?;
|
||||
|
||||
if !response.status().is_success() {
|
||||
return Err(ApiError::Api {
|
||||
message: format!("HTTP {}: {}", response.status(), response.text().await?),
|
||||
});
|
||||
let status = response.status();
|
||||
let error_text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read POST error response body")?;
|
||||
return Err(Report::new(Error::Api)
|
||||
.attach_printable(format!("POST HTTP {}: {}", status, error_text)));
|
||||
}
|
||||
|
||||
let text = response.text().await?;
|
||||
let text = response
|
||||
.text()
|
||||
.await
|
||||
.change_context(Error::Request)
|
||||
.attach_printable("Failed to read POST response body")?;
|
||||
let deser = &mut serde_json::Deserializer::from_str(&text);
|
||||
serde_path_to_error::deserialize(deser).map_err(ApiError::from)
|
||||
serde_path_to_error::deserialize(deser)
|
||||
.change_context(Error::Serialization)
|
||||
.attach_printable("Failed to deserialize POST API response")
|
||||
}
|
||||
|
||||
pub async fn get_system_status(&self) -> Result<SystemStatus> {
|
||||
pub async fn get_system_status(&self) -> Result<SystemStatus, Error> {
|
||||
self.get("/system/status").await
|
||||
}
|
||||
|
||||
pub async fn get_series(&self) -> Result<Vec<Series>> {
|
||||
pub async fn get_series(&self) -> Result<Vec<Series>, Error> {
|
||||
self.get("/series").await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_series_by_id(&self, id: u32) -> Result<Series> {
|
||||
pub async fn get_series_by_id(&self, id: u32) -> Result<Series, Error> {
|
||||
self.get(&format!("/series/{}", id)).await
|
||||
}
|
||||
|
||||
@@ -119,7 +147,7 @@ impl SonarrClient {
|
||||
&self,
|
||||
series_id: Option<u32>,
|
||||
season_number: Option<u32>,
|
||||
) -> Result<Vec<Episode>> {
|
||||
) -> Result<Vec<Episode>, Error> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(id) = series_id {
|
||||
query.push(format!("seriesId={}", id));
|
||||
@@ -141,7 +169,7 @@ impl SonarrClient {
|
||||
&self,
|
||||
start: Option<&str>,
|
||||
end: Option<&str>,
|
||||
) -> Result<Vec<Episode>> {
|
||||
) -> Result<Vec<Episode>, Error> {
|
||||
let mut query = Vec::new();
|
||||
if let Some(start_date) = start {
|
||||
query.push(format!("start={}", start_date));
|
||||
@@ -159,24 +187,24 @@ impl SonarrClient {
|
||||
self.get(&format!("/calendar{}", query_string)).await
|
||||
}
|
||||
|
||||
pub async fn get_queue(&self) -> Result<QueuePagingResource> {
|
||||
pub async fn get_queue(&self) -> Result<QueuePagingResource, Error> {
|
||||
self.get("/queue").await
|
||||
}
|
||||
|
||||
pub async fn get_history(&self) -> Result<HistoryPagingResource> {
|
||||
pub async fn get_history(&self) -> Result<HistoryPagingResource, Error> {
|
||||
self.get("/history").await
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn get_missing_episodes(&self) -> Result<EpisodePagingResource> {
|
||||
pub async fn get_missing_episodes(&self) -> Result<EpisodePagingResource, Error> {
|
||||
self.get("/wanted/missing").await
|
||||
}
|
||||
|
||||
pub async fn get_health(&self) -> Result<Vec<HealthResource>> {
|
||||
pub async fn get_health(&self) -> Result<Vec<HealthResource>, Error> {
|
||||
self.get("/health").await
|
||||
}
|
||||
|
||||
pub async fn search_series(&self, term: &str) -> Result<Vec<Series>> {
|
||||
pub async fn search_series(&self, term: &str) -> Result<Vec<Series>, Error> {
|
||||
self.get(&format!(
|
||||
"/series/lookup?term={}",
|
||||
urlencoding::encode(term)
|
||||
@@ -185,7 +213,7 @@ impl SonarrClient {
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub async fn add_series(&self, series: &Series) -> Result<Series> {
|
||||
pub async fn add_series(&self, series: &Series) -> Result<Series, Error> {
|
||||
self.post("/series", series).await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
// Removed unused imports Report and ResultExt
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
#[error("An error occurred")]
|
||||
pub struct Error;
|
||||
use error_stack::Report;
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub type Result<T, E = error_stack::Report<Error>> = core::result::Result<T, E>;
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("API error")]
|
||||
Api,
|
||||
#[error("HTTP request failed")]
|
||||
Request,
|
||||
#[error("Serialization/Deserialization error")]
|
||||
Serialization,
|
||||
}
|
||||
|
||||
pub type Result<T, E = Error> = core::result::Result<T, Report<E>>;
|
||||
|
||||
Reference in New Issue
Block a user