From e9ecd2a295ac2afe10602aba09792dc906cb5013 Mon Sep 17 00:00:00 2001 From: uttarayan21 Date: Wed, 8 Oct 2025 16:25:56 +0530 Subject: [PATCH] refactor(api): enhance error handling using error_stack crate --- src/api.rs | 122 +++++++++++++++++++++++++++++++------------------- src/errors.rs | 18 +++++--- 2 files changed, 87 insertions(+), 53 deletions(-) diff --git a/src/api.rs b/src/api.rs index 64b97b5..13a98db 100644 --- a/src/api.rs +++ b/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), - #[error("API error: {message}")] - Api { message: String }, -} - -pub type Result = std::result::Result; #[derive(Clone)] pub struct SonarrClient { @@ -31,86 +20,125 @@ impl SonarrClient { } } - async fn get Deserialize<'de>>(&self, endpoint: &str) -> Result { + async fn get Deserialize<'de>>(&self, endpoint: &str) -> Result { 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 Deserialize<'de>>(&self, endpoint: &str) -> Result { + async fn get_debug Deserialize<'de>>(&self, endpoint: &str) -> Result { 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 Deserialize<'de>>( &self, endpoint: &str, - data: &T, - ) -> Result { + body: &T, + ) -> Result { 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 { + pub async fn get_system_status(&self) -> Result { self.get("/system/status").await } - pub async fn get_series(&self) -> Result> { + pub async fn get_series(&self) -> Result, Error> { self.get("/series").await } #[allow(dead_code)] - pub async fn get_series_by_id(&self, id: u32) -> Result { + pub async fn get_series_by_id(&self, id: u32) -> Result { self.get(&format!("/series/{}", id)).await } @@ -119,7 +147,7 @@ impl SonarrClient { &self, series_id: Option, season_number: Option, - ) -> Result> { + ) -> Result, 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> { + ) -> Result, 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 { + pub async fn get_queue(&self) -> Result { self.get("/queue").await } - pub async fn get_history(&self) -> Result { + pub async fn get_history(&self) -> Result { self.get("/history").await } #[allow(dead_code)] - pub async fn get_missing_episodes(&self) -> Result { + pub async fn get_missing_episodes(&self) -> Result { self.get("/wanted/missing").await } - pub async fn get_health(&self) -> Result> { + pub async fn get_health(&self) -> Result, Error> { self.get("/health").await } - pub async fn search_series(&self, term: &str) -> Result> { + pub async fn search_series(&self, term: &str) -> Result, 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 { + pub async fn add_series(&self, series: &Series) -> Result { self.post("/series", series).await } } diff --git a/src/errors.rs b/src/errors.rs index 2964c41..75205af 100644 --- a/src/errors.rs +++ b/src/errors.rs @@ -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> = core::result::Result; +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("API error")] + Api, + #[error("HTTP request failed")] + Request, + #[error("Serialization/Deserialization error")] + Serialization, +} + +pub type Result = core::result::Result>;