dynasty-api 1.1.0

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

use crate::{DynastyReaderRoute, DYNASTY_READER_BASE};

/// A configuration to get a [Directory]
#[allow(missing_docs)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct DirectoryConfig {
    pub kind: DirectoryKind,
    pub page_number: u64,
}

impl DynastyReaderRoute for DirectoryConfig {
    fn request_builder(
        &self,
        client: &reqwest::Client,
        url: reqwest::Url,
    ) -> reqwest::RequestBuilder {
        client.get(url).query(&[("page", self.page_number)])
    }

    fn request_url(&self) -> reqwest::Url {
        DYNASTY_READER_BASE
            .join(&format!("{}.json", self.kind))
            .unwrap()
    }
}

/// A Dynasty Reader's directory types
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
pub enum DirectoryKind {
    /// <https://dynasty-scans.com/anthologies>
    Anthology,

    /// <https://dynasty-scans.com/doujins>
    Doujin,

    /// <https://dynasty-scans.com/issues>
    Issue,

    /// <https://dynasty-scans.com/series>
    Series,

    /// <https://dynasty-scans.com/authors>
    Author,

    /// <https://dynasty-scans.com/scanlators>
    Scanlator,

    /// <https://dynasty-scans.com/tags>
    #[serde(alias = "General")]
    Tag,

    /// <https://dynasty-scans.com/pairings>
    Pairing,
}

impl std::fmt::Display for DirectoryKind {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let s = {
            use DirectoryKind::*;

            match &self {
                Anthology => "anthologies",
                Doujin => "doujins",
                Issue => "issues",
                Series => "series",
                Author => "authors",
                Scanlator => "scanlators",
                Tag => "tags",
                Pairing => "pairings",
            }
        };

        write!(f, "{s}")
    }
}

impl std::str::FromStr for DirectoryKind {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        use DirectoryKind::*;

        match s {
            "anthologies" => Ok(Anthology),
            "authors" => Ok(Author),
            "doujins" => Ok(Doujin),
            "tags" => Ok(Tag),
            "issues" => Ok(Issue),
            "pairings" => Ok(Pairing),
            "scanlators" => Ok(Scanlator),
            "series" => Ok(Series),
            _ => Err(anyhow::anyhow!("`{}` is not a valid directory", s)),
        }
    }
}

#[derive(Debug, Clone, Serialize)]
pub(crate) struct UntaggedDirectory {
    items: Vec<UntaggedDirectoryItem>,
    page_number: u64,
    max_page_number: u64,
}

impl<'de> Deserialize<'de> for UntaggedDirectory {
    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        use std::collections::HashMap;

        #[derive(Deserialize)]
        struct UntaggedDirectoryWrapper {
            tags: Vec<HashMap<String, Vec<UntaggedDirectoryItem>>>,
            current_page: u64,
            total_pages: u64,
        }

        let wrapper: UntaggedDirectoryWrapper = Deserialize::deserialize(deserializer)?;

        let UntaggedDirectoryWrapper {
            tags: items,
            current_page: page_number,
            total_pages: max_page_number,
        } = wrapper;

        let items = items
            .into_iter()
            .flatten()
            .flat_map(|(_, items)| items)
            .collect();

        Ok(UntaggedDirectory {
            items,
            page_number,
            max_page_number,
        })
    }
}

/// A wrapper around Dynasty Reader's directory
///
/// # Example urls
///
/// - <https://dynasty-scans.com/tags>
/// - <https://dynasty-scans.com/doujins>
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct Directory {
    pub items: Vec<DirectoryItem>,
    pub page_number: u64,
    pub max_page_number: u64,
}

impl UntaggedDirectory {
    pub(crate) fn into_tagged(self, kind: DirectoryKind) -> Directory {
        let items = self
            .items
            .into_iter()
            .map(|item| item.into_tagged(kind))
            .collect();

        Directory {
            items,
            page_number: self.page_number,
            max_page_number: self.max_page_number,
        }
    }
}

#[derive(Debug, Clone, Deserialize, Serialize)]
pub(crate) struct UntaggedDirectoryItem {
    name: String,
    permalink: String,
}

/// A Dynasty Reader's [Directory] item
#[allow(missing_docs)]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
pub struct DirectoryItem {
    pub name: String,
    pub permalink: String,
    pub kind: DirectoryKind,
}

impl UntaggedDirectoryItem {
    pub(crate) fn into_tagged(self, kind: DirectoryKind) -> DirectoryItem {
        DirectoryItem {
            kind,
            name: self.name,
            permalink: self.permalink,
        }
    }
}

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

    use crate::test_utils::tryhard_configs;

    use super::*;

    fn create_config(kind: DirectoryKind) -> DirectoryConfig {
        DirectoryConfig {
            kind,
            page_number: 1,
        }
    }

    #[tokio::test]
    #[ignore = "requires internet"]
    async fn response_structure() -> Result<()> {
        let configs = {
            use DirectoryKind::*;

            [
                Anthology, Doujin, Issue, Series, Author, Scanlator, Tag, Pairing,
            ]
            .map(create_config)
        };

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

        Ok(())
    }
}