// TODO: Remove this #![allow(unused)] use chrono::{DateTime, FixedOffset}; use serde::Deserialize; use std::fmt::Display; #[derive(Debug, Clone)] pub struct Id(String); #[derive(Debug)] pub enum ResponseResult { Ok, } #[derive(Debug)] pub enum Language { Tagalog, SimplifiedChinese, Greek, Persian, TraditionalChinese, Ukranian, Romanian, Arabic, German, Vietnamese, French, Turkish, Korean, SpanishLatinAmerican, Hungarian, BrazilianPortugese, English, Japanese, JapaneseRomaji, Italian, Russian, Indonesian, Bulgarian, Hebrew, Spanish, Esperanto, Polish, } #[derive(Debug)] pub enum Status { Completed, Ongoing, Hiatus, Cancelled, } #[derive(Debug)] pub enum ContentRating { Safe, Suggestive, Erotica, Pornographic, } #[derive(Debug)] pub enum PublicationDemographic { Shounen, Shoujo, Seinen, Josei, } #[derive(Debug)] pub enum State { Published, } #[derive(Debug)] pub enum Response { Collection, } #[derive(Debug)] pub enum DataType { Manga, Chapter, CoverArt, Author, Artist, ScanlationGroup, Tag, User, CustomList, Creator, } #[derive(Debug)] pub struct SearchResult { pub result: ResponseResult, pub response: Response, pub data: Vec, } #[derive(Debug, Deserialize)] #[serde(rename_all = "camelCase")] struct ChapterImagesContent { result: String, base_url: String, chapter: ChapterImageDataContent, } #[derive(Debug, Deserialize)] struct ChapterImageDataContent { hash: String, data: Vec, } pub struct ChapterImageData { pub hash: String, pub data: Vec, } pub struct ChapterImages { pub result: ResponseResult, pub base_url: String, pub chapter: ChapterImageData, } pub struct ChapterFeed { pub result: ResponseResult, pub response: Response, pub data: Vec, pub limit: u32, pub offset: u32, pub total: u32, } #[derive(Deserialize, Debug)] struct ChapterFeedResponse { result: String, response: String, data: Vec, limit: u32, offset: u32, total: u32, } #[derive(Debug)] pub struct MangaAttributes { pub title: Titles, pub alt_titles: Vec, pub description: Description, pub is_locked: bool, pub links: Option, pub original_language: Language, pub last_volume: Option, 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, } #[derive(Deserialize, Debug)] struct ChapterContent { id: String, #[serde(rename = "type")] type_name: String, attributes: ChapterAttributesContent, relationships: Vec, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct ChapterAttributesContent { volume: Option, chapter: Option, title: String, translated_language: String, external_url: Option, publish_at: String, readable_at: String, created_at: String, updated_at: String, pages: u32, version: u32, } pub struct Chapter { pub id: Id, pub data_type: DataType, pub attributes: ChapterAttributes, pub relationships: Vec, } pub struct ChapterAttributes { pub volume: Option, pub chapter: Option, pub title: String, pub translated_language: Language, pub external_url: Option, pub published_at: DateTime, pub created_at: DateTime, pub updated_at: DateTime, pub pages: u32, pub version: u32, } #[derive(Debug)] pub struct Manga { pub id: Id, pub data_type: DataType, pub attributes: MangaAttributes, pub relationships: Vec, } #[derive(Deserialize, Debug)] struct SearchResponse { result: String, response: String, data: Vec, limit: u32, offset: u32, total: u32, } #[derive(Deserialize, Debug)] struct ContentData { id: String, #[serde(rename = "type")] type_name: String, attributes: ContentAttributes, relationships: Vec, } #[derive(Deserialize, Debug)] #[serde(rename_all = "camelCase")] struct ContentAttributes { title: Titles, alt_titles: Vec, description: Description, is_locked: bool, links: Option, original_language: String, last_volume: Option, last_chapter: Option, publication_demographic: Option, status: String, year: Option, content_rating: String, tags: Vec, state: String, chapter_numbers_reset_on_new_volume: bool, created_at: String, updated_at: String, version: u32, available_translated_languages: Vec>, latest_uploaded_chapter: Option, } #[derive(Deserialize, Debug, Clone)] #[serde(rename_all = "camelCase")] struct ContentCoverAttributes { description: String, volume: Option, file_name: String, locale: String, created_at: String, updated_at: String, version: u32, } #[derive(Debug)] pub struct CoverAttributes { pub description: String, pub volume: Option, pub file_name: Id, pub locale: Language, pub created_at: DateTime, pub updated_at: DateTime, pub version: u32, } #[derive(Deserialize, Debug, Clone)] pub struct ContentTag { id: String, #[serde(rename = "type")] type_name: String, attributes: TagAttributes, relationships: Vec, } #[derive(Debug)] pub struct Tag { pub id: Id, pub data_type: DataType, pub attributes: TagAttributes, pub relationships: Vec, } #[derive(Deserialize, Debug, Clone)] struct ContentRelationShip { id: String, #[serde(rename = "type")] type_name: String, related: Option, attributes: Option, } #[derive(Debug)] pub struct ChapterRelationShip { id: Id, data_type: DataType, } #[derive(Debug)] // TODO: Typo: Relationship pub struct RelationShip { pub id: Id, pub data_type: DataType, pub related: Option, pub attributes: Option, } #[derive(Deserialize, Debug, Clone)] pub struct TagAttributes { pub name: TagName, pub description: Description, pub group: String, pub version: u32, } #[derive(Deserialize, Debug, Clone)] pub struct TagName { pub en: String, } #[derive(Deserialize, Debug, Default, Clone)] pub struct Links { al: Option, ap: Option, bw: Option, kt: Option, mu: Option, amz: Option, cdj: Option, ebj: Option, mal: Option, engtl: Option, } #[derive(Deserialize, Debug, Clone)] pub struct Description { en: Option, ru: Option, } #[derive(Deserialize, Debug, Clone)] pub struct AltTitles { en: Option, ja: Option, ru: Option, } #[derive(Deserialize, Debug, Clone)] pub struct Titles { pub en: String, } #[derive(Debug)] enum ResponseConversionError { AttributeError(AttributeConversionError), Response(String), Result(String), ContentType(String), } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "published" => State::Published, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "safe" => ContentRating::Safe, "suggestive" => ContentRating::Suggestive, "erotica" => ContentRating::Erotica, "pornographic" => ContentRating::Pornographic, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "ja" => Language::Japanese, "ja-ro" => Language::JapaneseRomaji, "en" => Language::English, "ru" => Language::Russian, "pt-br" => Language::BrazilianPortugese, "tr" => Language::Turkish, "it" => Language::Italian, "es-la" => Language::SpanishLatinAmerican, "hu" => Language::Hungarian, "bg" => Language::Bulgarian, "id" => Language::Indonesian, "he" => Language::Hebrew, "es" => Language::Spanish, "eo" => Language::Esperanto, "pl" => Language::Polish, "ko" => Language::Korean, "fr" => Language::French, "vi" => Language::Vietnamese, "de" => Language::German, "ar" => Language::Arabic, "ro" => Language::Romanian, "uk" => Language::Ukranian, "zh-hk" => Language::TraditionalChinese, "fa" => Language::Persian, "el" => Language::Greek, "zh" => Language::SimplifiedChinese, "tl" => Language::Tagalog, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "shounen" => PublicationDemographic::Shounen, "josei" => PublicationDemographic::Josei, "shoujo" => PublicationDemographic::Shoujo, "seinen" => PublicationDemographic::Seinen, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "manga" => DataType::Manga, "chapter" => DataType::Chapter, "cover_art" => DataType::CoverArt, "author" => DataType::Author, "artist" => DataType::Artist, "scanlation_group" => DataType::ScanlationGroup, "tag" => DataType::Tag, "user" => DataType::User, "custom_list" => DataType::CustomList, "creator" => DataType::Creator, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { Ok(match self { "ongoing" => Status::Ongoing, "completed" => Status::Completed, "hiatus" => Status::Hiatus, "cancelled" => Status::Cancelled, _ => return Err(()), }) } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { match self { "ok" => Ok(ResponseResult::Ok), _ => Err(()), } } } impl TryInto for &str { type Error = (); fn try_into(self) -> Result { match self { "collection" => Ok(Response::Collection), _ => Err(()), } } } fn convert_response_to_result( search_response: SearchResponse, ) -> Result { let response = (search_response.response.as_str()) .try_into() .map_err(|_| ResponseConversionError::Result(search_response.response.clone()))?; let result: ResponseResult = (search_response.result.as_str()) .try_into() .map_err(|_| ResponseConversionError::Result({ search_response.result.clone() }))?; let data: Vec = search_response .data .iter() .map(|m| { Ok(Manga { id: Id(m.id.clone()), data_type: (m.type_name.as_str()).try_into().map_err(|_| { ResponseConversionError::AttributeError(AttributeConversionError::DataType( m.type_name.clone(), )) })?, attributes: convert_attributes(&m.attributes) .map_err(ResponseConversionError::AttributeError)?, relationships: m .relationships .iter() .map(|m| { Ok(RelationShip { id: Id(m.id.clone()), data_type: (m.type_name.as_str()).try_into().map_err(|_| { AttributeConversionError::DataType(m.type_name.clone()) })?, attributes: { if let Some(attributes) = &m.attributes { Some(CoverAttributes { created_at: DateTime::parse_from_rfc3339( &attributes.created_at, ) .map_err(|_| { AttributeConversionError::CreatedAtDateTime( attributes.created_at.clone(), ) })?, updated_at: DateTime::parse_from_rfc3339( &attributes.created_at, ) .map_err(|_| { AttributeConversionError::CreatedAtDateTime( attributes.created_at.clone(), ) })?, // TODO: Something should probably be done here description: String::new(), file_name: Id(attributes.file_name.clone()), locale: (attributes.locale.as_str()).try_into().map_err( |_| { AttributeConversionError::Locale( attributes.locale.clone(), ) }, )?, version: attributes.version, volume: match &attributes.volume { Some(v) => v.parse().ok(), None => None, }, }) } else { None } }, related: m.related.clone(), }) }) .collect::, AttributeConversionError>>() .map_err(ResponseConversionError::AttributeError)?, }) }) .collect::, ResponseConversionError>>()?; Ok(SearchResult { response, result, data, }) } #[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 { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { self.0.fmt(f) } } impl Display for Status { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Ongoing => "ongoing".fmt(f), Self::Completed => "completed".fmt(f), Self::Cancelled => "cancelled".fmt(f), Self::Hiatus => "hiatus".fmt(f), } } } impl Display for ContentRating { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { Self::Safe => "safe".fmt(f), Self::Suggestive => "suggestive".fmt(f), Self::Erotica => "erotica".fmt(f), Self::Pornographic => "pornographic".fmt(f), } } } fn convert_attributes( attributes: &ContentAttributes, ) -> Result { Ok(MangaAttributes { title: attributes.title.clone(), alt_titles: attributes.alt_titles.clone(), description: attributes.description.clone(), is_locked: attributes.is_locked, links: attributes.links.clone(), original_language: (attributes.original_language.as_str()) .try_into() .map_err(|_| { AttributeConversionError::Language(attributes.original_language.clone()) })?, last_volume: match attributes.last_volume.clone() { Some(s) => match s.parse() { Ok(v) => Some(v), Err(_) => { if s.is_empty() { None } else { return Err(AttributeConversionError::LastVolume(s)); } } }, None => None, }, last_chapter: match attributes.last_chapter.clone() { Some(n) => match n.parse() { Ok(v) => Some(v), Err(_) => { if n.is_empty() { None } else { return Err(AttributeConversionError::LastVolume(n)); } } }, None => None, }, publication_demographic: match attributes.publication_demographic.clone() { Some(s) => Some( (s.as_str()) .try_into() .map_err(|_| AttributeConversionError::PublicationDemographic(s.clone()))?, ), None => None, }, status: (attributes.status.as_str()) .try_into() .map_err(|_| AttributeConversionError::Status(attributes.status.clone()))?, year: attributes.year, content_rating: attributes.content_rating.as_str().try_into().map_err(|_| { AttributeConversionError::ContentRating(attributes.content_rating.clone()) })?, tags: attributes .tags .clone() .iter() .map(|m| { Ok(Tag { data_type: (m.type_name.as_str()) .try_into() .map_err(|_| AttributeConversionError::DataType(m.type_name.clone()))?, id: Id(m.id.clone()), relationships: m .relationships .iter() .map(|m| { Ok(RelationShip { id: Id(m.id.clone()), data_type: (m.type_name.as_str()).try_into().map_err(|_| { AttributeConversionError::DataType(m.type_name.clone()) })?, // TODO: Do this attributes: None, related: m.related.clone(), }) }) .collect::, AttributeConversionError>>()?, attributes: TagAttributes { name: m.attributes.name.clone(), group: m.attributes.group.clone(), version: m.attributes.version, description: Description { en: m.attributes.description.en.clone(), ru: m.attributes.description.ru.clone(), }, }, }) }) .collect::, AttributeConversionError>>()?, state: (attributes.state.as_str()) .try_into() .map_err(|_| AttributeConversionError::State(attributes.state.clone()))?, chapter_numbers_reset_on_new_volume: attributes.chapter_numbers_reset_on_new_volume, created_at: DateTime::parse_from_rfc3339(&attributes.created_at).map_err(|_| { AttributeConversionError::CreatedAtDateTime(attributes.created_at.clone()) })?, updated_at: DateTime::parse_from_rfc3339(&attributes.updated_at).map_err(|_| { AttributeConversionError::UpdatedAtDateTime(attributes.created_at.clone()) })?, version: attributes.version, available_translated_languages: attributes .available_translated_languages .iter() .map(|m| { Ok(match m { Some(s) => Some( (s.as_str()) .try_into() .map_err(|_| AttributeConversionError::Language(s.clone()))?, ), None => None, }) }) .collect::>, AttributeConversionError>>()?, latest_uploaded_chapter: attributes .latest_uploaded_chapter .as_ref() .map(|m| Id(m.clone())), }) } pub fn deserialize_chapter_feed(json: &str) -> ChapterFeed { let chapter_feed_response: ChapterFeedResponse = match serde_json::from_str(json) { Ok(v) => v, Err(e) => { std::fs::write("out.json", json).unwrap(); eprintln!("ERROR: {:#?}", e); std::process::exit(1); } }; convert_chapter_feed(chapter_feed_response).unwrap() } pub fn deserializer(json: &str) -> SearchResult { let search_response: SearchResponse = match serde_json::from_str(json) { Ok(v) => v, Err(e) => { std::fs::write("out.json", json).unwrap(); eprintln!("ERROR: {:#?}", e); std::process::exit(1); } }; let search_result = convert_response_to_result(search_response); 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), } fn convert_chapter_feed( feed: ChapterFeedResponse, ) -> Result { Ok(ChapterFeed { result: (feed.result.as_str()) .try_into() .map_err(|_| ChapterFeedConversionError::Result(feed.result.clone()))?, response: (feed.response.as_str()) .try_into() .map_err(|_| ChapterFeedConversionError::Result(feed.response.clone()))?, data: feed .data .iter() .map(|m| { Ok(Chapter { data_type: (m.type_name.as_str()) .try_into() .map_err(|_| ChapterConversionError::DataType(m.type_name.clone()))?, id: Id(m.id.clone()), attributes: convert_chapter_attributes(&m.attributes) .map_err(ChapterConversionError::Attributes)?, relationships: m .relationships .iter() .map(|m| { Ok(ChapterRelationShip { data_type: (m.type_name.as_str()).try_into().map_err(|_| { ChapterRelationShipError::TypeData(m.type_name.clone()) })?, id: Id(m.id.clone()), }) }) .collect::, ChapterRelationShipError>>() .map_err(ChapterConversionError::RelationShip)?, }) }) .collect::, ChapterConversionError>>() .map_err(ChapterFeedConversionError::Chapter)?, limit: feed.limit, offset: feed.offset, total: feed.total, }) } #[derive(Debug)] enum ChapterAttributeConversionError { Volume(String), Chapter(String), CreatedAt(String), UpdatedAt(String), PublishedAt(String), TranslatedLanguage(String), } fn convert_chapter_attributes( attributes: &ChapterAttributesContent, ) -> Result { Ok(ChapterAttributes { volume: match &attributes.volume { Some(v) => match v.parse() { Ok(n) => Some(n), Err(_) => return Err(ChapterAttributeConversionError::Volume(v.to_owned())), }, None => None, }, chapter: match &attributes.chapter { Some(v) => match v.parse() { Ok(v) => Some(v), Err(_) => return Err(ChapterAttributeConversionError::Chapter(v.to_owned())), }, None => None, }, created_at: DateTime::parse_from_rfc3339(&attributes.created_at).map_err(|_| { ChapterAttributeConversionError::CreatedAt(attributes.created_at.clone()) })?, published_at: DateTime::parse_from_rfc3339(&attributes.publish_at).map_err(|_| { ChapterAttributeConversionError::CreatedAt(attributes.publish_at.clone()) })?, updated_at: DateTime::parse_from_rfc3339(&attributes.updated_at).map_err(|_| { ChapterAttributeConversionError::CreatedAt(attributes.updated_at.clone()) })?, external_url: attributes.external_url.clone(), title: attributes.title.clone(), pages: attributes.pages, translated_language: (attributes.translated_language.as_str()) .try_into() .map_err(|_| { ChapterAttributeConversionError::TranslatedLanguage( attributes.translated_language.clone(), ) })?, version: attributes.version, }) } #[derive(Debug)] enum ChapterImageError { Result(String), } fn convert_chapter_images(data: ChapterImagesContent) -> Result { Ok(ChapterImages { result: (data.result.as_str()) .try_into() .map_err(|_| ChapterImageError::Result(data.result.clone()))?, base_url: data.base_url, chapter: ChapterImageData { hash: data.chapter.hash, data: data.chapter.data, }, }) } pub fn deserialize_chapter_images(json: &str) -> ChapterImages { let chapter_images: ChapterImagesContent = match serde_json::from_str(json) { Ok(v) => v, Err(e) => { std::fs::write("out.json", json).unwrap(); eprintln!("ERROR: {:#?}", e); std::process::exit(1); } }; convert_chapter_images(chapter_images).unwrap() }