dmm-api 0.5.2

DMM Web API SDK
Documentation
use std::error::Error;
use std::ops::Deref;

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

use crate::actress_search::{ActressSearchParams, ActressSearchResult};
use crate::author_search::{AuthorSearchParams, AuthorSearchResult};
use crate::dmm::ApiParams::{
    ActressSearch, AuthorSearch, FloorList, GenreSearch, ItemList, MakerSearch, SeriesSearch,
};
use crate::floor_list::{FloorListParams, FloorListResult};
use crate::genre_search::{GenreSearchParams, GenreSearchResult};
use crate::item_list::{ItemListParams, ItemListResult};
use crate::maker_search::{MakerSearchParams, MakerSearchResult};
use crate::series_search::{SeriesSearchParams, SeriesSearchResult};

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

#[derive(Serialize, Debug)]
#[serde(untagged)]
#[allow(clippy::large_enum_variant)]
enum ApiParams {
    ItemList(ItemListParams),
    FloorList(FloorListParams),
    ActressSearch(ActressSearchParams),
    GenreSearch(GenreSearchParams),
    MakerSearch(MakerSearchParams),
    SeriesSearch(SeriesSearchParams),
    AuthorSearch(AuthorSearchParams),
}

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

pub(crate) trait ApiResult {}

#[derive(Deserialize, Debug)]
pub struct ElementVec<T> {
    #[serde(rename = "item")]
    pub items: Vec<T>,
}

impl<T> Deref for ElementVec<T> {
    type Target = Vec<T>;

    fn deref(&self) -> &Self::Target {
        &self.items
    }
}
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<T, Box<dyn Error>>
    where
        T: ApiResult + DeserializeOwned,
    {
        let auth = querystring::stringify(vec![
            ("api_id", &self.api_id),
            ("affiliate_id", &self.affiliate_id),
            ("output", "xml"),
        ]);
        let qs = serde_qs::to_string(&params)?;
        // println!("{}", &qs);
        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?;
        // println!("{}", &text);
        let res: ApiResponse<T> = serde_xml_rs::from_str(&text)?;
        Ok(res.result)
    }

    pub async fn item_list(
        &self,
        params: ItemListParams,
    ) -> Result<ItemListResult, Box<dyn Error>> {
        self.call("ItemList", ItemList(params)).await
    }

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

    pub async fn actress_search(
        &self,
        params: ActressSearchParams,
    ) -> Result<ActressSearchResult, Box<dyn Error>> {
        self.call("ActressSearch", ActressSearch(params)).await
    }

    pub async fn genre_search(
        &self,
        params: GenreSearchParams,
    ) -> Result<GenreSearchResult, Box<dyn Error>> {
        self.call("GenreSearch", GenreSearch(params)).await
    }

    pub async fn maker_search(
        &self,
        params: MakerSearchParams,
    ) -> Result<MakerSearchResult, Box<dyn Error>> {
        self.call("MakerSearch", MakerSearch(params)).await
    }

    pub async fn series_search(
        &self,
        params: SeriesSearchParams,
    ) -> Result<SeriesSearchResult, Box<dyn Error>> {
        self.call("SeriesSearch", SeriesSearch(params)).await
    }

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

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

    use crate::actress_search::SortValue;
    use crate::item_list::SiteValue;

    use super::*;

    #[tokio::test]
    async fn test_item_list() {
        let dmm = client();
        let r = dmm
            .item_list(ItemListParams {
                site: SiteValue::Dmm,
                hits: Some(100),
                ..ItemListParams::default()
            })
            .await
            .unwrap();

        // for i in &r.items.items {
        //     println!("{:?}", i.date)
        // }
        assert_eq!(r.status, 200);
    }

    #[tokio::test]
    async fn test_floor_list() {
        let dmm = client();
        let r = dmm.floor_list(FloorListParams {}).await.unwrap();
        assert_eq!(
            r.site
                .get(0)
                .unwrap()
                .service
                .get(0)
                .unwrap()
                .floor
                .get(0)
                .unwrap()
                .name,
            "AKB48"
        );
    }
    #[tokio::test]
    async fn test_actress_search() {
        let dmm = client();
        let resp = dmm
            .actress_search(ActressSearchParams {
                keyword: Some("あさみ".to_string()),
                gte_bust: Some(90),
                lte_waist: Some(60),
                sort: Some(SortValue::BustDesc),
                offset: Some(1), // given that, returns string first_position
                ..ActressSearchParams::default()
            })
            .await
            .unwrap();
        assert_eq!(resp.first_position, 1);
        let a = resp.actress.unwrap();
        // a.iter().for_each(|e| println!("{:?}", &e.birthday));
        let g = a.iter().find(|g| g.id == "15365");
        assert_eq!(g.unwrap().name, "麻美ゆま");

        let resp = dmm
            .actress_search(ActressSearchParams {
                gte_birthday: Some(chrono::NaiveDate::from_ymd_opt(2000, 1, 1).unwrap()),
                // not given offset, returns number first_position
                ..ActressSearchParams::default()
            })
            .await
            .unwrap();
        assert_eq!(resp.first_position, 1);
    }

    #[tokio::test]
    async fn test_genre_search() {
        let dmm = client();
        let resp = dmm
            .genre_search(GenreSearchParams {
                floor_id: "25".to_string(),
                initial: Some(''),
                hits: Some(10),
                offset: Some(10),
            })
            .await
            .unwrap();
        let g = resp.genre.iter().find(|g| g.genre_id == "73115").unwrap();
        assert_eq!(g.name, "キャラクター");
    }

    #[tokio::test]
    async fn test_maker_search() {
        let dmm = client();
        let res = dmm
            .series_search(SeriesSearchParams {
                floor_id: "27".to_string(),
                initial: Some(''),
                hits: Some(10),
                ..SeriesSearchParams::default()
            })
            .await
            .unwrap();
        let s = res.series.iter().find(|m| m.series_id == "100864").unwrap();
        assert_eq!(s.name, "おいしい銀座");
    }

    #[tokio::test]
    async fn test_series_search() {
        let dmm = client();
        let res = dmm
            .maker_search(MakerSearchParams {
                floor_id: "27".to_string(),
                initial: Some(''),
                offset: Some(1),
                ..MakerSearchParams::default()
            })
            .await
            .unwrap();
        let m = res.maker.iter().find(|m| m.maker_id == "93146").unwrap();
        assert_eq!(m.name, "講談社");
    }

    #[tokio::test]
    async fn test_author_search() {
        let dmm = client();
        let res = dmm
            .author_search(AuthorSearchParams {
                floor_id: "27".to_string(),
                initial: Some(''),
                offset: Some(100),
                ..AuthorSearchParams::default()
            })
            .await
            .unwrap();
        assert_eq!(res.status, "200");
    }

    fn client() -> Dmm {
        let api_id = env::var("DMM_API_ID").unwrap();
        let affiliate_id = env::var("DMM_AFFILIATE_ID").unwrap();
        Dmm::new(&api_id, &affiliate_id)
    }
}