open-opus 0.1.0

Open Opus API client
Documentation
use serde::{de, Deserialize};
use serde_with::{serde_as, DeserializeAs, DisplayFromStr};

use crate::{Genre, OpenOpusError, OpenOpusResult, Status, ID};

#[derive(Debug, Deserialize)]
struct Works {
    status: Status,
    works: Option<Vec<Work>>,
}

#[serde_as]
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
pub struct Work {
    pub title: String,
    pub subtitle: String,
    #[serde(rename = "searchterms")]
    pub search_terms: String,
    #[serde_as(as = "IntStrBool")]
    pub popular: bool,
    #[serde_as(as = "IntStrBool")]
    pub recommended: bool,
    #[serde_as(as = "DisplayFromStr")]
    id: ID,
    pub genre: Genre,
}

impl Work {
    async fn list_common(url: &str) -> OpenOpusResult<Vec<Self>> {
        let result = reqwest::get(url).await?.json::<Works>().await?;

        dbg!(&result);

        match result.status {
            Status::Ok(_) => Ok(result.works.unwrap()),
            Status::Err(err) => Err(OpenOpusError::OpenOpusAPIError(err.error)),
        }
    }

    pub(crate) async fn list_by_composer_id_and_genre(
        composer_id: ID,
        genre: Genre,
    ) -> OpenOpusResult<Vec<Self>> {
        Self::list_common(&format!(
            "https://api.openopus.org/work/list/composer/{}/genre/{}.json",
            composer_id,
            genre.into_url_str()
        ))
        .await
    }

    pub(crate) async fn saerch_with_composer_id_and_genre(
        composer_id: ID,
        genre: Genre,
        word: &str,
    ) -> OpenOpusResult<Vec<Self>> {
        Self::list_common(&format!(
            "https://api.openopus.org/work/list/composer/{}/genre/{}/search/{}.json",
            composer_id,
            genre.into_url_str(),
            word
        ))
        .await
    }
}

struct IntStrBool;

impl<'de> DeserializeAs<'de, bool> for IntStrBool {
    fn deserialize_as<D>(deserializer: D) -> Result<bool, D::Error>
    where
        D: serde::Deserializer<'de>,
    {
        let s = String::deserialize(deserializer).map_err(de::Error::custom)?;
        Ok(match s.as_ref() {
            "0" => false,
            "1" => true,
            _ => {
                return Err(de::Error::custom(
                    r#"Invalid string: {}. should be "0" or "1""#,
                ))
            }
        })
    }
}

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

    #[tokio::test]
    async fn test_list_by_composer_id() -> anyhow::Result<()> {
        let _ = Work::list_by_composer_id_and_genre(130, Genre::All).await?;
        Ok(())
    }

    #[tokio::test]
    async fn test_list_by_composer_id_and_genre() -> anyhow::Result<()> {
        let _ = Work::list_by_composer_id_and_genre(2, Genre::Orchestral).await?;
        Ok(())
    }

    #[tokio::test]
    async fn test_saerch_with_composer_id() -> anyhow::Result<()> {
        let _ = Work::saerch_with_composer_id_and_genre(196, Genre::All, "Sonata").await?;
        Ok(())
    }

    #[tokio::test]
    async fn test_saerch_with_composer_id_and_genre() -> anyhow::Result<()> {
        let _ =
            Work::saerch_with_composer_id_and_genre(145, Genre::Chamber, "Cello Sonata").await?;
        Ok(())
    }
}