dynasty-api 1.1.0

Dynasty Reader's wrappers
Documentation
use serde::{Deserialize, Serialize};

use crate::{
    directory_list::DirectoryListChapterItem, recent_chapter::RecentChapterItem, tag::TagItem,
    DynastyReaderRoute, DYNASTY_READER_BASE,
};

use self::utils::chapter_name_to_permalink;

/// A configuration to get a [Chapter]
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ChapterConfig {
    pub name: String,
}

impl From<String> for ChapterConfig {
    fn from(s: String) -> Self {
        ChapterConfig { name: s }
    }
}

impl From<DirectoryListChapterItem> for ChapterConfig {
    fn from(item: DirectoryListChapterItem) -> Self {
        ChapterConfig { name: item.title }
    }
}

impl From<RecentChapterItem> for ChapterConfig {
    fn from(item: RecentChapterItem) -> Self {
        ChapterConfig { name: item.title }
    }
}

#[cfg(feature = "search")]
impl TryFrom<crate::search::SearchItem> for ChapterConfig {
    type Error = anyhow::Error;

    fn try_from(value: crate::search::SearchItem) -> Result<Self, Self::Error> {
        if matches!(value.kind, crate::search::SearchCategory::Chapter) {
            Ok(ChapterConfig { name: value.title })
        } else {
            Err(anyhow::anyhow!("this search item is not a chapter"))
        }
    }
}

impl DynastyReaderRoute for ChapterConfig {
    fn request_builder(
        &self,
        client: &reqwest::Client,
        url: reqwest::Url,
    ) -> reqwest::RequestBuilder {
        client.get(url)
    }

    fn request_url(&self) -> reqwest::Url {
        let permalink = chapter_name_to_permalink(&self.name);

        DYNASTY_READER_BASE
            .join(&format!("chapters/{}.json", permalink))
            .unwrap()
    }
}

/// A wrapper around Dynasty Reader's chapter
///
/// # Example urls
///
/// - <https://dynasty-scans.com/chapters/momoiro_trance_ch01>
/// - <https://dynasty-scans.com/chapters/liar_satsuki_can_see_death_ch54>
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Chapter {
    pub title: String,
    pub long_title: String,
    pub permalink: String,
    pub released_on: String,
    pub added_on: String,
    pub tags: Vec<TagItem>,
    pub pages: Vec<ChapterPage>,
}

/// A Dynasty Reader's chapter page
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct ChapterPage {
    pub name: String,
    #[serde(deserialize_with = "crate::utils::join_path_with_dynasty_reader_base")]
    pub url: String,
}

mod utils {
    use lazy_regex::regex;

    use crate::utils::name_to_permalink;

    pub(super) fn chapter_name_to_permalink(chapter_name: &str) -> String {
        let chapter_re = regex!(r"(ch[\d.]+):");

        let name = chapter_re
            .find(chapter_name)
            .map(|matches| &chapter_name[..matches.end()])
            .unwrap_or(chapter_name);

        name_to_permalink(name)
    }

    #[cfg(test)]
    mod tests {
        use super::*;

        #[test]
        fn should_convert_chapter_name_to_permalink() {
            let predicates = [
                ("\"Toda-san,\"", "toda_san"),
                ("B.G.M.R.S.P.", "b_g_m_r_s_p"),
                ("Assorted NicoMaki (03015)", "assorted_nicomaki_03015"),
                (
                    "Adachi and Shimamura (Moke ver.) ch27.1: Leaving Azure",
                    "adachi_and_shimamura_moke_ver_ch27_1",
                ),
                (
                    "Love Live! Comic Anthology μ’s Precious Days ch01",
                    "love_live_comic_anthology_μs_precious_days_ch01",
                ),
                (
                    "a_story_about_doing_xx_to_girls_from_different_species_ch51",
                    "a_story_about_doing_xx_to_girls_from_different_species_ch51",
                ),
            ];

            for (left, right) in predicates {
                assert_eq!(chapter_name_to_permalink(left), right)
            }
        }
    }
}

#[cfg(test)]
mod tests {
    use anyhow::Result;

    use crate::test_utils::tryhard_configs;

    use super::*;

    fn create_config(c: &str) -> ChapterConfig {
        c.to_string().into()
    }

    #[tokio::test]
    #[ignore = "requires internet"]
    async fn response_structure() -> Result<()> {
        let configs = [
            "4-Koma Starlight ch01",
            "4-Koma Starlight ch01: Act 1: Nice To Meet You!",
            "4_koma_starlight_ch01",
            "Just ChisaTaki Kissing",
            "just_chisataki_kissing",
        ]
        .map(create_config);

        tryhard_configs(configs, |client, config| client.chapter(config)).await?;

        Ok(())
    }
}