guacamole-client 0.5.1

Rust client library for the Guacamole REST API
Documentation
use serde::{Deserialize, Serialize};
use serde_json::Value;

/// A single JSON Patch operation for Guacamole PATCH endpoints.
#[derive(Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[non_exhaustive]
pub struct PatchOperation {
    /// The operation type (`"add"` or `"remove"`).
    #[serde(skip_serializing_if = "Option::is_none")]
    pub op: Option<String>,

    /// The JSON Pointer path for this operation.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub path: Option<String>,

    /// The value for `add` operations; omitted for `remove`.
    ///
    /// This is a [`serde_json::Value`] to support any JSON type (strings,
    /// booleans, numbers, objects) as required by RFC 6902.
    #[serde(skip_serializing_if = "Option::is_none")]
    pub value: Option<Value>,
}

impl PatchOperation {
    /// Creates an `add` patch operation.
    #[must_use]
    pub fn add(path: impl Into<String>, value: impl Into<Value>) -> Self {
        Self {
            op: Some("add".to_owned()),
            path: Some(path.into()),
            value: Some(value.into()),
        }
    }

    /// Creates a `remove` patch operation.
    #[must_use]
    pub fn remove(path: impl Into<String>) -> Self {
        Self {
            op: Some("remove".to_owned()),
            path: Some(path.into()),
            value: None,
        }
    }
}

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

    #[test]
    fn add_operation_serde_roundtrip() {
        let op = PatchOperation::add("/connectionPermissions/1", "READ");
        let json = serde_json::to_string(&op).unwrap();
        let deserialized: PatchOperation = serde_json::from_str(&json).unwrap();
        assert_eq!(op, deserialized);
    }

    #[test]
    fn remove_operation_serde_roundtrip() {
        let op = PatchOperation::remove("/connectionPermissions/1");
        let json = serde_json::to_string(&op).unwrap();
        let deserialized: PatchOperation = serde_json::from_str(&json).unwrap();
        assert_eq!(op, deserialized);
    }

    #[test]
    fn remove_operation_omits_value() {
        let op = PatchOperation::remove("/path");
        let json = serde_json::to_value(&op).unwrap();
        assert!(json.get("value").is_none());
    }

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

    #[test]
    fn add_operation_field_values() {
        let op = PatchOperation::add("/connectionPermissions/1", "READ");
        assert_eq!(op.op.as_deref(), Some("add"));
        assert_eq!(op.path.as_deref(), Some("/connectionPermissions/1"));
        assert_eq!(op.value, Some(Value::String("READ".to_string())));
    }

    #[test]
    fn add_operation_non_string_values() {
        let bool_op = PatchOperation::add("/path", true);
        assert_eq!(bool_op.value, Some(Value::Bool(true)));

        let num_op = PatchOperation::add("/path", 42);
        assert_eq!(num_op.value, Some(serde_json::json!(42)));

        let obj_op = PatchOperation::add("/path", serde_json::json!({"key": "val"}));
        assert!(obj_op.value.as_ref().unwrap().is_object());
    }

    #[test]
    fn add_operation_array_and_null_values() {
        let arr_op = PatchOperation::add("/path", serde_json::json!(["a", "b"]));
        assert_eq!(arr_op.value, Some(serde_json::json!(["a", "b"])));

        let null_op = PatchOperation::add("/path", serde_json::Value::Null);
        assert_eq!(null_op.value, Some(serde_json::Value::Null));
    }

    #[test]
    fn remove_operation_field_values() {
        let op = PatchOperation::remove("/connectionPermissions/1");
        assert_eq!(op.op.as_deref(), Some("remove"));
        assert_eq!(op.path.as_deref(), Some("/connectionPermissions/1"));
        assert!(op.value.is_none());
    }
}