lemon_bugsnag_rs 0.1.2

A library for interacting with the BugSnag error-reporting and sessions APIs.
Documentation
use std::ops::{Deref, DerefMut};
#[cfg(feature = "ureq")]
use ureq::AgentBuilder;

use super::payload::events::enums::Severity;
use super::payload::events::App;
use super::payload::events::Event;
use super::payload::events::Exception;
use super::payload::events::StackTrace;
use super::payload::notifier::Notifier;
use crate::common::builder::client_builder::ClientBuilderInternal;

#[cfg(feature = "reqwest")]
use super::reqwest::builder::decider::ReqwestErrorDecider;
#[cfg(feature = "ureq")]
use super::ureq::builder::blocking::ClientBuilderUreqErrorSync;

/// Builder specific to the BugSnag error-reporting API
///
/// ```
/// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
/// use lemon_bugsnag_rs::error::payload::notifier::Notifier;
/// use lemon_bugsnag_rs::common::traits::backend_builder_trait_sync::BackendBuilderTraitSync;
///
/// // Obtain an ErrorClientBuilder
/// let mut cb = ClientBuilder::error();
///
/// // Configure the builder.
/// cb.set_api_key(
///   "This is a fake API key"
/// ); // This function exists on the underlying ClientBuilderInternal struct
///    // which is the Deref target for ErrorClientBuilder.
/// cb.set_notifier(
///     Notifier {
///         name: "Notifier name".to_string()
///         , version: "Notifier version".to_string()
///         , url: "https://some.notifier.url/".to_string()
///         , dependencies: None
///     }
/// );  // This function is specific to the ErrorClientBuilder, and exists on
///     // that struct, and not on ClientBuilderInternal.
///
/// // Continue configuring the builder to suit your needs.
/// // ...
/// # cb.configure(
/// # "Fake API Key",
/// # "Doctest configure example error class",
/// # "It's not really an error, per se...",
/// # "ErrorClientBuilder documentation test notifier",
/// # "1.0.0",
/// # "Doctest app type",
/// # "Production, of course"
/// # );
///
/// // Now that you've set your options, fetch a client for making requests to
/// // the BugSnag API.
/// // Note that `unwrap()` needs to be handled gracefully in real-life usage.
/// let client =
///     cb.ureq()
///     .build()
///     .unwrap();
/// // You could also choose to use the Reqwest networking back-end by replacing
/// // `ureq()` with `reqwest()`. If and when other networking back-ends are
/// // implemented, this is where you will select the on you want.
/// ```
#[derive(Clone, Debug, PartialEq)]
pub struct ErrorClientBuilder {
    pub(crate) builder_internal: ClientBuilderInternal,
    pub(crate) notifier: Option<Notifier>,
    pub(crate) events: Option<Vec<Event>>,
}

impl ErrorClientBuilder {
    /// Retrieve the [Notifier] currently set for this [ErrorClientBuilder].
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    ///
    /// let mut cb = ClientBuilder::error();
    /// // Let's assume you've thoroughly configured the ErrorClientBuilder.
    ///
    /// if let Some(notifier) = cb.notifier() {
    ///     // Do things with the notifier
    /// } else {
    ///     // Do something else.
    /// }
    /// ```
    pub fn notifier(&self) -> Option<&Notifier> {
        self.notifier.as_ref()
    }

    /// Set a [Notifier] on the [ErrorClientBuilder].
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::error::payload::notifier::Notifier;
    ///
    /// let notifier: Notifier = Notifier {
    ///     name: "Notifier name".to_string()
    ///     , version: "Notifier version".to_string()
    ///     , url: "https://some.notifier.url/".to_string()
    ///     , dependencies: None
    /// };
    ///
    /// let mut cb = ClientBuilder::error();
    ///
    /// cb.set_notifier(notifier);
    ///
    /// ```
    pub fn set_notifier(
        &mut self,
        notifier: impl Into<Option<Notifier>>,
    ) -> &mut Self {
        self.notifier = notifier.into();

        self
    }

    /// Retrieve the vector of [Event]s currently set for this
    /// [ErrorClientBuilder].
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    ///
    /// let mut cb = ClientBuilder::error();
    /// // Let's assume you've thoroughly configured the ErrorClientBuilder.
    ///
    /// if let Some(notifier) = cb.events() {
    ///     // Do things with the events
    /// } else {
    ///     // Do something else.
    /// }
    /// ```
    pub fn events(&self) -> Option<Vec<Event>> {
        self.events.clone()
    }

    /// Set a vector of [Event]s on the [ErrorClientBuilder].
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::error::payload::events::Event;
    ///
    /// let events = vec![
    ///     Event::default()   // Configuration of Events omitted for brevity.
    ///     , Event::default()
    /// ];
    ///
    /// let mut cb = ClientBuilder::error();
    ///
    /// cb.set_events(events);
    ///
    /// ```
    pub fn set_events(
        &mut self,
        events: impl Into<Option<Vec<Event>>>,
    ) -> &mut Self {
        self.events = events.into();

        self
    }

    /// Creates a valid error payload with backtrace from a minimal
    /// configuration.
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::common::traits::backend_builder_trait_sync::BackendBuilderTraitSync;
    ///
    /// let mut cb = ClientBuilder::error();
    ///
    /// cb.configure(
    ///   "Fake API Key",
    ///   "Doctest configure example error class",
    ///   "It's not really an error, per se...",
    ///   "ErrorClientBuilder documentation test notifier",
    ///   "1.0.0",
    ///   "Doctest app type",
    ///   "Production, of course"
    /// );
    ///
    /// // Fetch a blocking client which uses the Ureq networking library.
    /// let client = cb.ureq().build();
    /// ```
    pub fn configure<'a>(
        &mut self,
        api_key: impl Into<String>,
        error_class: impl Into<String>,
        error_message: impl Into<String>,
        notifier_name: impl Into<String>,
        notifier_version: impl Into<String>,
        app_type: impl Into<Option<&'a str>>,
        release_stage: impl Into<Option<&'a str>>,
    ) -> &mut Self {
        let (notifier_name, notifier_version): (String, String) =
            (notifier_name.into(), notifier_version.into());

        let app_type_option = app_type.into().map(|item| item.to_string());
        let release_stage_option =
            release_stage.into().map(|item| item.to_string());

        self.api_key = Some(api_key.into());

        self.notifier = Some(Notifier {
            name: notifier_name.clone(),
            version: notifier_version.clone(),
            // BugSnag API doumentation shows this field as "required," yet
            // it does not seem to show up in the BugSnag error-reporting
            // interface, and passing an empty string does not stop BugSnag
            // from accepting the payload and recording the error.
            url: "".into(),
            dependencies: None,
        });

        let app_name = notifier_name;
        let app_version = notifier_version;
        let events: Vec<Event> = [Event {
            exceptions: [Exception {
                error_class: error_class.into(),
                message: Some(error_message.into()),
                stack_trace: StackTrace::grab(),
                runtime_type: None,
            }]
            .into(),
            breadcrumbs: None,
            request: None,
            threads: None,
            context: None,
            grouping_hash: None,
            unhandled: None,
            severity: Some(Severity::Error),
            severity_reason: None,
            project_packages: None,
            user: None,
            app: {
                if app_type_option.is_some() || release_stage_option.is_some() {
                    Some(App {
                        id: Some(app_name),
                        version: Some(app_version),
                        version_code: None,
                        bundle_version: None,
                        code_bundle_id: None,
                        build_uuid: None,
                        release_stage: release_stage_option,
                        app_type: app_type_option,
                        dsym_uuids: None,
                        duration: None,
                        duration_in_foreground: None,
                        in_foreground: None,
                        is_launching: None,
                        binary_arch: None,
                        running_on_rosetta: None,
                    })
                } else {
                    None
                }
            },
            device: None,
            session: None,
            feature_flags: None,
            meta_data: None,
        }]
        .into();

        match self.events.as_mut() {
            Some(current_events) => current_events.extend(events),
            None => self.events = Some(events.into()),
        }

        self
    }
}

// Implement functions to return backend-specific client builders.
impl ErrorClientBuilder {
    /// Retrieve a [ReqwestErrorDecider] which, in conjunction with the
    /// [DeciderTraitSync](crate::common::traits::decider_trait_sync::DeciderTraitSync)
    /// and [DeciderTraitAsync](crate::common::traits::decider_trait_async::DeciderTraitAsync)
    /// traits, allows you to retrieve a blocking or non-blocking client based
    /// on Reqwest.
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::common::traits::decider_trait_sync::DeciderTraitSync;
    ///
    /// let mut cb = ClientBuilder::error();
    /// // Configuration of builder ommitted for brevity.
    ///
    /// // Select the Reqwest back-end. With the proper traits in scope, you
    /// // can now call decider.blocking() or decider.non_blocking(), depending
    /// // on your needs.
    /// let mut decider = cb.reqwest();
    /// ```
    #[cfg_attr(docsrs, doc(cfg(feature = "reqwest")))]
    #[cfg(feature = "reqwest")]
    pub fn reqwest(&self) -> ReqwestErrorDecider {
        ReqwestErrorDecider { builder: self.clone() }
    }

    /// Retrieve a [ClientBuilderUreqErrorSync] based on the Reqwest networking library.
    ///
    /// ```
    /// use lemon_bugsnag_rs::common::builder::client_builder::ClientBuilder;
    /// use lemon_bugsnag_rs::common::traits::decider_trait_sync::DeciderTraitSync;
    ///
    /// let mut cb = ClientBuilder::error();
    /// // Configuration of builder ommitted for brevity.
    ///
    /// // Select the Ureq back-end. With the proper traits in scope, you
    /// // can now call decider.blocking() or decider.non_blocking(), depending
    /// // on your needs. In the case of `Ureq`, only the `blocking` back-end
    /// // is available.
    /// let mut decider = cb.reqwest();
    /// ```
    #[cfg_attr(docsrs, doc(cfg(feature = "ureq")))]
    #[cfg(feature = "ureq")]
    pub fn ureq(&self) -> ClientBuilderUreqErrorSync {
        ClientBuilderUreqErrorSync {
            client_builder: AgentBuilder::new(),
            options: self.options.clone(),
            api_key: self.api_key.clone(),
            payload_version: self
                .payload_version_override
                .clone()
                .unwrap_or(self.payload_version.clone()),
            notifier: self.notifier.clone(),
            events: self.events.clone(),
        }
    }
}

// Implement Deref so the ErrorClientBuilder can retrieve the portions of the
// payload that are common across multiple APIs.
impl Deref for ErrorClientBuilder {
    type Target = ClientBuilderInternal;

    fn deref(&self) -> &Self::Target {
        &self.builder_internal
    }
}

// Implement DerefMut so the ErrorClientBuilder can configure the portions of
// the payload that are common across multiple APIs.
impl DerefMut for ErrorClientBuilder {
    fn deref_mut(&mut self) -> &mut Self::Target {
        &mut self.builder_internal
    }
}