Skip to main content

guacamole_client/
connection_group.rs

1use std::collections::HashMap;
2
3use serde::{Deserialize, Serialize};
4use serde_json::Value;
5
6use crate::client::GuacamoleClient;
7use crate::error::Result;
8use crate::validation::{validate_connection_group_id, validate_query_param};
9
10/// A Guacamole connection group.
11#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
12#[serde(rename_all = "camelCase")]
13#[non_exhaustive]
14pub struct ConnectionGroup {
15    /// The unique identifier (numeric string or `"ROOT"`).
16    #[serde(skip_serializing_if = "Option::is_none")]
17    pub identifier: Option<String>,
18
19    /// Identifier of the parent connection group.
20    #[serde(skip_serializing_if = "Option::is_none")]
21    pub parent_identifier: Option<String>,
22
23    /// Human-readable name of the connection group.
24    #[serde(skip_serializing_if = "Option::is_none")]
25    pub name: Option<String>,
26
27    /// The group type: `"ORGANIZATIONAL"` or `"BALANCING"`.
28    #[serde(rename = "type", skip_serializing_if = "Option::is_none")]
29    pub type_: Option<String>,
30
31    /// Arbitrary connection group attributes.
32    #[serde(skip_serializing_if = "Option::is_none")]
33    pub attributes: Option<HashMap<String, String>>,
34
35    /// Number of active connections in this group.
36    #[serde(skip_serializing_if = "Option::is_none")]
37    pub active_connections: Option<i32>,
38}
39
40impl GuacamoleClient {
41    /// Lists all connection groups in the given data source.
42    pub async fn list_connection_groups(
43        &self,
44        data_source: Option<&str>,
45    ) -> Result<HashMap<String, ConnectionGroup>> {
46        let ds = self.resolve_data_source(data_source)?;
47        let url = self.url(&format!("/api/session/data/{ds}/connectionGroups"))?;
48        let response = self.http.get(&url).send().await?;
49        Self::parse_response(response, "connection groups").await
50    }
51
52    /// Retrieves a single connection group by its identifier.
53    pub async fn get_connection_group(
54        &self,
55        data_source: Option<&str>,
56        group_id: &str,
57    ) -> Result<ConnectionGroup> {
58        validate_connection_group_id(group_id)?;
59        let ds = self.resolve_data_source(data_source)?;
60        let url = self.url(&format!(
61            "/api/session/data/{ds}/connectionGroups/{group_id}"
62        ))?;
63        let response = self.http.get(&url).send().await?;
64        Self::parse_response(response, &format!("connection group {group_id}")).await
65    }
66
67    /// Retrieves the connection group tree starting at the given group.
68    ///
69    /// Returns `Value` since the tree is a recursive structure.
70    /// Optionally filters by `permission` (e.g. `"READ"`).
71    pub async fn get_connection_group_tree(
72        &self,
73        data_source: Option<&str>,
74        group_id: &str,
75        permission: Option<&str>,
76    ) -> Result<Value> {
77        validate_connection_group_id(group_id)?;
78        let ds = self.resolve_data_source(data_source)?;
79        let url = self.url(&format!(
80            "/api/session/data/{ds}/connectionGroups/{group_id}/tree"
81        ))?;
82
83        let mut request = self.http.get(&url);
84        if let Some(p) = permission {
85            validate_query_param("permission", p)?;
86            request = request.query(&[("permission", p)]);
87        }
88
89        let response = request.send().await?;
90        Self::parse_response(response, &format!("connection group {group_id} tree")).await
91    }
92
93    /// Creates a new connection group. Returns the created group (with assigned identifier).
94    pub async fn create_connection_group(
95        &self,
96        data_source: Option<&str>,
97        group: &ConnectionGroup,
98    ) -> Result<ConnectionGroup> {
99        let ds = self.resolve_data_source(data_source)?;
100        let url = self.url(&format!("/api/session/data/{ds}/connectionGroups"))?;
101        let response = self.http.post(&url).json(group).send().await?;
102        Self::parse_response(response, "create connection group").await
103    }
104
105    /// Updates an existing connection group. Returns `()` on success (204 No Content).
106    pub async fn update_connection_group(
107        &self,
108        data_source: Option<&str>,
109        group_id: &str,
110        group: &ConnectionGroup,
111    ) -> Result<()> {
112        validate_connection_group_id(group_id)?;
113        let ds = self.resolve_data_source(data_source)?;
114        let url = self.url(&format!(
115            "/api/session/data/{ds}/connectionGroups/{group_id}"
116        ))?;
117        let response = self.http.put(&url).json(group).send().await?;
118        Self::handle_error(response, &format!("connection group {group_id}")).await?;
119        Ok(())
120    }
121
122    /// Deletes a connection group by its identifier. Returns `()` on success (204 No Content).
123    pub async fn delete_connection_group(
124        &self,
125        data_source: Option<&str>,
126        group_id: &str,
127    ) -> Result<()> {
128        validate_connection_group_id(group_id)?;
129        let ds = self.resolve_data_source(data_source)?;
130        let url = self.url(&format!(
131            "/api/session/data/{ds}/connectionGroups/{group_id}"
132        ))?;
133        let response = self.http.delete(&url).send().await?;
134        Self::handle_error(response, &format!("connection group {group_id}")).await?;
135        Ok(())
136    }
137}
138
139#[cfg(test)]
140mod tests {
141    use super::*;
142
143    #[test]
144    fn connection_group_serde_roundtrip() {
145        let group = ConnectionGroup {
146            identifier: Some("1".to_string()),
147            parent_identifier: Some("ROOT".to_string()),
148            name: Some("Servers".to_string()),
149            type_: Some("ORGANIZATIONAL".to_string()),
150            attributes: Some(HashMap::new()),
151            active_connections: Some(0),
152        };
153        let json = serde_json::to_string(&group).unwrap();
154        let deserialized: ConnectionGroup = serde_json::from_str(&json).unwrap();
155        assert_eq!(group, deserialized);
156    }
157
158    #[test]
159    fn connection_group_camel_case_keys() {
160        let group = ConnectionGroup {
161            parent_identifier: Some("ROOT".to_string()),
162            active_connections: Some(2),
163            ..Default::default()
164        };
165        let json = serde_json::to_value(&group).unwrap();
166        assert!(json.get("parentIdentifier").is_some());
167        assert!(json.get("activeConnections").is_some());
168    }
169
170    #[test]
171    fn connection_group_type_field_rename() {
172        let group = ConnectionGroup {
173            type_: Some("BALANCING".to_string()),
174            ..Default::default()
175        };
176        let json = serde_json::to_value(&group).unwrap();
177        assert!(json.get("type").is_some());
178        assert_eq!(json.get("type").unwrap(), "BALANCING");
179        assert!(json.get("type_").is_none(), "Rust field name must not leak into JSON");
180    }
181
182    #[test]
183    fn connection_group_skip_none_fields() {
184        let group = ConnectionGroup::default();
185        let json = serde_json::to_value(&group).unwrap();
186        let obj = json.as_object().unwrap();
187        assert!(obj.is_empty());
188    }
189
190    #[test]
191    fn deserialize_connection_group_from_api_json() {
192        let json = r#"{
193            "identifier": "1",
194            "parentIdentifier": "ROOT",
195            "name": "Servers",
196            "type": "ORGANIZATIONAL",
197            "attributes": {},
198            "activeConnections": 3
199        }"#;
200        let group: ConnectionGroup = serde_json::from_str(json).unwrap();
201        assert_eq!(group.identifier.as_deref(), Some("1"));
202        assert_eq!(group.parent_identifier.as_deref(), Some("ROOT"));
203        assert_eq!(group.name.as_deref(), Some("Servers"));
204        assert_eq!(group.type_.as_deref(), Some("ORGANIZATIONAL"));
205        assert_eq!(group.active_connections, Some(3));
206    }
207
208    #[test]
209    fn deserialize_connection_group_unknown_fields_ignored() {
210        let json = r#"{"identifier": "1", "unknownField": true}"#;
211        let group: ConnectionGroup = serde_json::from_str(json).unwrap();
212        assert_eq!(group.identifier.as_deref(), Some("1"));
213    }
214}