use std::collections::HashMap;
use serde::{Deserialize, Serialize};
use crate::client::GuacamoleClient;
use crate::error::Result;
use crate::history::HistoryEntry;
use crate::patch::PatchOperation;
use crate::sharing_profile::SharingProfileSummary;
use crate::validation::validate_connection_id;
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct Connection {
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parent_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub protocol: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub parameters: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub attributes: Option<HashMap<String, String>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub active_connections: Option<i32>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[non_exhaustive]
pub struct ActiveConnection {
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub connection_identifier: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub start_date: Option<i64>,
#[serde(skip_serializing_if = "Option::is_none")]
pub remote_host: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub username: Option<String>,
}
impl GuacamoleClient {
pub async fn list_connections(
&self,
data_source: Option<&str>,
) -> Result<HashMap<String, Connection>> {
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!("/api/session/data/{ds}/connections"))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(response, "connections").await
}
pub async fn get_connection(
&self,
data_source: Option<&str>,
connection_id: &str,
) -> Result<Connection> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}"
))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(response, &format!("connection {connection_id}")).await
}
pub async fn get_connection_parameters(
&self,
data_source: Option<&str>,
connection_id: &str,
) -> Result<HashMap<String, String>> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}/parameters"
))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(response, &format!("connection {connection_id} parameters"))
.await
}
pub async fn get_connection_history(
&self,
data_source: Option<&str>,
connection_id: &str,
) -> Result<Vec<HistoryEntry>> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}/history"
))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(response, &format!("connection {connection_id} history"))
.await
}
pub async fn get_connection_sharing_profiles(
&self,
data_source: Option<&str>,
connection_id: &str,
) -> Result<HashMap<String, SharingProfileSummary>> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}/sharingProfiles"
))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(
response,
&format!("connection {connection_id} sharing profiles"),
)
.await
}
pub async fn create_connection(
&self,
data_source: Option<&str>,
connection: &Connection,
) -> Result<Connection> {
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!("/api/session/data/{ds}/connections"))?;
let response = self.http.post(&url).json(connection).send().await?;
Self::parse_response(response, "create connection").await
}
pub async fn update_connection(
&self,
data_source: Option<&str>,
connection_id: &str,
connection: &Connection,
) -> Result<()> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}"
))?;
let response = self.http.put(&url).json(connection).send().await?;
Self::handle_error(response, &format!("connection {connection_id}")).await?;
Ok(())
}
pub async fn delete_connection(
&self,
data_source: Option<&str>,
connection_id: &str,
) -> Result<()> {
validate_connection_id(connection_id)?;
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/connections/{connection_id}"
))?;
let response = self.http.delete(&url).send().await?;
Self::handle_error(response, &format!("connection {connection_id}")).await?;
Ok(())
}
pub async fn list_active_connections(
&self,
data_source: Option<&str>,
) -> Result<HashMap<String, ActiveConnection>> {
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/activeConnections"
))?;
let response = self.http.get(&url).send().await?;
Self::parse_response(response, "active connections").await
}
pub async fn kill_connections(
&self,
data_source: Option<&str>,
patches: &[PatchOperation],
) -> Result<()> {
let ds = self.resolve_data_source(data_source)?;
let url = self.url(&format!(
"/api/session/data/{ds}/activeConnections"
))?;
let response = self.http.patch(&url).json(patches).send().await?;
Self::handle_error(response, "kill connections").await?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn connection_serde_roundtrip() {
let conn = Connection {
identifier: Some("1".to_string()),
parent_identifier: Some("ROOT".to_string()),
name: Some("my-server".to_string()),
protocol: Some("ssh".to_string()),
parameters: Some(HashMap::from([
("hostname".to_string(), "10.0.0.5".to_string()),
("port".to_string(), "22".to_string()),
])),
attributes: Some(HashMap::new()),
active_connections: Some(0),
};
let json = serde_json::to_string(&conn).unwrap();
let deserialized: Connection = serde_json::from_str(&json).unwrap();
assert_eq!(conn, deserialized);
}
#[test]
fn connection_camel_case_keys() {
let conn = Connection {
parent_identifier: Some("ROOT".to_string()),
active_connections: Some(2),
..Default::default()
};
let json = serde_json::to_value(&conn).unwrap();
assert!(json.get("parentIdentifier").is_some());
assert!(json.get("activeConnections").is_some());
}
#[test]
fn connection_skip_none_fields() {
let conn = Connection::default();
let json = serde_json::to_value(&conn).unwrap();
let obj = json.as_object().unwrap();
assert!(obj.is_empty());
}
#[test]
fn deserialize_connection_from_api_json() {
let json = r#"{
"identifier": "1",
"parentIdentifier": "ROOT",
"name": "my-server",
"protocol": "ssh",
"parameters": {},
"attributes": {},
"activeConnections": 0
}"#;
let conn: Connection = serde_json::from_str(json).unwrap();
assert_eq!(conn.identifier.as_deref(), Some("1"));
assert_eq!(conn.parent_identifier.as_deref(), Some("ROOT"));
assert_eq!(conn.protocol.as_deref(), Some("ssh"));
assert_eq!(conn.active_connections, Some(0));
}
#[test]
fn active_connection_serde_roundtrip() {
let ac = ActiveConnection {
identifier: Some("abc-123".to_string()),
connection_identifier: Some("1".to_string()),
start_date: Some(1_700_000_000_000),
remote_host: Some("10.0.0.1".to_string()),
username: Some("admin".to_string()),
};
let json = serde_json::to_string(&ac).unwrap();
let deserialized: ActiveConnection = serde_json::from_str(&json).unwrap();
assert_eq!(ac, deserialized);
}
#[test]
fn active_connection_camel_case_keys() {
let ac = ActiveConnection {
connection_identifier: Some("1".to_string()),
start_date: Some(1),
remote_host: Some("host".to_string()),
..Default::default()
};
let json = serde_json::to_value(&ac).unwrap();
assert!(json.get("connectionIdentifier").is_some());
assert!(json.get("startDate").is_some());
assert!(json.get("remoteHost").is_some());
}
#[test]
fn active_connection_skip_none_fields() {
let ac = ActiveConnection::default();
let json = serde_json::to_value(&ac).unwrap();
let obj = json.as_object().unwrap();
assert!(obj.is_empty());
}
#[test]
fn deserialize_connection_unknown_fields_ignored() {
let json = r#"{"identifier": "1", "unknownField": "value"}"#;
let conn: Connection = serde_json::from_str(json).unwrap();
assert_eq!(conn.identifier.as_deref(), Some("1"));
}
#[test]
fn deserialize_active_connection_from_api_json() {
let json = r#"{
"identifier": "abc-def-123",
"connectionIdentifier": "42",
"startDate": 1700000000000,
"remoteHost": "192.168.1.100",
"username": "guacadmin"
}"#;
let ac: ActiveConnection = serde_json::from_str(json).unwrap();
assert_eq!(ac.identifier.as_deref(), Some("abc-def-123"));
assert_eq!(ac.connection_identifier.as_deref(), Some("42"));
assert_eq!(ac.start_date, Some(1_700_000_000_000));
assert_eq!(ac.remote_host.as_deref(), Some("192.168.1.100"));
assert_eq!(ac.username.as_deref(), Some("guacadmin"));
}
#[test]
fn active_connection_unknown_fields_ignored() {
let json = r#"{"identifier": "abc", "unknownField": 99}"#;
let ac: ActiveConnection = serde_json::from_str(json).unwrap();
assert_eq!(ac.identifier.as_deref(), Some("abc"));
}
}