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
use std::error::Error;

use reqwest::Url;
use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};

use author_search::{AuthorSearchParams, AuthorSearchResult};
use floor_list::FloorListParams;
use floor_list::FloorListResult;

use crate::ApiParams::{AuthorSearch, FloorList};

mod author_search;
mod floor_list;

const ENDPOINT_BASE: &str = "https://api.dmm.com/affiliate/v3/";

#[derive(Serialize, Debug)]
#[serde(untagged)]
enum ApiParams {
    FloorList(FloorListParams),
    AuthorSearch(AuthorSearchParams),
}

#[derive(Deserialize, Debug)]
pub struct ApiResponse<T: ApiResult> {
    pub result: T,
}

pub trait ApiResult {}

pub struct Dmm {
    api_id: String,
    affiliate_id: String,
}

impl Dmm {
    pub fn new(dmm_api_id: &str, dmm_affiliate_id: &str) -> Self {
        Dmm {
            api_id: dmm_api_id.to_string(),
            affiliate_id: dmm_affiliate_id.to_string(),
        }
    }

    async fn call<T>(&self, api: &str, params: ApiParams) -> Result<ApiResponse<T>, Box<dyn Error>>
    where
        T: ApiResult + DeserializeOwned,
    {
        let auth = querystring::stringify(vec![
            ("api_id", &self.api_id),
            ("affiliate_id", &self.affiliate_id),
        ]);
        let qs = serde_qs::to_string(&params)?;
        let mut url = Url::parse(ENDPOINT_BASE)?.join(api)?;
        url.set_query(Some(&(auth + &qs)));
        let res = reqwest::get(url).await?;
        let text = res.text().await?;
        let res = serde_json::from_str(&text)?;
        Ok(res)
    }

    pub async fn floor_list(
        &self,
        params: FloorListParams,
    ) -> Result<ApiResponse<FloorListResult>, Box<dyn Error>> {
        self.call("FloorList", FloorList(params)).await
    }

    pub async fn author_search(
        &self,
        params: AuthorSearchParams,
    ) -> Result<ApiResponse<AuthorSearchResult>, Box<dyn Error>> {
        self.call("AuthorSearch", AuthorSearch(params)).await
    }
}

#[cfg(test)]
mod tests {
    use std::env;

    use super::*;

    #[tokio::test]
    async fn test_floor_list() {
        let api_id = env::var("DMM_API_ID").unwrap();
        let affiliate_id = env::var("DMM_AFFILIATE_ID").unwrap();
        let dmm = Dmm::new(&api_id, &affiliate_id);
        let r = dmm.floor_list(FloorListParams {}).await.unwrap();

        assert_eq!(r.result.site.get(0).unwrap().name, "DMM.com(一般)");
    }

    #[tokio::test]
    async fn test_author_search() {
        let api_id = env::var("DMM_API_ID").unwrap();
        let affiliate_id = env::var("DMM_AFFILIATE_ID").unwrap();
        let dmm = Dmm::new(&api_id, &affiliate_id);
        let r = dmm
            .author_search(AuthorSearchParams {
                floor_id: "27".to_string(),
                initial: Some('と'),
                offset: Some(100),
                ..AuthorSearchParams::default()
            })
            .await
            .unwrap();
        assert_eq!(r.result.status, "200");
    }
}