rrw 0.1.2

A crate to easily build clients for REST-APIs.
Documentation
use std::rc::Rc;

use serde::{de::DeserializeOwned, Serialize};

use crate::{
    authenticate::Authenticator,
    throttle::{Throttle, ThrottleMechanism},
    Error, RestRequest,
};

/// A configuration to simplify REST-API-requests.
///
/// # Examples
///
/// Simple [RestConfig].
///
/// ```rust
/// # use std::error::Error;
/// # use rrw::RestConfig;
/// #
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let config = RestConfig::new("https://example.com/rest");
/// #
/// #     Ok(())
/// # }
/// ```
///
/// More Complicated [RestConfig] using [RestConfigBuilder].
///
/// ```rust
/// # use std::error::Error;
/// # use rrw::RestConfigBuilder;
/// # use rrw::throttle::StupidThrottle;
/// #
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let config = RestConfigBuilder::new("https://example.com/rest")
///     .client(reqwest::Client::new())
///     .throttle(StupidThrottle::new(10.0))
///     .build();
/// #
/// #     Ok(())
/// # }
/// ```
#[derive(Clone)]
pub struct RestConfig {
    endpoint: String,
    client: reqwest::Client,
    throttle: Option<Throttle>,
    authenticator: Option<Rc<Box<dyn Authenticator>>>,
}

impl RestConfig {
    /// Construct a new, simple [RestConfig] using the endpoint to use.
    ///
    /// The client of the requests will be set to [reqwest::Client::new()], no throttling will be
    /// applied.
    ///
    /// For more sophisticated construction, look at [RestConfigBuilder].
    pub fn new<S: AsRef<str>>(endpoint: S) -> Self {
        log::trace!(
            "Create a new RestConfig with endpoint {}.",
            endpoint.as_ref()
        );
        Self {
            endpoint: endpoint.as_ref().to_string(),
            client: reqwest::Client::new(),
            throttle: None,
            authenticator: None,
        }
    }

    /// Execute a [RestRequest] using the configuration.
    ///
    /// # Example
    ///
    /// ```rust
    /// # use rrw::RestConfig;
    /// # use rrw::throttle::StupidThrottle;
    /// # use rrw::RestRequest;
    /// # use rrw::Error;
    /// # use rrw::StandardRestError;
    /// # use serde::Deserialize;
    /// # use httpmock::prelude::*;
    /// #
    /// #[derive(Deserialize)]
    /// struct Json {
    ///     key: String
    /// }
    ///
    /// # #[tokio::main]
    /// # async fn main() -> Result<(), Error<StandardRestError>> {
    /// # let server = MockServer::start();
    /// #
    /// # let mock = server.mock(|when, then| {
    /// #     when.method(GET).path("/test");
    /// #     then.status(200)
    /// #         .header("content-type", "application/json; charset=UTF-8")
    /// #         .body(r#"{"key": "value"}"#);
    /// # });
    /// #
    /// # let config = RestConfig::new(server.base_url());
    /// let result: Json = config
    ///     .execute(&RestRequest::<(), ()>::get("/test"))
    ///     .await?;
    /// #
    /// # mock.assert();
    /// #
    /// # Ok(())
    /// # }
    /// ```
    pub async fn execute<T: DeserializeOwned, E: DeserializeOwned, Q: Serialize, B: Serialize>(
        &self,
        req: &RestRequest<Q, B>,
    ) -> Result<T, Error<E>> {
        // Throttle request.
        if let Some(throttle) = &self.throttle {
            throttle.throttle().await;
        }

        // Calculating full request path.
        let full_path = self.endpoint.clone() + &req.path;
        log::debug!("Executing a REST-Request to {:?}", full_path);

        // Building request.
        let request = self.client.request(req.method.clone(), full_path);
        let request = if let Some(query) = &req.query {
            log::trace!("Request got a query.");
            request.query(query)
        } else {
            request
        };
        let request = if let Some(body) = &req.body {
            log::trace!("Request got a body.");
            request.json(body)
        } else {
            request
        };

        let request = if req.authenticate {
            if let Some(authenticator) = &self.authenticator {
                authenticator.authenticate(request)
            } else {
                log::warn!(
                    "A request wanted to be authenticated, but no authenticator was registered."
                );
                request
            }
        } else {
            request
        };

        // Sending request.
        log::trace!("Sending REST-Request.");
        let response = request.send().await.map_err(|e| Error::Request(e))?;

        // Converting request to json.
        log::trace!("Converting REST-Request to wanted result type.");
        let bytes = response.bytes().await.unwrap(); // map_err(|e| Error::Request(e))?;
        let maybe_json = serde_json::from_slice(&bytes);
        if let Ok(converted) = maybe_json {
            Ok(converted)
        } else if let Ok(error) = serde_json::from_slice(&bytes) {
            Err(Error::Api(error))
        } else {
            Err(Error::BodyNotParsable(
                maybe_json.err().expect("Error to be present"),
            ))
        }
    }
}

/// A builder for a [RestConfig].
pub struct RestConfigBuilder {
    config: RestConfig,
}

impl RestConfigBuilder {
    /// Construct a [RestConfigBuilder] with the given endpoint.
    ///
    /// The client of the requests will be set to [reqwest::Client::new()], no throttling will be
    /// applied.
    pub fn new<S: AsRef<str>>(endpoint: S) -> Self {
        Self {
            config: RestConfig::new(endpoint),
        }
    }

    /// Customize the [reqwest::Client] to use to make the requests.
    pub fn client(mut self, client: reqwest::Client) -> Self {
        self.config.client = client;
        self
    }

    /// Apply a [ThrottleMechanism] to throttle requests.
    pub fn throttle<T: 'static + ThrottleMechanism>(mut self, throttle: T) -> Self {
        self.config.throttle = Some(Throttle::new(throttle));
        self
    }

    /// Apply a [Authenticator] to authenticate requests.
    pub fn authenticator<T: 'static + Authenticator>(mut self, authenticator: T) -> Self {
        self.config.authenticator = Some(Rc::new(Box::new(authenticator)));
        self
    }

    /// Build the [RestConfig] from the [RestConfigBuilder].
    pub fn build(self) -> RestConfig {
        self.config
    }
}

#[cfg(test)]
mod test {
    use crate::StandardRestError;

    use super::*;

    use httpmock::prelude::*;
    use serde::Deserialize;

    #[derive(Deserialize)]
    struct Empty {}

    #[derive(Debug, PartialEq, Eq, Serialize, Deserialize)]
    struct Json {
        key: String,
    }

    #[tokio::test]
    async fn config_simple_get() -> Result<(), Error<StandardRestError>> {
        let server = MockServer::start();

        let mock = server.mock(|when, then| {
            when.method(GET).path("/test");
            then.status(200)
                .header("content-type", "application/json; charset=UTF-8")
                .body("{}");
        });

        let config = RestConfig::new(server.base_url());
        let _: Empty = config.execute(&RestRequest::<(), ()>::get("/test")).await?;

        mock.assert();

        Ok(())
    }

    #[tokio::test]
    async fn config_query_get() -> Result<(), Error<StandardRestError>> {
        let server = MockServer::start();

        let mock = server.mock(|when, then| {
            when.method(GET).path("/test").query_param("key", "value");
            then.status(200)
                .header("content-type", "application/json; charset=UTF-8")
                .body("{}");
        });

        let config = RestConfig::new(server.base_url());
        let _: Empty = config
            .execute(&RestRequest::<&Json, ()>::get("/test").query(&Json {
                key: "value".to_string(),
            }))
            .await?;

        mock.assert();

        Ok(())
    }

    #[tokio::test]
    async fn config_simple_post() -> Result<(), Error<StandardRestError>> {
        let server = MockServer::start();

        let mock = server.mock(|when, then| {
            when.method(POST).path("/test").body(r#"{"key":"value"}"#);
            then.status(200)
                .header("content-type", "application/json; charset=UTF-8")
                .body("{}");
        });

        let config = RestConfig::new(server.base_url());
        let _: Empty = config
            .execute(&RestRequest::<(), &Json>::post("/test").body(&Json {
                key: "value".to_string(),
            }))
            .await?;

        mock.assert();

        Ok(())
    }

    #[tokio::test]
    async fn config_simple_return() -> Result<(), Error<StandardRestError>> {
        let server = MockServer::start();

        let mock = server.mock(|when, then| {
            when.method(GET).path("/test");
            then.status(200)
                .header("content-type", "application/json; charset=UTF-8")
                .body(r#"{"key":"value"}"#);
        });

        let config = RestConfig::new(server.base_url());
        let query: Json = config.execute(&RestRequest::<(), ()>::get("/test")).await?;

        mock.assert();
        assert_eq!(
            query,
            Json {
                key: "value".to_string()
            }
        );

        Ok(())
    }
}