1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
use anyhow::Result;

use crate::{latest::LatestChapter, tag::ChapterTag, utils::permalink_from_title};

/// This trait is sealed and cannot be implemented for types outside this crate.
pub trait IntoDynastyChapterPath: IntoDynastyChapterPathSealed {}

impl IntoDynastyChapterPath for &ChapterTag {}
impl IntoDynastyChapterPath for ChapterTag {}
impl IntoDynastyChapterPath for &LatestChapter {}
impl IntoDynastyChapterPath for LatestChapter {}
impl IntoDynastyChapterPath for &str {}
impl IntoDynastyChapterPath for String {}
impl IntoDynastyChapterPath for &String {}

pub trait IntoDynastyChapterPathSealed {
    fn dynasty_path(&self) -> Result<String>;
}

impl IntoDynastyChapterPathSealed for &ChapterTag {
    fn dynasty_path(&self) -> Result<String> {
        Ok(self.path())
    }
}

impl IntoDynastyChapterPathSealed for ChapterTag {
    fn dynasty_path(&self) -> Result<String> {
        Ok(self.path())
    }
}

impl IntoDynastyChapterPathSealed for &LatestChapter {
    fn dynasty_path(&self) -> Result<String> {
        Ok(self.path())
    }
}

impl IntoDynastyChapterPathSealed for LatestChapter {
    fn dynasty_path(&self) -> Result<String> {
        Ok(self.path())
    }
}

impl IntoDynastyChapterPathSealed for &str {
    fn dynasty_path(&self) -> Result<String> {
        let stripped = self.trim().trim_matches('/').to_lowercase();
        let slash_count = stripped.matches('/').count();

        anyhow::ensure!(!stripped.is_empty(), "chapter path must not be empty!");

        let (prefix, s) = if slash_count == 1 {
            anyhow::ensure!(
                stripped.starts_with("chapters/"),
                "chapter path must also starts with `chapters/` if it contains 1 `/`! {}",
                self
            );

            self.split_once('/').unwrap()
        } else if slash_count == 0 {
            ("chapters", stripped.as_str())
        } else {
            return Err(anyhow::anyhow!(
                "chapter path must contains either 1 or 0 `/`! {}",
                self
            ));
        };

        let permalink = permalink_from_title(s);

        Ok(format!("{}/{}", prefix, permalink))
    }
}

impl IntoDynastyChapterPathSealed for String {
    fn dynasty_path(&self) -> Result<String> {
        self.as_str().dynasty_path()
    }
}

impl IntoDynastyChapterPathSealed for &String {
    fn dynasty_path(&self) -> Result<String> {
        self.as_str().dynasty_path()
    }
}

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

    #[test]
    fn chapter_path_from_str() {
        let sample = [
            ["kichiku_yousei", "chapters/kichiku_yousei"],
            ["chapters/a_channel_ch21", "chapters/a_channel_ch21"],
        ];

        for [a, b] in sample {
            assert_eq!(a.dynasty_path().unwrap(), b)
        }
    }

    #[test]
    fn chapter_path_from_invalid_str() {
        let sample = [
            ["", "chapter path must not be empty!"],
            [
                "invalid/format/here",
                "chapter path must contains either 1 or 0 `/`! invalid/format/here",
            ],
            [
                "this/is_invalid",
                "chapter path must also starts with `chapters/` if it contains 1 `/`! this/is_invalid",
            ],
        ];

        for [a, err] in sample {
            assert_eq!(a.dynasty_path().unwrap_err().to_string(), err)
        }
    }
}