chatwork 0.1.0

Chatwork API client.
Documentation
use reqwest::{
    header::{HeaderMap, HeaderName, HeaderValue},
    multipart::Form,
    redirect::Policy,
    Method, RequestBuilder, StatusCode, Url,
};
use serde::{de::DeserializeOwned, Serialize};

use std::{fmt, str::FromStr, time::Duration};

#[derive(Clone)]
struct Config {
    base_uri: Url,
    timeout: Duration,
}

impl Default for Config {
    fn default() -> Self {
        Self {
            base_uri: Url::from_str("https://api.chatwork.com/").unwrap(),
            timeout: Duration::from_secs(10),
        }
    }
}

pub(crate) struct PathAndQuery<'a, T> {
    pub path: &'a str,
    pub query: Option<&'a T>,
}

// https://developer.chatwork.com/ja/endpoint_rooms.html
// https://github.com/chatwork/api/blob/master/RAML/api-ja.raml
#[derive(Clone)]
pub struct Client {
    inner: reqwest::Client,
    config: Config,
}

impl Client {
    #[cfg(any(feature = "default", feature = "rustls-tls"))]
    pub fn new(api_token: impl AsRef<str>) -> Self {
        let config = Config::default();

        let inner = reqwest::Client::builder()
            .user_agent(concat!(
                "github.com/mechiru/",
                env!("CARGO_PKG_NAME"),
                "/",
                env!("CARGO_PKG_VERSION")
            ))
            .default_headers({
                let mut headers = HeaderMap::new();
                headers.insert(
                    HeaderName::from_static("x-chatworktoken"),
                    HeaderValue::from_str(api_token.as_ref()).expect("invalid api token"),
                );
                headers
            })
            .redirect(Policy::none())
            .https_only(true)
            .timeout(config.timeout)
            .connect_timeout(config.timeout)
            .build()
            .unwrap();

        Self { inner, config }
    }

    #[cfg(not(any(feature = "default", feature = "rustls-tls")))]
    pub fn new_with(client: reqwest::Client) -> Self {
        Self { inner: client, config }
    }
}

impl Client {
    fn url<Query>(&self, path_and_query: PathAndQuery<'_, Query>) -> crate::Result<Url>
    where
        Query: Serialize,
    {
        let mut url = self.config.base_uri.clone();
        url.set_path(path_and_query.path);
        if let Some(query) = path_and_query.query {
            let query = serde_urlencoded::to_string(query)?;
            url.set_query(Some(query.as_str()));
        }
        Ok(url)
    }

    async fn request<Query, Body, Model>(
        &self,
        method: Method,
        path_and_query: PathAndQuery<'_, Query>,
        body: Option<&Body>,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Body: Serialize,
        Model: DeserializeOwned,
    {
        let mut req = self.inner.request(method, self.url(path_and_query)?);
        if let Some(body) = body {
            req = req.form(body);
        }
        self.handle(req).await
    }

    async fn handle<Model>(&self, req: RequestBuilder) -> crate::Result<Model>
    where
        Model: DeserializeOwned,
    {
        let resp = req.send().await?;
        match resp.status() {
            StatusCode::OK => Ok(resp.json().await?),
            code => Err(crate::Error::StatusCode(code)),
        }
    }

    pub(crate) async fn get<Query, Model>(
        &self,
        path_and_query: PathAndQuery<'_, Query>,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Model: DeserializeOwned,
    {
        self.request(Method::GET, path_and_query, Option::<&()>::None).await
    }

    pub(crate) async fn post<Query, Body, Model>(
        &self,
        path_and_query: PathAndQuery<'_, Query>,
        body: &Body,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Body: Serialize,
        Model: DeserializeOwned,
    {
        self.request(Method::POST, path_and_query, Some(body)).await
    }

    pub(crate) async fn put<Query, Body, Model>(
        &self,
        path_and_query: PathAndQuery<'_, Query>,
        body: Option<&Body>,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Body: Serialize,
        Model: DeserializeOwned,
    {
        self.request(Method::PUT, path_and_query, body).await
    }

    pub(crate) async fn delete<Query, Body, Model>(
        &self,
        path_and_query: PathAndQuery<'_, Query>,
        body: Option<&Body>,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Body: Serialize,
        Model: DeserializeOwned,
    {
        self.request(Method::DELETE, path_and_query, body).await
    }

    pub(crate) async fn multipart<Query, Model>(
        &self,
        path_and_query: PathAndQuery<'_, Query>,
        form: Form,
    ) -> crate::Result<Model>
    where
        Query: Serialize,
        Model: DeserializeOwned,
    {
        self.handle(self.inner.post(self.url(path_and_query)?).multipart(form)).await
    }
}

impl Client {
    pub fn v2(&self) -> crate::v2::Service {
        crate::v2::Service { inner: self.clone() }
    }
}

impl fmt::Debug for Client {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        f.debug_struct("Client").finish()
    }
}