more sane error handling with thiserror

This commit is contained in:
2025-06-26 17:05:40 +02:00
parent 9b649a39fe
commit 920d9ee25a
3 changed files with 295 additions and 144 deletions

215
src/error.rs Normal file
View File

@@ -0,0 +1,215 @@
#![allow(unused)]
use serde::Deserialize;
use std::fmt;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum ResponseConversionError {
#[error("failed to convert Attribute")]
Attribute(#[from] AttributeConversionError),
#[error("failed to convert Response, got {0}")]
Response(String),
#[error("failed to convert Result, got {0}")]
Result(String),
#[error("failed to convert ContentType, got {0}")]
ContentType(String),
}
#[derive(Error, Debug)]
pub enum AttributeConversionError {
#[error("failed to convert attribute Language, got {0}")]
Language(String),
#[error("failed to convert attribute Locale, got {0}")]
Locale(String),
#[error("failed to convert attribute LastVolume, got {0}")]
LastVolume(String),
#[error("failed to convert attribute LastChapter, got {0}")]
LastChapter(String),
#[error("failed to convert attribute CreatedAtDateTime, got {0}")]
CreatedAtDateTime(String),
#[error("failed to convert attribute UpdatedAtDateTime, got {0}")]
UpdatedAtDateTime(String),
#[error("failed to convert attribute State, got {0}")]
State(String),
#[error("failed to convert attribute ContentRating, got {0}")]
ContentRating(String),
#[error("failed to convert attribute Status, got {0}")]
Status(String),
#[error("failed to convert attribute PublicationDemographic, got {0}")]
PublicationDemographic(String),
#[error("failed to convert attribute DataType, got {0}")]
DataType(String),
}
#[derive(Debug, Error)]
pub enum ChapterImageError {
#[error("chapter image result {0}")]
Result(String),
}
#[derive(Debug, Error)]
pub enum IdQueryResponseError {
#[error("failed to convert id query response Result")]
Result,
#[error("failed to convert id query response Response")]
Response,
#[error("failed to convert id query response Data")]
Data(#[from] ResponseConversionError),
}
#[derive(Debug, Error)]
pub enum JsonError {
#[error("failed to parse json, msg: {0}")]
Message(String),
#[error("failed to parse json, unexpected end of file")]
Eof,
#[error("failed to parse json, syntax error")]
Syntax,
#[error("failed to parse json, expected boolean")]
ExpectedBoolean,
#[error("failed to parse json, expected integer")]
ExpectedInteger,
#[error("failed to parse json, expected string")]
ExpectedString,
#[error("failed to parse json, expected null")]
ExpectedNull,
#[error("failed to parse json, expected array")]
ExpectedArray,
#[error("failed to parse json, expected array comma")]
ExpectedArrayComma,
#[error("failed to parse json, expected array end")]
ExpectedArrayEnd,
#[error("failed to parse json, expected map")]
ExpectedMap,
#[error("failed to parse json, expected map colon")]
ExpectedMapColon,
#[error("failed to parse json, expected map comma")]
ExpectedMapComma,
#[error("failed to parse json, expected map end")]
ExpectedMapEnd,
#[error("failed to parse json, expected enum")]
ExpectedEnum,
#[error("failed to parse json, expected trailing characters")]
TrailingCharacters,
}
#[derive(Debug, Error)]
pub enum ChapterFeedError {
#[error("failed to parse json")]
Serde(#[from] JsonError),
#[error("failed to convert chapter feed")]
Conversion(#[from] ChapterFeedConversionError),
}
#[derive(Debug, Error)]
pub enum ChapterFeedConversionError {
#[error("failed to convert chapter feed result, got {0}")]
Result(String),
#[error("failed to convert chapter feed response, got {0}")]
Response(String),
#[error("failed to convert chapter feed chapter")]
Chapter(#[from] ChapterConversionError),
}
#[derive(Debug, Error)]
pub enum ChapterConversionError {
#[error("failed to convert chapter DataType, got {0}")]
DataType(String),
#[error("failed to convert chapter Id, got {0}")]
Id(String),
#[error("failed to convert chapter Relationship")]
Relationship(#[from] ChapterRelationshipError),
#[error("failed to convert chapter Attributes")]
Attributes(#[from] ChapterAttributeConversionError),
}
#[derive(Debug, Error)]
pub enum ChapterRelationshipError {
#[error("failed to convert chapter relationship TypeData, got {0}")]
TypeData(String),
#[error("failed to convert chapter relationship Id, got {0}")]
Id(String),
}
#[derive(Error, Debug)]
pub enum ChapterAttributeConversionError {
#[error("unable to convert chapter attribute Volume, got {0}")]
Volume(String),
#[error("unable to convert chapter attribute Chapter, got {0}")]
Chapter(String),
#[error("unable to convert chapter attribute CreatedAt, got {0}")]
CreatedAt(String),
#[error("unable to convert chapter attribute UpdatedAt, got {0}")]
UpdatedAt(String),
#[error("unable to convert chapter attribute PublishedAt, got {0}")]
PublishedAt(String),
#[error("unable to convert chapter attribute TranslatedLanguage, got {0}")]
TranslatedLanguage(String),
}
#[derive(Debug, Error)]
pub enum ChapterImagesError {
#[error("failed to deserialize chapter images")]
Image(#[from] ChapterImageError),
#[error("failed to deserialize chapter images")]
Content(#[from] ChapterImagesContentError),
}
#[derive(Debug, Error)]
pub enum SearchResultError {
#[error("failed to deserialize json")]
Serde(#[from] JsonError),
#[error("failed to convert response to SearchResult")]
SearchResult(#[from] ResponseConversionError),
}
#[derive(Debug, Error)]
pub enum IdQueryResultError {
#[error("failed to deserialize json")]
Serde(#[from] JsonError),
#[error("failed to convert to IdQueryResult")]
IdQueryResult(#[from] IdQueryResponseError),
}
#[derive(Debug, Error, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
pub id: String,
pub status: u32,
pub title: String,
pub detail: String,
pub context: Option<String>,
}
#[derive(Debug, Error, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChapterImagesContentError {
pub result: String,
pub errors: Vec<ApiError>,
}
impl fmt::Display for ApiError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
format!(
"Api Error:\nid: {}\nstatus: {}\ntitle: {}\ndetail: {}\ncontext{:?}",
self.id, self.status, self.title, self.detail, self.context
)
.fmt(f)
}
}
impl fmt::Display for ChapterImagesContentError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
format!(
"chapter images content error:\nresult: {}\nerrors: \n{:#?}",
self.result, self.errors
)
.fmt(f)
}
}
impl serde::de::Error for JsonError {
fn custom<T: fmt::Display>(msg: T) -> Self {
JsonError::Message(msg.to_string())
}
}

View File

@@ -1,6 +1,7 @@
use reqwest_middleware::{ClientBuilder, ClientWithMiddleware}; use reqwest_middleware::{ClientBuilder, ClientWithMiddleware};
use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware}; use reqwest_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use response_deserializer::{ChapterImages, SearchResult}; use response_deserializer::{ChapterImages, SearchResult};
use error::{ChapterImageError, ChapterImagesError};
use std::fs::File; use std::fs::File;
use std::io::Write; use std::io::Write;
use std::path::Path; use std::path::Path;
@@ -12,6 +13,8 @@ mod response_deserializer;
mod select; mod select;
mod test; mod test;
mod util; mod util;
mod error;
mod client;
use response_deserializer::{Chapter, Id}; use response_deserializer::{Chapter, Id};
use select::Entry; use select::Entry;
@@ -34,7 +37,7 @@ async fn main() {
let client = &client; let client = &client;
let filters = [ let filters = [
// ("publicationDemographic[]", "seinen"), // ("publicationDemographic[]", "seinen"),
//("status[]", "completed"), // ("status[]", "completed"),
// ("contentRating[]", "suggestive"), // ("contentRating[]", "suggestive"),
]; ];
let limit = config.result_limit; let limit = config.result_limit;
@@ -179,17 +182,25 @@ async fn main() {
let result = response_deserializer::deserialize_chapter_images(&json); let result = response_deserializer::deserialize_chapter_images(&json);
match result { match result {
Ok(v) => break v, Ok(v) => break v,
Err(e) => { Err(e) => match e {
if e.result != "error" { ChapterImagesError::Image(i) => match i {
panic!("brotha, api gone wrong (wild)"); ChapterImageError::Result(s) => {
} eprintln!("chapter image error: {s}");
for error in e.errors { std::process::exit(1);
if error.status == 429 { }
println!("you sent too many requests"); },
ChapterImagesError::Content(e) => {
if e.result != "error" {
panic!("brotha, api gone wrong (wild)");
}
for error in e.errors {
if error.status == 429 {
eprintln!("you sent too many requests");
}
std::thread::sleep(std::time::Duration::from_millis(20000));
} }
std::thread::sleep(std::time::Duration::from_millis(20000));
} }
} },
} }
}; };
println!( println!(
@@ -350,7 +361,7 @@ async fn get_chapters(client: &Client, id: &Id) -> Result<Vec<Chapter>, reqwest_
let params = [("limit", limit.as_str()), ("translatedLanguage[]", "en")]; let params = [("limit", limit.as_str()), ("translatedLanguage[]", "en")];
let url = format!("{BASE}/manga/{id}/feed"); let url = format!("{BASE}/manga/{id}/feed");
let json = client.get(url).query(&params).send().await?.text().await?; let json = client.get(url).query(&params).send().await?.text().await?;
let mut result = response_deserializer::deserialize_chapter_feed(&json); let mut result = response_deserializer::deserialize_chapter_feed(&json).unwrap();
let mut total_chapters_received = result.limit; let mut total_chapters_received = result.limit;
while total_chapters_received < result.total { while total_chapters_received < result.total {
@@ -362,7 +373,7 @@ async fn get_chapters(client: &Client, id: &Id) -> Result<Vec<Chapter>, reqwest_
]; ];
let url = format!("{BASE}/manga/{id}/feed"); let url = format!("{BASE}/manga/{id}/feed");
let json = client.get(url).query(&params).send().await?.text().await?; let json = client.get(url).query(&params).send().await?.text().await?;
let mut new_result = response_deserializer::deserialize_chapter_feed(&json); let mut new_result = response_deserializer::deserialize_chapter_feed(&json).unwrap();
result.data.append(&mut new_result.data); result.data.append(&mut new_result.data);
total_chapters_received += result.limit; total_chapters_received += result.limit;
} }
@@ -391,7 +402,7 @@ async fn search(
.text() .text()
.await .await
.unwrap(); .unwrap();
response_deserializer::deserializer(&json) response_deserializer::deserializer(&json).unwrap()
} }
async fn select_manga_from_search( async fn select_manga_from_search(
@@ -473,5 +484,5 @@ async fn id_query_get_info(client: &Client, id: &Id) -> response_deserializer::I
.text() .text()
.await .await
.unwrap(); .unwrap();
response_deserializer::deserialize_id_query(&json) response_deserializer::deserialize_id_query(&json).unwrap()
} }

View File

@@ -1,8 +1,9 @@
// TODO: Remove this
#![allow(unused)] #![allow(unused)]
// TODO: Remove this
use crate::error::*;
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
use serde::Deserialize; use serde::Deserialize;
use std::fmt::Display; use std::fmt;
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Id(pub String); pub struct Id(pub String);
@@ -94,7 +95,6 @@ pub enum Language {
Greenlandic, Greenlandic,
Guarani, Guarani,
Gujarati, Gujarati,
Gwic,
Haitian, Haitian,
Hausa, Hausa,
Hebrew, Hebrew,
@@ -168,7 +168,6 @@ pub enum Language {
NorwegianNynorsk, NorwegianNynorsk,
Nuosu, Nuosu,
Nyanja, Nyanja,
Nynors,
Occidental, Occidental,
Occitan, Occitan,
Ojibwa, Ojibwa,
@@ -210,7 +209,6 @@ pub enum Language {
Slovenian, Slovenian,
Somali, Somali,
Sotho, Sotho,
SouthNdebele,
Spanish, Spanish,
Sundanese, Sundanese,
Swahili, Swahili,
@@ -322,23 +320,6 @@ struct ChapterImagesContent {
chapter: ChapterImageDataContent, chapter: ChapterImageDataContent,
} }
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ApiError {
pub id: String,
pub status: u32,
pub title: String,
pub detail: String,
pub context: Option<String>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ChapterImagesContentError {
pub result: String,
pub errors: Vec<ApiError>,
}
#[derive(Debug, Deserialize)] #[derive(Debug, Deserialize)]
struct ChapterImageDataContent { struct ChapterImageDataContent {
hash: String, hash: String,
@@ -612,19 +593,6 @@ pub struct Titles {
pub en: String, pub en: String,
} }
#[derive(Debug)]
enum ResponseConversionError {
AttributeError(AttributeConversionError),
Response(String),
Result(String),
ContentType(String),
}
#[derive(Debug)]
enum ChapterImageError {
Result(String),
}
impl TryFrom<&str> for State { impl TryFrom<&str> for State {
type Error = (); type Error = ();
@@ -635,6 +603,7 @@ impl TryFrom<&str> for State {
}) })
} }
} }
impl TryFrom<&str> for ContentRating { impl TryFrom<&str> for ContentRating {
type Error = (); type Error = ();
@@ -648,6 +617,7 @@ impl TryFrom<&str> for ContentRating {
}) })
} }
} }
impl TryFrom<&str> for Language { impl TryFrom<&str> for Language {
type Error = (); type Error = ();
@@ -873,6 +843,7 @@ impl TryFrom<&str> for Language {
"zh-ro" => Language::RomanizedChinese, "zh-ro" => Language::RomanizedChinese,
"zh" => Language::SimplifiedChinese, "zh" => Language::SimplifiedChinese,
"zh-hk" => Language::TraditionalChinese, "zh-hk" => Language::TraditionalChinese,
"ko-ro" => Language::RomanizedKorean,
"pt" => Language::Portuguese, "pt" => Language::Portuguese,
"es" => Language::CastilianSpanish, "es" => Language::CastilianSpanish,
"es-la" => Language::LatinAmericanSpanish, "es-la" => Language::LatinAmericanSpanish,
@@ -883,6 +854,7 @@ impl TryFrom<&str> for Language {
}) })
} }
} }
impl TryFrom<&str> for PublicationDemographic { impl TryFrom<&str> for PublicationDemographic {
type Error = (); type Error = ();
@@ -896,6 +868,7 @@ impl TryFrom<&str> for PublicationDemographic {
}) })
} }
} }
impl TryFrom<&str> for DataType { impl TryFrom<&str> for DataType {
type Error = (); type Error = ();
@@ -915,6 +888,7 @@ impl TryFrom<&str> for DataType {
}) })
} }
} }
impl TryFrom<&str> for Status { impl TryFrom<&str> for Status {
type Error = (); type Error = ();
@@ -928,6 +902,7 @@ impl TryFrom<&str> for Status {
}) })
} }
} }
impl TryFrom<&str> for ResponseResult { impl TryFrom<&str> for ResponseResult {
type Error = (); type Error = ();
@@ -938,6 +913,7 @@ impl TryFrom<&str> for ResponseResult {
} }
} }
} }
impl TryFrom<&str> for Response { impl TryFrom<&str> for Response {
type Error = (); type Error = ();
@@ -981,28 +957,13 @@ impl TryFrom<SearchResponse> for SearchResult {
} }
} }
#[derive(Debug)] impl fmt::Display for Id {
enum AttributeConversionError {
Language(String),
Locale(String),
LastVolume(String),
LastChapter(String),
CreatedAtDateTime(String),
UpdatedAtDateTime(String),
State(String),
ContentRating(String),
Status(String),
PublicationDemographic(String),
DataType(String),
}
impl Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f) self.0.fmt(f)
} }
} }
impl Display for Status { impl fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Ongoing => "ongoing".fmt(f), Self::Ongoing => "ongoing".fmt(f),
@@ -1013,7 +974,7 @@ impl Display for Status {
} }
} }
impl Display for ContentRating { impl fmt::Display for ContentRating {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self { match self {
Self::Safe => "safe".fmt(f), Self::Safe => "safe".fmt(f),
@@ -1167,79 +1128,55 @@ impl TryFrom<ContentAttributes> for MangaAttributes {
} }
} }
pub fn deserialize_id_query(json: &str) -> IdQueryResult { pub fn deserialize_id_query(json: &str) -> Result<IdQueryResult, IdQueryResultError> {
let id_query_response: IdQueryResponse = match serde_json::from_str(json) { let id_query_response: IdQueryResponse = serde_json::from_str(json)
Ok(v) => v, .map_err(|e| IdQueryResultError::Serde(JsonError::Message(e.to_string())))?;
Err(e) => { id_query_response
eprintln!("ERROR: {e:#?}"); .try_into()
std::fs::write("out.json", json).unwrap(); .map_err(IdQueryResultError::IdQueryResult)
std::process::exit(1);
}
};
id_query_response.try_into().unwrap()
} }
impl TryFrom<IdQueryResponse> for IdQueryResult { impl TryFrom<IdQueryResponse> for IdQueryResult {
type Error = AttributeConversionError; type Error = IdQueryResponseError;
fn try_from(response: IdQueryResponse) -> Result<Self, Self::Error> { fn try_from(response: IdQueryResponse) -> Result<Self, Self::Error> {
Ok(IdQueryResult { Ok(IdQueryResult {
result: response.result.as_str().try_into().unwrap(), result: response
response: response.response.as_str().try_into().unwrap(), .result
data: response.data.try_into().unwrap(), .as_str()
.try_into()
.map_err(|_| IdQueryResponseError::Result)?,
response: response
.response
.as_str()
.try_into()
.map_err(|_| IdQueryResponseError::Response)?,
data: response
.data
.try_into()
.map_err(IdQueryResponseError::Data)?,
}) })
} }
} }
pub fn deserialize_chapter_feed(json: &str) -> ChapterFeed { pub fn deserialize_chapter_feed(json: &str) -> Result<ChapterFeed, ChapterFeedError> {
let chapter_feed_response: ChapterFeedResponse = match serde_json::from_str(json) { let chapter_feed_response: ChapterFeedResponse = match serde_json::from_str(json) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
eprintln!("ERROR: {e:#?}"); // TODO: Actually do error handling here
std::fs::write("chapter_feed.json", json).unwrap(); return Err(ChapterFeedError::Serde(JsonError::Message(e.to_string())));
std::process::exit(1);
} }
}; };
chapter_feed_response.try_into().unwrap() chapter_feed_response
.try_into()
.map_err(|e| ChapterFeedError::Conversion(e))
} }
pub fn deserializer(json: &str) -> SearchResult { pub fn deserializer(json: &str) -> Result<SearchResult, SearchResultError> {
let search_response: SearchResponse = match serde_json::from_str(json) { let search_response: SearchResponse = serde_json::from_str(json)
Ok(v) => v, .map_err(|e| SearchResultError::Serde(JsonError::Message(e.to_string())))?;
Err(e) => { search_response
eprintln!("ERROR: {e:#?}"); .try_into()
std::fs::write("search_result.json", json).unwrap(); .map_err(SearchResultError::SearchResult)
std::process::exit(1);
}
};
let search_result = search_response.try_into();
match search_result {
Ok(v) => v,
Err(e) => {
eprintln!("ERROR: Failed to convert search response: {e:#?}");
std::process::exit(1);
}
}
}
#[derive(Debug)]
enum ChapterFeedConversionError {
Result(String),
Response(String),
Chapter(ChapterConversionError),
}
#[derive(Debug)]
enum ChapterConversionError {
DataType(String),
Id(String),
Relationship(ChapterRelationshipError),
Attributes(ChapterAttributeConversionError),
}
#[derive(Debug)]
enum ChapterRelationshipError {
TypeData(String),
Id(String),
} }
impl TryFrom<ChapterFeedResponse> for ChapterFeed { impl TryFrom<ChapterFeedResponse> for ChapterFeed {
@@ -1308,16 +1245,6 @@ impl TryFrom<ChapterFeedResponse> for ChapterFeed {
} }
} }
#[derive(Debug)]
enum ChapterAttributeConversionError {
Volume(String),
Chapter(String),
CreatedAt(String),
UpdatedAt(String),
PublishedAt(String),
TranslatedLanguage(String),
}
impl TryFrom<ChapterAttributesContent> for ChapterAttributes { impl TryFrom<ChapterAttributesContent> for ChapterAttributes {
type Error = ChapterAttributeConversionError; type Error = ChapterAttributeConversionError;
fn try_from(attributes: ChapterAttributesContent) -> Result<Self, Self::Error> { fn try_from(attributes: ChapterAttributesContent) -> Result<Self, Self::Error> {
@@ -1373,21 +1300,21 @@ impl TryFrom<ChapterImagesContent> for ChapterImages {
} }
} }
pub fn deserialize_chapter_images(json: &str) -> Result<ChapterImages, ChapterImagesContentError> { pub fn deserialize_chapter_images(json: &str) -> Result<ChapterImages, ChapterImagesError> {
let chapter_images: ChapterImagesContent = match serde_json::from_str(json) { let chapter_images: ChapterImagesContent = match serde_json::from_str(json) {
Ok(v) => v, Ok(v) => v,
Err(e) => { Err(e) => {
match serde_json::from_str::<ChapterImagesContentError>(json) { match serde_json::from_str::<ChapterImagesContentError>(json) {
Ok(v) => return Err(v), Ok(v) => return Err(ChapterImagesError::Content(v)),
Err(e) => { Err(f) => {
// If you can't parse the error then there is no point in continuing. // If you can't parse the error then there is no point in continuing.
eprintln!("ERROR: {e:#?}"); eprintln!("ERROR: {e:#?}, with {f:#?}");
std::process::exit(1); std::process::exit(1);
} }
} }
} }
}; };
Ok(chapter_images.try_into().unwrap()) chapter_images.try_into().map_err(ChapterImagesError::Image)
} }
impl TryFrom<ContentData> for Manga { impl TryFrom<ContentData> for Manga {
@@ -1396,14 +1323,12 @@ impl TryFrom<ContentData> for Manga {
Ok(Manga { Ok(Manga {
id: Id(m.id), id: Id(m.id),
data_type: (m.type_name.as_str()).try_into().map_err(|_| { data_type: (m.type_name.as_str()).try_into().map_err(|_| {
ResponseConversionError::AttributeError(AttributeConversionError::DataType( ResponseConversionError::Attribute(AttributeConversionError::DataType(m.type_name))
m.type_name,
))
})?, })?,
attributes: m attributes: m
.attributes .attributes
.try_into() .try_into()
.map_err(ResponseConversionError::AttributeError)?, .map_err(ResponseConversionError::Attribute)?,
relationships: { relationships: {
let mut relationships = Vec::with_capacity(m.relationships.len()); let mut relationships = Vec::with_capacity(m.relationships.len());
for m in m.relationships { for m in m.relationships {
@@ -1458,7 +1383,7 @@ impl TryFrom<ContentData> for Manga {
}) })
} }
})() })()
.map_err(ResponseConversionError::AttributeError)?, .map_err(ResponseConversionError::Attribute)?,
); );
} }
relationships relationships