dregs 1.0.0

The official Rust client for Dregs, which scores the users of your application for fraud and abuse.
Documentation
//! The `client.groups()` namespace.

use reqwest::Method;
use serde::Serialize;
use serde_json::json;

use crate::client::{Client, object_or_null, segment};
use crate::error::Result;
use crate::event::{Group, Serialized, object_or_empty};
use crate::models::TrackedGroup;
use crate::paging::{ItemStream, ListCall, Page, Paging, filter, paging_setters};

/// Read and manage groups: the organizations, teams, or workspaces your users belong to. Get one
/// from [`Client::groups`].
///
/// [`Client::track`] already records groups and memberships as events arrive. These calls are for
/// reading them back, and for keeping Dregs in step when something changes outside an event, such
/// as a user leaving a team.
#[derive(Debug, Clone, Copy)]
pub struct Groups<'a> {
    client: &'a Client,
}

impl<'a> Groups<'a> {
    pub(crate) fn new(client: &'a Client) -> Self {
        Groups { client }
    }

    /// Returns one page of groups, most recently active first.
    ///
    /// The groups in a list leave out their attributes; [`get`](Self::get) returns them. Use
    /// [`list_all`](Self::list_all) to walk every page.
    pub async fn list(&self, params: ListGroups) -> Result<Page<TrackedGroup>> {
        params.call().page(self.client).await
    }

    /// Every group that matches, fetching pages as it goes. See [`list`](Self::list).
    pub fn list_all(&self, params: ListGroups) -> ItemStream<TrackedGroup> {
        params.call().walk(self.client)
    }

    /// Returns the group, with its attributes.
    ///
    /// Returns an error of kind [`NotFound`](crate::ApiErrorKind::NotFound) when no group of this
    /// type has this id.
    pub async fn get(&self, group_type: &str, group_id: &str) -> Result<TrackedGroup> {
        let payload = self
            .client
            .request(Method::GET, &path(group_type, group_id, "")?, &[], None)
            .await?;

        Ok(TrackedGroup::from_value(object_or_null(payload)))
    }

    /// Creates a group, or merges its data into the group that already has this type and id.
    ///
    /// ```no_run
    /// # async fn run(client: dregs::Client) -> dregs::Result<()> {
    /// use dregs::Group;
    /// use serde_json::json;
    ///
    /// client
    ///     .groups()
    ///     .create(Group::organization("org_678").data(json!({ "name": "Acme Inc" })))
    ///     .await?;
    /// # Ok(())
    /// # }
    /// ```
    ///
    /// The group's type is `"organization"` when empty, and its data, sent as the attributes to
    /// merge in, is an empty object when unset. Merging makes this safe to call on every sync.
    pub async fn create(&self, group: Group) -> Result<TrackedGroup> {
        let body = group.into_create_body()?;
        let payload = self
            .client
            .request(Method::POST, "/groups", &[], Some(&body))
            .await?;

        Ok(TrackedGroup::from_value(object_or_null(payload)))
    }

    /// Merges `attributes` into the group and returns it. Anything that serializes to a JSON
    /// object works.
    ///
    /// Returns an error of kind [`NotFound`](crate::ApiErrorKind::NotFound) when no group of this
    /// type has this id.
    pub async fn update(
        &self,
        group_type: &str,
        group_id: &str,
        attributes: impl Serialize,
    ) -> Result<TrackedGroup> {
        let path = path(group_type, group_id, "")?;
        let attributes = object_or_empty(Some(Serialized::from(attributes)), "attributes")?;
        let body = json!({ "attributes": attributes });
        let payload = self
            .client
            .request(Method::PATCH, &path, &[], Some(&body))
            .await?;

        Ok(TrackedGroup::from_value(object_or_null(payload)))
    }

    /// Makes the identity a member of the group. Adding an existing member does nothing.
    ///
    /// Returns an error of kind [`NotFound`](crate::ApiErrorKind::NotFound) when the group or the
    /// identity is unknown.
    pub async fn add_member(
        &self,
        group_type: &str,
        group_id: &str,
        identity_id: &str,
    ) -> Result<()> {
        self.member(Method::PUT, group_type, group_id, identity_id)
            .await
    }

    /// Removes the identity from the group, such as when a user leaves a team.
    ///
    /// Returns an error of kind [`NotFound`](crate::ApiErrorKind::NotFound) when the group or the
    /// identity is unknown.
    pub async fn remove_member(
        &self,
        group_type: &str,
        group_id: &str,
        identity_id: &str,
    ) -> Result<()> {
        self.member(Method::DELETE, group_type, group_id, identity_id)
            .await
    }

    async fn member(
        &self,
        method: Method,
        group_type: &str,
        group_id: &str,
        identity_id: &str,
    ) -> Result<()> {
        let suffix = format!("/identities/{}", segment(identity_id, "An identity id")?);

        self.client
            .request(method, &path(group_type, group_id, &suffix)?, &[], None)
            .await?;

        Ok(())
    }
}

/// The filters and paging for [`Groups::list`] and [`Groups::list_all`].
///
/// Sort on fields such as `"displayName"` or `"lastTrackedAt"`. A filter left unset is not sent.
#[derive(Debug, Clone, Default)]
pub struct ListGroups {
    group_type: Option<String>,
    identity: Option<String>,
    term: Option<String>,
    paging: Paging,
}

impl ListGroups {
    /// Every group, from the first page.
    pub fn new() -> Self {
        ListGroups::default()
    }

    /// Only groups of this type, such as `"team"`.
    pub fn group_type(mut self, group_type: impl Into<String>) -> Self {
        self.group_type = Some(group_type.into());
        self
    }

    /// Only groups this identity belongs to.
    pub fn identity(mut self, identity_id: impl Into<String>) -> Self {
        self.identity = Some(identity_id.into());
        self
    }

    /// Free text matched against group ids and names.
    pub fn term(mut self, term: impl Into<String>) -> Self {
        self.term = Some(term.into());
        self
    }

    paging_setters!();

    fn call(self) -> ListCall<TrackedGroup> {
        let mut filters = Vec::new();

        filter(&mut filters, "type", self.group_type);
        filter(&mut filters, "identity", self.identity);
        filter(&mut filters, "term", self.term);

        ListCall {
            path: "/groups",
            filters,
            paging: self.paging,
            parse: TrackedGroup::from_value,
        }
    }
}

fn path(group_type: &str, group_id: &str, suffix: &str) -> Result<String> {
    Ok(format!(
        "/groups/{}/{}{suffix}",
        segment(group_type, "A group type")?,
        segment(group_id, "A group id")?
    ))
}