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;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct UserGroup {
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, Option<String>>>,
}
impl GuacamoleClient {
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
}
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
}
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(())
}
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(())
}
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(())
}
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(())
}
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(())
}
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())));
}
}