lemon_bugsnag_rs 0.1.2

A library for interacting with the BugSnag error-reporting and sessions APIs.
Documentation
use reqwest::{
    header::{HeaderMap, HeaderName, HeaderValue},
    ClientBuilder,
};

use crate::{
    common::{
        builder::{builder_options::BuilderOptions, error::BuilderError},
        cobbler::Cobbler,
        error::ErrorStack,
        traits::backend_builder_trait_async::BackendBuilderTraitAsync,
    },
    session::{
        payload::{
            device::Device, notifier::Notifier,
            session_counts::session_count::SessionCount, sessions::Session,
            App, Payload,
        },
        reqwest::client::non_blocking::ReqwestSessionAsync,
    },
};

/// The client builder for reqwest containing information needed to build the
/// [ReqwestSessionAsync] struct which sends asynchronous requests to Bugsnag's
/// sessions API
///
/// ```
/// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
/// use lemon_bugsnag_rs::common::traits::decider_trait_async::DeciderTraitAsync;
///
/// let mut session_client_builder = ClientBuilder::session();
///
/// let configured_builder = session_client_builder.configure(
///     "Fake API Key",
///     "Fake Notifier",
///     "1.3.2",
///     "Production",
///     "1.0.0"
/// );
///
/// // This client_builder will build the asynchronous client that sends the
/// // request to Bugsnag's Session API
/// let client_builder = configured_builder.reqwest().non_blocking();
/// ```
pub struct ClientBuilderReqwestSessionAsync {
    /// Reqwest backend client builder that builds the client which sends
    /// asynchronous requests to Bugsnag Session API
    pub client_builder: ClientBuilder,
    /// Options struct common to all BugSnag client builders in this library.
    pub(crate) options: BuilderOptions,
    /// The API Key associated with the project. Informs Bugsnag which project
    /// has generated this error.
    pub(crate) api_key: Option<String>,
    /// The version number of the payload. The Bugsnag-Payload-Version header
    /// should be included as well, for compatibility reasons.
    pub(crate) payload_version: String,
    /// Information about the notifier used to send the session information
    pub(crate) notifier: Option<Notifier>,
    /// Information about the running app that the session started on
    pub(crate) app: Option<App>,
    /// Information about the host device running the application that the
    /// session started on
    pub(crate) device: Option<Device>,
    /// Details of the sessions that have started. Multiple sessions on the same
    /// app and device can be batched together to minimize network traffic.
    ///
    /// This field is required unless sessionsCounts are being used.
    pub(crate) sessions: Option<Vec<Session>>,
    /// Summary counts of the number of sessions started in a minute.
    ///
    /// This can be used instead of sessions for server side applications to
    /// provide a session count summary and reduce the payload's size being sent
    pub(crate) session_counts: Option<Vec<SessionCount>>,
}

// #[cfg(all(feature = "reqwest", feature = "async"))]
// impl BackendBuilderTrait<ReqwestSessionDecider, Error> for ClientBuilderReqwestSessionAsync {}

impl BackendBuilderTraitAsync<ReqwestSessionAsync, ErrorStack>
    for ClientBuilderReqwestSessionAsync
{
    /// Builds the reqwest client that sends synchronous requests to the Bugsnag
    /// Session API validating fields needed to send the requests.
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::common::traits::decider_trait_async::{
    ///     DeciderTraitAsync
    /// };
    /// use lemon_bugsnag_rs::common::traits::backend_builder_trait_async::{
    ///     BackendBuilderTraitAsync
    /// };
    ///
    /// let mut session_client_builder = ClientBuilder::session();
    ///
    /// let configured_builder = session_client_builder.configure(
    ///     "Fake API Key",
    ///     "Fake Notifier",
    ///     "1.3.2",
    ///     "Production",
    ///     "1.0.0"
    /// );
    ///
    /// let client_builder = configured_builder.reqwest().non_blocking().build()
    ///     .unwrap();
    /// ```
    fn build(mut self) -> Result<ReqwestSessionAsync, ErrorStack> {
        let mut error_stack = ErrorStack::default();
        let mut missing_fields: Vec<String> = Vec::new();

        let headers = self.options.headers.as_mut().unwrap();
        if self.api_key.is_some() {
            headers.insert(
                "Bugsnag-Api-Key".to_string(),
                self.api_key.clone().unwrap(),
            );
        }

        headers.insert(
            "Bugsnag-Payload-Version".into(),
            self.payload_version.clone(),
        );

        // We check the headers and not self.api_key because it is possible for
        // the user to have directly set the API key in the headers, bypassing
        // set_api_key().
        if !headers.contains_key("Bugsnag-Api-Key") {
            missing_fields.push("api_key".to_string());
        }

        if self.notifier.is_none() {
            missing_fields.push("notifier".to_string());
        }

        if missing_fields.len() > 0 {
            error_stack.errors.push(
                BuilderError::MissingField {
                    context:
                        "BackendBuilderTrait<ReqwestSessionAsync, ErrorStack>"
                            .to_string(),
                    fields: missing_fields,
                }
                .into(),
            );
        }

        if self.sessions.is_some() && self.session_counts.is_some() {
            let mut conflicting_fields: Vec<String> = Vec::new();
            conflicting_fields.push("sessions".to_string());
            conflicting_fields.push("session_counts".to_string());
            error_stack.errors.push(
                BuilderError::ConflictingField {
                    context:
                        "BackendBuilderTrait<ReqwestSessionAsync, ErrorStack>"
                            .to_string(),
                    fields: conflicting_fields,
                }
                .into(),
            );
        }

        match &self.app {
            Some(app) => {
                if app.version_code.is_some() && app.bundle_version.is_some() {
                    let mut conflicting_fields: Vec<String> = Vec::new();
                    conflicting_fields.push("app.version_code".to_string());
                    conflicting_fields.push("app.bundle_version".to_string());
                    error_stack.errors.push(
                        BuilderError::ConflictingField {
                            context: "BackendBuilderTrait<ReqwestSessionAsync, ErrorStack>".to_string(),
                            fields: conflicting_fields,
                        }
                        .into(),
                    );
                }
            }
            None => (),
        }

        if error_stack.errors.len() == 0 {
            let client = self
                .client_builder
                .default_headers(
                    self.options
                        .headers
                        .as_ref()
                        .unwrap()
                        .into_iter()
                        .map(|(header_name, header_value)| {
                            (
                                HeaderName::from_bytes(header_name.as_bytes())
                                    .unwrap(),
                                HeaderValue::from_str(&header_value).unwrap(),
                            )
                        })
                        .collect::<HeaderMap>(),
                )
                .build()?;

            let payload = Payload {
                notifier: self.notifier.unwrap(),
                app: self.app,
                device: self.device,
                sessions: self.sessions,
                session_counts: self.session_counts,
            };

            let url = Cobbler::cobble(self.options)?;

            Ok(ReqwestSessionAsync { url, client, payload })
        } else {
            Err(error_stack)
        }
    }
}