refactor(api): enhance error handling using error_stack crate
Some checks failed
build / checks-matrix (push) Successful in 19m20s
build / checks-build (push) Has been cancelled
docs / docs (push) Has been cancelled
build / codecov (push) Has been cancelled

This commit is contained in:
uttarayan21
2025-10-08 16:25:56 +05:30
parent 8139fe4cb3
commit e9ecd2a295
2 changed files with 87 additions and 53 deletions

View File

@@ -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
}
}

View File

@@ -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>>;