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_retry::{policies::ExponentialBackoff, RetryTransientMiddleware};
use response_deserializer::{ChapterImages, SearchResult};
use error::{ChapterImageError, ChapterImagesError};
use std::fs::File;
use std::io::Write;
use std::path::Path;
@@ -12,6 +13,8 @@ mod response_deserializer;
mod select;
mod test;
mod util;
mod error;
mod client;
use response_deserializer::{Chapter, Id};
use select::Entry;
@@ -34,7 +37,7 @@ async fn main() {
let client = &client;
let filters = [
// ("publicationDemographic[]", "seinen"),
//("status[]", "completed"),
// ("status[]", "completed"),
// ("contentRating[]", "suggestive"),
];
let limit = config.result_limit;
@@ -179,17 +182,25 @@ async fn main() {
let result = response_deserializer::deserialize_chapter_images(&json);
match result {
Ok(v) => break v,
Err(e) => {
if e.result != "error" {
panic!("brotha, api gone wrong (wild)");
}
for error in e.errors {
if error.status == 429 {
println!("you sent too many requests");
Err(e) => match e {
ChapterImagesError::Image(i) => match i {
ChapterImageError::Result(s) => {
eprintln!("chapter image error: {s}");
std::process::exit(1);
}
},
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!(
@@ -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 url = format!("{BASE}/manga/{id}/feed");
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;
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 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);
total_chapters_received += result.limit;
}
@@ -391,7 +402,7 @@ async fn search(
.text()
.await
.unwrap();
response_deserializer::deserializer(&json)
response_deserializer::deserializer(&json).unwrap()
}
async fn select_manga_from_search(
@@ -473,5 +484,5 @@ async fn id_query_get_info(client: &Client, id: &Id) -> response_deserializer::I
.text()
.await
.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)]
// TODO: Remove this
use crate::error::*;
use chrono::{DateTime, FixedOffset};
use serde::Deserialize;
use std::fmt::Display;
use std::fmt;
#[derive(Debug, Clone)]
pub struct Id(pub String);
@@ -94,7 +95,6 @@ pub enum Language {
Greenlandic,
Guarani,
Gujarati,
Gwic,
Haitian,
Hausa,
Hebrew,
@@ -168,7 +168,6 @@ pub enum Language {
NorwegianNynorsk,
Nuosu,
Nyanja,
Nynors,
Occidental,
Occitan,
Ojibwa,
@@ -210,7 +209,6 @@ pub enum Language {
Slovenian,
Somali,
Sotho,
SouthNdebele,
Spanish,
Sundanese,
Swahili,
@@ -322,23 +320,6 @@ struct ChapterImagesContent {
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)]
struct ChapterImageDataContent {
hash: String,
@@ -612,19 +593,6 @@ pub struct Titles {
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 {
type Error = ();
@@ -635,6 +603,7 @@ impl TryFrom<&str> for State {
})
}
}
impl TryFrom<&str> for ContentRating {
type Error = ();
@@ -648,6 +617,7 @@ impl TryFrom<&str> for ContentRating {
})
}
}
impl TryFrom<&str> for Language {
type Error = ();
@@ -873,6 +843,7 @@ impl TryFrom<&str> for Language {
"zh-ro" => Language::RomanizedChinese,
"zh" => Language::SimplifiedChinese,
"zh-hk" => Language::TraditionalChinese,
"ko-ro" => Language::RomanizedKorean,
"pt" => Language::Portuguese,
"es" => Language::CastilianSpanish,
"es-la" => Language::LatinAmericanSpanish,
@@ -883,6 +854,7 @@ impl TryFrom<&str> for Language {
})
}
}
impl TryFrom<&str> for PublicationDemographic {
type Error = ();
@@ -896,6 +868,7 @@ impl TryFrom<&str> for PublicationDemographic {
})
}
}
impl TryFrom<&str> for DataType {
type Error = ();
@@ -915,6 +888,7 @@ impl TryFrom<&str> for DataType {
})
}
}
impl TryFrom<&str> for Status {
type Error = ();
@@ -928,6 +902,7 @@ impl TryFrom<&str> for Status {
})
}
}
impl TryFrom<&str> for ResponseResult {
type Error = ();
@@ -938,6 +913,7 @@ impl TryFrom<&str> for ResponseResult {
}
}
}
impl TryFrom<&str> for Response {
type Error = ();
@@ -981,28 +957,13 @@ impl TryFrom<SearchResponse> for SearchResult {
}
}
#[derive(Debug)]
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 {
impl fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
self.0.fmt(f)
}
}
impl Display for Status {
impl fmt::Display for Status {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
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 {
match self {
Self::Safe => "safe".fmt(f),
@@ -1167,79 +1128,55 @@ impl TryFrom<ContentAttributes> for MangaAttributes {
}
}
pub fn deserialize_id_query(json: &str) -> IdQueryResult {
let id_query_response: IdQueryResponse = match serde_json::from_str(json) {
Ok(v) => v,
Err(e) => {
eprintln!("ERROR: {e:#?}");
std::fs::write("out.json", json).unwrap();
std::process::exit(1);
}
};
id_query_response.try_into().unwrap()
pub fn deserialize_id_query(json: &str) -> Result<IdQueryResult, IdQueryResultError> {
let id_query_response: IdQueryResponse = serde_json::from_str(json)
.map_err(|e| IdQueryResultError::Serde(JsonError::Message(e.to_string())))?;
id_query_response
.try_into()
.map_err(IdQueryResultError::IdQueryResult)
}
impl TryFrom<IdQueryResponse> for IdQueryResult {
type Error = AttributeConversionError;
type Error = IdQueryResponseError;
fn try_from(response: IdQueryResponse) -> Result<Self, Self::Error> {
Ok(IdQueryResult {
result: response.result.as_str().try_into().unwrap(),
response: response.response.as_str().try_into().unwrap(),
data: response.data.try_into().unwrap(),
result: response
.result
.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) {
Ok(v) => v,
Err(e) => {
eprintln!("ERROR: {e:#?}");
std::fs::write("chapter_feed.json", json).unwrap();
std::process::exit(1);
// TODO: Actually do error handling here
return Err(ChapterFeedError::Serde(JsonError::Message(e.to_string())));
}
};
chapter_feed_response.try_into().unwrap()
chapter_feed_response
.try_into()
.map_err(|e| ChapterFeedError::Conversion(e))
}
pub fn deserializer(json: &str) -> SearchResult {
let search_response: SearchResponse = match serde_json::from_str(json) {
Ok(v) => v,
Err(e) => {
eprintln!("ERROR: {e:#?}");
std::fs::write("search_result.json", json).unwrap();
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),
pub fn deserializer(json: &str) -> Result<SearchResult, SearchResultError> {
let search_response: SearchResponse = serde_json::from_str(json)
.map_err(|e| SearchResultError::Serde(JsonError::Message(e.to_string())))?;
search_response
.try_into()
.map_err(SearchResultError::SearchResult)
}
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 {
type Error = ChapterAttributeConversionError;
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) {
Ok(v) => v,
Err(e) => {
match serde_json::from_str::<ChapterImagesContentError>(json) {
Ok(v) => return Err(v),
Err(e) => {
Ok(v) => return Err(ChapterImagesError::Content(v)),
Err(f) => {
// 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);
}
}
}
};
Ok(chapter_images.try_into().unwrap())
chapter_images.try_into().map_err(ChapterImagesError::Image)
}
impl TryFrom<ContentData> for Manga {
@@ -1396,14 +1323,12 @@ impl TryFrom<ContentData> for Manga {
Ok(Manga {
id: Id(m.id),
data_type: (m.type_name.as_str()).try_into().map_err(|_| {
ResponseConversionError::AttributeError(AttributeConversionError::DataType(
m.type_name,
))
ResponseConversionError::Attribute(AttributeConversionError::DataType(m.type_name))
})?,
attributes: m
.attributes
.try_into()
.map_err(ResponseConversionError::AttributeError)?,
.map_err(ResponseConversionError::Attribute)?,
relationships: {
let mut relationships = Vec::with_capacity(m.relationships.len());
for m in m.relationships {
@@ -1458,7 +1383,7 @@ impl TryFrom<ContentData> for Manga {
})
}
})()
.map_err(ResponseConversionError::AttributeError)?,
.map_err(ResponseConversionError::Attribute)?,
);
}
relationships