dynasty 1.4.1

Dynasty Reader's CLI downloader
Documentation
use std::{fmt::Display, str::FromStr};

use anyhow::Result;
use dynasty_api::directory::DirectoryKind;
use lazy_regex::regex;

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ParsedArgumentValue {
    Chapter(String),
    #[cfg(feature = "search")]
    Search(SearchValue),
    Tag(TagValue),
}

#[cfg(feature = "search")]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SearchValue {
    pub query: String,
    pub sort: Option<dynasty_api::search::SearchSort>,
    pub categories: Vec<dynasty_api::search::SearchCategory>,
}

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

    fn try_from(value: crate::search::SearchCommand) -> Result<Self, Self::Error> {
        let query = if let Some(query) = value.query {
            query
        } else {
            dialoguer::Input::with_theme(&dialoguer::theme::ColorfulTheme::default())
                .with_prompt("query")
                .report(false)
                .interact_text()?
        };
        let sort = value.sort.map(|i| i.0);
        let categories = value.categories.iter().map(|i| i.0).collect();

        Ok(SearchValue {
            query,
            sort,
            categories,
        })
    }
}

#[derive(Debug, Clone, PartialEq, Eq)]
pub struct TagValue {
    pub kind: DirectoryKind,
    pub permalink: String,
}

impl Display for TagValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        write!(f, "{}/{}", self.kind, self.permalink)
    }
}

impl Display for ParsedArgumentValue {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match &self {
            ParsedArgumentValue::Chapter(value) => value.fmt(f),
            ParsedArgumentValue::Tag(value) => value.fmt(f),
            #[cfg(feature = "search")]
            ParsedArgumentValue::Search(value) => value.query.fmt(f),
        }
    }
}

impl FromStr for ParsedArgumentValue {
    type Err = anyhow::Error;

    fn from_str(s: &str) -> Result<Self, Self::Err> {
        let re = regex!(
            r"(?x)
        ^
        (?:https?://dynasty-scans.com)
        /?
        (?P<directory>\w+)
        /?
        (?P<permalink>\w+)?
        /?
        $"
        );

        if let Some(captures) = re.captures(s) {
            let directory = captures.name("directory").unwrap().as_str();

            if let Some(permalink_match) = captures.name("permalink") {
                if directory == "chapters" {
                    return Ok(ParsedArgumentValue::Chapter(
                        permalink_match.as_str().to_string(),
                    ));
                } else if let Ok(kind) = directory.parse::<DirectoryKind>() {
                    return Ok(ParsedArgumentValue::Tag(TagValue {
                        kind,
                        permalink: permalink_match.as_str().to_string(),
                    }));
                }
            }
        }

        Err(anyhow::anyhow!("not a valid Dynasty Reader URL"))
    }
}

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

    #[test]
    fn should_parse_dynasty_api_argument() -> Result<()> {
        let predicates = [
            (
                "https://dynasty-scans.com/series/liar_satsuki_can_see_death",
                ParsedArgumentValue::Tag(TagValue {
                    kind: DirectoryKind::Series,
                    permalink: "liar_satsuki_can_see_death".to_string(),
                }),
            ),
            (
                "https://dynasty-scans.com/chapters/kiss_distance",
                ParsedArgumentValue::Chapter("kiss_distance".to_string()),
            ),
        ];

        for (left, right) in predicates {
            assert_eq!(left.parse::<ParsedArgumentValue>()?, right);
        }

        Ok(())
    }

    #[test]
    fn should_fail_parse_dynasty_api_argument() -> Result<()> {
        let predicates = [
            "https://google.com/a/b",         // wrong website host
            "anytextorwhateverthisisn",       // <-
            "https://dynasty-scans.com/tags", // incomplete path
        ];

        for predicate in predicates {
            assert!(predicate.parse::<ParsedArgumentValue>().is_err())
        }

        Ok(())
    }
}