diff --git a/Cargo.toml b/Cargo.toml index fc90cb5..3ec4646 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -chrono = { version = "0.4.38", default-features = false } +chrono = { version = "0.4.38", default-features = false, features = ["serde"] } crossterm = { version = "0.29", default-features = false, features = ["events"] } futures = { version = "0.3.30", default-features = false } icy_sixel = { version = "0.1.2", default-features = false } diff --git a/src/error.rs b/src/error.rs index 2d47ebe..641a1de 100644 --- a/src/error.rs +++ b/src/error.rs @@ -176,6 +176,13 @@ pub enum PublicationDemographicError { InvalidValue, } +#[derive(Error, Debug)] +pub enum ParseLangErr { + #[error("found invalid lang")] + Invalid, +} + + #[derive(Debug, Error, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ApiError { diff --git a/src/language.rs b/src/language.rs index 73e5cf5..27212f1 100644 --- a/src/language.rs +++ b/src/language.rs @@ -2,6 +2,7 @@ use serde::{Serialize, Serializer}; use std::fmt; +use crate::error::ParseLangErr; #[derive(Debug, Serialize)] pub enum Language { @@ -62,13 +63,19 @@ impl ExtraLang { } } -impl Language { - pub fn from(s: &str) -> Self { +use std::str::FromStr; + +impl FromStr for Language { + type Err = ParseLangErr; + + fn from_str(s: &str) -> Result { if let Some(extra) = ExtraLang::from(s) { - Language::Extra(extra) - } else { - Language::Normal(isolang::Language::from_639_1(s).unwrap()) + return Ok(Language::Extra(extra)); } + if let Some(lang) = isolang::Language::from_639_1(s) { + return Ok(Language::Normal(lang)); + } + Err(Self::Err::Invalid) } } diff --git a/src/main.rs b/src/main.rs index e547bae..7a0f05c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,7 @@ // TODO: Only use a single struct for deserializing use client::{MangaClient, MangaClientBuilder, Search, SearchOptionsBuilder}; use language::Language; -use response_deserializer::{Chapter, DataType, Id, Manga}; +use types::{Chapter, DataType, Id, Manga}; use select::Entry; use std::{fs, future::Future, path::Path, pin::Pin, process}; use util::{ConfigSearch, CoverSize, SelectionType}; @@ -14,6 +14,7 @@ mod select; mod test; mod util; mod language; +mod types; const BASE: &str = "https://api.mangadex.org"; const ONLY_METADATA: bool = true; diff --git a/src/response_deserializer.rs b/src/response_deserializer.rs index 444dc05..ed517f8 100644 --- a/src/response_deserializer.rs +++ b/src/response_deserializer.rs @@ -1,311 +1,10 @@ // TODO: Remove this -#![allow(unused)] use crate::error::*; +use crate::types::*; use crate::language::Language; -use chrono::{DateTime, FixedOffset}; -use serde::{Deserialize, Deserializer, Serialize}; +use serde::{Deserialize, Deserializer}; use std::fmt; -#[derive(Debug, Clone, PartialEq, Deserialize)] -pub struct Id(pub String); - -#[derive(Deserialize, Debug, PartialEq)] -#[serde(rename_all = "lowercase")] -pub enum ResponseResult { - Ok, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Status { - Completed, - Ongoing, - Hiatus, - Cancelled, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum ContentRating { - Safe, - Suggestive, - Erotica, - Pornographic, -} - -#[derive(Debug, Serialize)] -pub enum PublicationDemographic { - Shounen, - Shoujo, - Seinen, - Josei, -} - -#[derive(Debug, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum State { - Published, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum Response { - Collection, - Entity, -} - -#[derive(Debug, PartialEq, Deserialize)] -#[serde(rename_all = "snake_case")] -pub enum DataType { - Manga, - Chapter, - CoverArt, - Author, - Artist, - ScanlationGroup, - Tag, - User, - CustomList, - Creator, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct ChapterImages { - pub result: ResponseResult, - pub base_url: String, - pub chapter: ChapterImageData, -} - -#[derive(Debug, Deserialize)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct ChapterImageData { - pub hash: String, - pub data: Vec, - pub data_saver: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct ChapterFeed { - pub result: ResponseResult, - pub response: Response, - pub data: Vec, - pub limit: u32, - pub offset: u32, - pub total: u32, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct Chapter { - pub id: Id, - #[serde(rename = "type")] - pub data_type: DataType, - pub attributes: ChapterAttributes, - pub relationships: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct ChapterAttributes { - #[serde(deserialize_with = "opt_string_to_opt_f32")] - pub volume: Option, - #[serde(deserialize_with = "opt_string_to_opt_f32")] - pub chapter: Option, - pub title: Option, - #[serde(deserialize_with = "string_to_language")] - pub translated_language: Language, - pub external_url: Option, - pub is_unavailable: bool, - #[serde(deserialize_with = "string_to_datetime")] - #[serde(rename = "publishAt")] - pub published_at: DateTime, - #[serde(deserialize_with = "string_to_datetime")] - pub readable_at: DateTime, - #[serde(deserialize_with = "string_to_datetime")] - pub created_at: DateTime, - #[serde(deserialize_with = "string_to_datetime")] - pub updated_at: DateTime, - pub pages: u32, - pub version: u32, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct SearchResult { - pub result: ResponseResult, - pub response: Response, - pub data: Vec, - pub limit: u32, - pub offset: u32, - pub total: u32, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct IdQueryResult { - pub result: ResponseResult, - pub response: Response, - pub data: Manga, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct Manga { - pub id: Id, - #[serde(rename = "type")] - pub data_type: DataType, - pub attributes: MangaAttributes, - pub relationships: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct MangaAttributes { - pub title: Titles, - pub alt_titles: Vec, - pub description: Description, - pub is_locked: bool, - pub links: Option, - pub official_links: Option>, - #[serde(deserialize_with = "string_to_language")] - pub original_language: Language, - #[serde(deserialize_with = "opt_string_to_opt_f32")] - pub last_volume: Option, - #[serde(deserialize_with = "opt_string_to_opt_f32")] - pub last_chapter: Option, - #[serde(deserialize_with = "opt_string_to_opt_demographic")] - pub publication_demographic: Option, - pub status: Status, - pub year: Option, - pub content_rating: ContentRating, - pub tags: Vec, - pub state: State, - pub chapter_numbers_reset_on_new_volume: bool, - #[serde(deserialize_with = "string_to_datetime")] - pub created_at: DateTime, - #[serde(deserialize_with = "string_to_datetime")] - pub updated_at: DateTime, - pub version: u32, - #[serde(deserialize_with = "vec_opt_string_to_vec_opt_language")] - pub available_translated_languages: Vec>, - #[serde(deserialize_with = "opt_string_to_opt_id")] - pub latest_uploaded_chapter: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(rename_all = "camelCase")] -#[serde(deny_unknown_fields)] -pub struct CoverAttributes { - pub description: String, - #[serde(deserialize_with = "opt_string_to_opt_f32")] - pub volume: Option, - pub file_name: Id, - #[serde(deserialize_with = "string_to_language")] - pub locale: Language, - #[serde(deserialize_with = "string_to_datetime")] - pub created_at: DateTime, - #[serde(deserialize_with = "string_to_datetime")] - pub updated_at: DateTime, - pub version: u32, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct Tag { - pub id: Id, - #[serde(rename = "type")] - pub data_type: DataType, - pub attributes: TagAttributes, - pub relationships: Vec, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct Relationship { - pub id: Id, - #[serde(rename = "type")] - pub data_type: DataType, - pub related: Option, - pub attributes: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct TagAttributes { - pub name: TagName, - pub description: Description, - pub group: String, - pub version: u32, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct TagName { - pub en: String, -} - -#[derive(Deserialize, Debug, Default)] -#[serde(deny_unknown_fields)] -pub struct Links { - al: Option, - ap: Option, - bw: Option, - kt: Option, - mu: Option, - amz: Option, - cdj: Option, - ebj: Option, - mal: Option, - engtl: Option, - raw: Option, - nu: Option, -} - -#[derive(Deserialize, Debug)] -// #[serde(deny_unknown_fields)] -// TODO: Fill -pub struct Description { - pub en: Option, - pub ru: Option, -} - -#[derive(Deserialize, Debug)] -// #[serde(deny_unknown_fields)] -// TODO: Fill -pub struct AltTitles { - en: Option, - ja: Option, - ru: Option, - de: Option, - lt: Option, - ar: Option, - zh: Option, - hi: Option, -} - -#[derive(Deserialize, Debug)] -#[serde(deny_unknown_fields)] -pub struct Titles { - pub en: String, -} - -impl TryFrom<&str> for State { - type Error = (); - - fn try_from(s: &str) -> Result { - Ok(match s { - "published" => Self::Published, - _ => return Err(()), - }) - } -} - impl TryFrom<&str> for ContentRating { type Error = (); @@ -447,29 +146,6 @@ pub fn deserialize_chapter_images(json: &str) -> ChapterImages { serde_json::from_str(json).unwrap() } -fn opt_string_to_opt_demographic<'de, D>( - deserializer: D, -) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let opt = Option::::deserialize(deserializer)?; - match opt { - Some(n) => Ok(Some( - n.as_str().try_into().map_err(serde::de::Error::custom)?, - )), - None => Ok(None), - } -} - -fn string_to_datetime<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - DateTime::parse_from_rfc3339(&s).map_err(serde::de::Error::custom) -} - fn opt_string_to_opt_f32<'de, D>(deserializer: D) -> Result, D::Error> where D: Deserializer<'de>, @@ -487,34 +163,65 @@ where } } -fn string_to_language<'de, D>(deserializer: D) -> Result -where - D: Deserializer<'de>, -{ - let s = String::deserialize(deserializer)?; - Ok(Language::from(s.as_str())) +use serde::de::{self, Visitor}; + +struct LanguageVisitor; + +impl<'de> Visitor<'de> for LanguageVisitor { + type Value = Language; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer or string for for converting to Language") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.parse().map_err(|_| { + E::custom(format!( + "invalid str for converting to Language, got `{value}`" + )) + }) + } } -fn vec_opt_string_to_vec_opt_language<'de, D>( - deserializer: D, -) -> Result>, D::Error> -where - D: Deserializer<'de>, -{ - Ok(Vec::>::deserialize(deserializer)? - .into_iter() - .map(|m| m.map(|v| Language::from(v.as_str()))) - .collect()) +impl<'de> Deserialize<'de> for Language { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(LanguageVisitor) + } } -fn opt_string_to_opt_id<'de, D>(deserializer: D) -> Result, D::Error> -where - D: Deserializer<'de>, -{ - let opt = Option::::deserialize(deserializer)?; - match opt { - Some(n) => Ok(Some(Id(n))), - None => Ok(None), +struct PublicationDemographicVisitor; + +impl<'de> Visitor<'de> for PublicationDemographicVisitor { + type Value = PublicationDemographic; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + formatter.write_str("an integer or string for for converting to PublicationDemographic") + } + + fn visit_str(self, value: &str) -> Result + where + E: de::Error, + { + value.try_into().map_err(|_| { + E::custom(format!( + "invalid str for converting to PublicationDemographic, got `{value}`" + )) + }) + } +} + +impl<'de> Deserialize<'de> for PublicationDemographic { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + deserializer.deserialize_any(PublicationDemographicVisitor) } } diff --git a/src/types.rs b/src/types.rs new file mode 100644 index 0000000..b9f5992 --- /dev/null +++ b/src/types.rs @@ -0,0 +1,303 @@ +#![allow(unused)] +use crate::language::Language; +use chrono::{DateTime, FixedOffset}; +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, PartialEq, Deserialize)] +pub struct Id(pub String); + +#[derive(Deserialize, Debug, PartialEq)] +#[serde(rename_all = "lowercase")] +pub enum ResponseResult { + Ok, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Status { + Completed, + Ongoing, + Hiatus, + Cancelled, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ContentRating { + Safe, + Suggestive, + Erotica, + Pornographic, +} + +#[derive(Debug, Serialize)] +pub enum PublicationDemographic { + Shounen, + Shoujo, + Seinen, + Josei, +} + +#[derive(Debug, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum State { + Published, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum Response { + Collection, + Entity, +} + +#[derive(Debug, PartialEq, Deserialize)] +#[serde(rename_all = "snake_case")] +pub enum DataType { + Manga, + Chapter, + CoverArt, + Author, + Artist, + ScanlationGroup, + Tag, + User, + CustomList, + Creator, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct ChapterImages { + pub result: ResponseResult, + pub base_url: String, + pub chapter: ChapterImageData, +} + +#[derive(Debug, Deserialize)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct ChapterImageData { + pub hash: String, + pub data: Vec, + pub data_saver: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct ChapterFeed { + pub result: ResponseResult, + pub response: Response, + pub data: Vec, + pub limit: u32, + pub offset: u32, + pub total: u32, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Chapter { + pub id: Id, + #[serde(rename = "type")] + pub data_type: DataType, + pub attributes: ChapterAttributes, + pub relationships: Vec, +} + +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct ChapterAttributes { + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub volume: Option, + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub chapter: Option, + pub title: Option, + pub translated_language: Language, + pub external_url: Option, + pub is_unavailable: bool, + #[serde(rename = "publishAt")] + pub published_at: DateTime, + pub readable_at: DateTime, + pub created_at: DateTime, + pub updated_at: DateTime, + pub pages: u32, + pub version: u32, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct SearchResult { + pub result: ResponseResult, + pub response: Response, + pub data: Vec, + pub limit: u32, + pub offset: u32, + pub total: u32, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct IdQueryResult { + pub result: ResponseResult, + pub response: Response, + pub data: Manga, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Manga { + pub id: Id, + #[serde(rename = "type")] + pub data_type: DataType, + pub attributes: MangaAttributes, + pub relationships: Vec, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct MangaAttributes { + pub title: Titles, + pub alt_titles: Vec, + pub description: Description, + pub is_locked: bool, + pub links: Option, + pub official_links: Option>, + pub original_language: Language, + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub last_volume: Option, + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub last_chapter: Option, + pub publication_demographic: Option, + pub status: Status, + pub year: Option, + pub content_rating: ContentRating, + pub tags: Vec, + pub state: State, + pub chapter_numbers_reset_on_new_volume: bool, + pub created_at: DateTime, + pub updated_at: DateTime, + pub version: u32, + pub available_translated_languages: Vec>, + pub latest_uploaded_chapter: Option, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(rename_all = "camelCase")] +#[serde(deny_unknown_fields)] +pub struct CoverAttributes { + pub description: String, + #[serde(deserialize_with = "opt_string_to_opt_f32")] + pub volume: Option, + pub file_name: Id, + pub locale: Language, + pub created_at: DateTime, + pub updated_at: DateTime, + pub version: u32, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Tag { + pub id: Id, + #[serde(rename = "type")] + pub data_type: DataType, + pub attributes: TagAttributes, + pub relationships: Vec, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Relationship { + pub id: Id, + #[serde(rename = "type")] + pub data_type: DataType, + pub related: Option, + pub attributes: Option, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TagAttributes { + pub name: TagName, + pub description: Description, + pub group: String, + pub version: u32, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct TagName { + pub en: String, +} + +#[allow(unused)] +#[derive(Deserialize, Debug, Default)] +#[serde(deny_unknown_fields)] +pub struct Links { + al: Option, + ap: Option, + bw: Option, + kt: Option, + mu: Option, + amz: Option, + cdj: Option, + ebj: Option, + mal: Option, + engtl: Option, + raw: Option, + nu: Option, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +// #[serde(deny_unknown_fields)] +// TODO: Fill +pub struct Description { + pub en: Option, + pub ru: Option, +} + +#[allow(unused)] +#[derive(Deserialize, Debug)] +// #[serde(deny_unknown_fields)] +// TODO: Fill +pub struct AltTitles { + en: Option, + ja: Option, + ru: Option, + de: Option, + lt: Option, + ar: Option, + zh: Option, + hi: Option, +} + +#[derive(Deserialize, Debug)] +#[serde(deny_unknown_fields)] +pub struct Titles { + pub en: String, +} + +impl TryFrom<&str> for State { + type Error = (); + + fn try_from(s: &str) -> Result { + Ok(match s { + "published" => Self::Published, + _ => return Err(()), + }) + } +} +