guacamole-client 0.5.1

Rust client library for the Guacamole REST API
Documentation
use std::collections::HashMap;

use serde::{Deserialize, Serialize};

use crate::client::GuacamoleClient;
use crate::error::Result;
use crate::patch::PatchOperation;
use crate::validation::validate_user_group_id;

/// A Guacamole user group.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UserGroup {
    /// The group identifier.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identifier: Option<String>,

    /// Arbitrary group attributes.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub attributes: Option<HashMap<String, Option<String>>>,
}

impl GuacamoleClient {
    /// Lists all user groups in the given data source.
    pub async fn list_user_groups(
        &self,
        data_source: Option<&str>,
    ) -> Result<HashMap<String, UserGroup>> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/userGroups"))?;
        let response = self.http.get(&url).send().await?;
        Self::parse_response(response, "user groups").await
    }

    /// Retrieves a single user group by its identifier.
    pub async fn get_user_group(
        &self,
        data_source: Option<&str>,
        group_id: &str,
    ) -> Result<UserGroup> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
        let response = self.http.get(&url).send().await?;
        Self::parse_response(response, &format!("user group {group_id}")).await
    }

    /// Creates a new user group. Returns `()` on success (204 No Content).
    pub async fn create_user_group(
        &self,
        data_source: Option<&str>,
        group: &UserGroup,
    ) -> Result<()> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/userGroups"))?;
        let response = self.http.post(&url).json(group).send().await?;
        Self::handle_error(response, "create user group").await?;
        Ok(())
    }

    /// Updates an existing user group. Returns `()` on success (204 No Content).
    pub async fn update_user_group(
        &self,
        data_source: Option<&str>,
        group_id: &str,
        group: &UserGroup,
    ) -> Result<()> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
        let response = self.http.put(&url).json(group).send().await?;
        Self::handle_error(response, &format!("user group {group_id}")).await?;
        Ok(())
    }

    /// Deletes a user group by its identifier. Returns `()` on success (204 No Content).
    pub async fn delete_user_group(
        &self,
        data_source: Option<&str>,
        group_id: &str,
    ) -> Result<()> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!("/api/session/data/{ds}/userGroups/{group_id}"))?;
        let response = self.http.delete(&url).send().await?;
        Self::handle_error(response, &format!("user group {group_id}")).await?;
        Ok(())
    }

    /// Updates the member users of a user group using JSON Patch operations.
    pub async fn update_user_group_member_users(
        &self,
        data_source: Option<&str>,
        group_id: &str,
        patches: &[PatchOperation],
    ) -> Result<()> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/userGroups/{group_id}/memberUsers"
        ))?;
        let response = self.http.patch(&url).json(patches).send().await?;
        Self::handle_error(response, &format!("user group {group_id} member users")).await?;
        Ok(())
    }

    /// Updates the member groups of a user group using JSON Patch operations.
    pub async fn update_user_group_member_groups(
        &self,
        data_source: Option<&str>,
        group_id: &str,
        patches: &[PatchOperation],
    ) -> Result<()> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/userGroups/{group_id}/memberUserGroups"
        ))?;
        let response = self.http.patch(&url).json(patches).send().await?;
        Self::handle_error(response, &format!("user group {group_id} member groups")).await?;
        Ok(())
    }

    /// Updates the parent groups of a user group using JSON Patch operations.
    pub async fn update_user_group_parent_groups(
        &self,
        data_source: Option<&str>,
        group_id: &str,
        patches: &[PatchOperation],
    ) -> Result<()> {
        validate_user_group_id(group_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/userGroups/{group_id}/userGroups"
        ))?;
        let response = self.http.patch(&url).json(patches).send().await?;
        Self::handle_error(response, &format!("user group {group_id} parent groups")).await?;
        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn user_group_serde_roundtrip() {
        let group = UserGroup {
            identifier: Some("admins".to_string()),
            attributes: Some(HashMap::from([(
                "disabled".to_string(),
                Some("false".to_string()),
            )])),
        };
        let json = serde_json::to_string(&group).unwrap();
        let deserialized: UserGroup = serde_json::from_str(&json).unwrap();
        assert_eq!(group, deserialized);
    }

    #[test]
    fn user_group_skip_none_fields() {
        let group = UserGroup::default();
        let json = serde_json::to_value(&group).unwrap();
        let obj = json.as_object().unwrap();
        assert!(obj.is_empty());
    }

    #[test]
    fn deserialize_user_group_from_api_json() {
        let json = r#"{
            "identifier": "developers",
            "attributes": {
                "disabled": "",
                "expired": ""
            }
        }"#;
        let group: UserGroup = serde_json::from_str(json).unwrap();
        assert_eq!(group.identifier.as_deref(), Some("developers"));
        assert!(group.attributes.is_some());
    }

    #[test]
    fn deserialize_user_group_unknown_fields_ignored() {
        let json = r#"{"identifier": "admins", "unknownField": 42}"#;
        let group: UserGroup = serde_json::from_str(json).unwrap();
        assert_eq!(group.identifier.as_deref(), Some("admins"));
    }

    #[test]
    fn user_group_null_attribute_values() {
        let json = r#"{
            "identifier": "team",
            "attributes": {
                "disabled": null,
                "notes": "hello"
            }
        }"#;
        let group: UserGroup = serde_json::from_str(json).unwrap();
        let attrs = group.attributes.unwrap();
        assert_eq!(attrs.get("disabled"), Some(&None));
        assert_eq!(attrs.get("notes"), Some(&Some("hello".to_string())));
    }
}