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::validation::validate_sharing_profile_id;

/// A Guacamole sharing profile.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SharingProfile {
    /// The unique numeric identifier (as a string).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identifier: Option<String>,

    /// The identifier of the primary connection this profile shares.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub primary_connection_identifier: Option<String>,

    /// Human-readable name of the sharing profile.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,

    /// Sharing profile parameters.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub parameters: Option<HashMap<String, String>>,

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

/// A summary of a sharing profile as returned when listing profiles for a connection.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct SharingProfileSummary {
    /// The unique numeric identifier (as a string).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub identifier: Option<String>,

    /// The identifier of the primary connection this profile shares.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub primary_connection_identifier: Option<String>,

    /// Human-readable name of the sharing profile.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub name: Option<String>,
}

impl GuacamoleClient {
    /// Lists all sharing profiles for a data source.
    pub async fn list_sharing_profiles(
        &self,
        data_source: Option<&str>,
    ) -> Result<HashMap<String, SharingProfile>> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/sharingProfiles"
        ))?;
        let response = self.http.get(&url).send().await?;
        Self::parse_response(response, "sharing profiles").await
    }

    /// Retrieves a single sharing profile by its identifier.
    pub async fn get_sharing_profile(
        &self,
        data_source: Option<&str>,
        sharing_profile_id: &str,
    ) -> Result<SharingProfile> {
        validate_sharing_profile_id(sharing_profile_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/sharingProfiles/{sharing_profile_id}"
        ))?;
        let response = self.http.get(&url).send().await?;
        Self::parse_response(response, &format!("sharing profile {sharing_profile_id}"))
            .await
    }

    /// Retrieves the parameters for a sharing profile.
    pub async fn get_sharing_profile_parameters(
        &self,
        data_source: Option<&str>,
        sharing_profile_id: &str,
    ) -> Result<HashMap<String, String>> {
        validate_sharing_profile_id(sharing_profile_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/sharingProfiles/{sharing_profile_id}/parameters"
        ))?;
        let response = self.http.get(&url).send().await?;
        Self::parse_response(
            response,
            &format!("sharing profile {sharing_profile_id} parameters"),
        )
        .await
    }

    /// Creates a new sharing profile. Returns the created profile (with assigned identifier).
    pub async fn create_sharing_profile(
        &self,
        data_source: Option<&str>,
        sharing_profile: &SharingProfile,
    ) -> Result<SharingProfile> {
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/sharingProfiles"
        ))?;
        let response = self
            .http
            .post(&url)
            .json(sharing_profile)
            .send()
            .await?;
        Self::parse_response(response, "create sharing profile").await
    }

    /// Updates an existing sharing profile. Returns `()` on success (204 No Content).
    pub async fn update_sharing_profile(
        &self,
        data_source: Option<&str>,
        sharing_profile_id: &str,
        sharing_profile: &SharingProfile,
    ) -> Result<()> {
        validate_sharing_profile_id(sharing_profile_id)?;
        let ds = self.resolve_data_source(data_source)?;
        let url = self.url(&format!(
            "/api/session/data/{ds}/sharingProfiles/{sharing_profile_id}"
        ))?;
        let response = self.http.put(&url).json(sharing_profile).send().await?;
        Self::handle_error(response, &format!("sharing profile {sharing_profile_id}"))
            .await?;
        Ok(())
    }

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

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

    #[test]
    fn sharing_profile_serde_roundtrip() {
        let sp = SharingProfile {
            identifier: Some("3".to_string()),
            primary_connection_identifier: Some("1".to_string()),
            name: Some("read-only-view".to_string()),
            parameters: Some(HashMap::new()),
            attributes: Some(HashMap::new()),
        };
        let json = serde_json::to_string(&sp).unwrap();
        let deserialized: SharingProfile = serde_json::from_str(&json).unwrap();
        assert_eq!(sp, deserialized);
    }

    #[test]
    fn sharing_profile_camel_case_keys() {
        let sp = SharingProfile {
            primary_connection_identifier: Some("1".to_string()),
            ..Default::default()
        };
        let json = serde_json::to_value(&sp).unwrap();
        assert!(json.get("primaryConnectionIdentifier").is_some());
    }

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

    #[test]
    fn deserialize_sharing_profile_from_api_json() {
        let json = r#"{
            "identifier": "3",
            "primaryConnectionIdentifier": "1",
            "name": "read-only-view",
            "parameters": {},
            "attributes": {}
        }"#;
        let sp: SharingProfile = serde_json::from_str(json).unwrap();
        assert_eq!(sp.identifier.as_deref(), Some("3"));
        assert_eq!(sp.primary_connection_identifier.as_deref(), Some("1"));
        assert_eq!(sp.name.as_deref(), Some("read-only-view"));
    }

    #[test]
    fn sharing_profile_summary_serde_roundtrip() {
        let summary = SharingProfileSummary {
            identifier: Some("3".to_string()),
            primary_connection_identifier: Some("1".to_string()),
            name: Some("read-only".to_string()),
        };
        let json = serde_json::to_string(&summary).unwrap();
        let deserialized: SharingProfileSummary = serde_json::from_str(&json).unwrap();
        assert_eq!(summary, deserialized);
    }

    #[test]
    fn deserialize_sharing_profile_unknown_fields_ignored() {
        let json = r#"{"identifier": "3", "unknownField": true}"#;
        let sp: SharingProfile = serde_json::from_str(json).unwrap();
        assert_eq!(sp.identifier.as_deref(), Some("3"));
    }

    #[test]
    fn summary_camel_case_keys() {
        let summary = SharingProfileSummary {
            primary_connection_identifier: Some("1".to_string()),
            ..Default::default()
        };
        let json = serde_json::to_value(&summary).unwrap();
        assert!(json.get("primaryConnectionIdentifier").is_some());
        assert!(json.get("primary_connection_identifier").is_none());
    }

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

    #[test]
    fn summary_unknown_fields_ignored() {
        let json = r#"{"identifier": "5", "unknownField": "value"}"#;
        let summary: SharingProfileSummary = serde_json::from_str(json).unwrap();
        assert_eq!(summary.identifier.as_deref(), Some("5"));
    }
}