treetop-client 0.0.1

Typed async Rust client for Treetop policy authorization servers
Documentation
//! Action type for Cedar authorization requests.

use serde::{Deserialize, Serialize};

use super::validation::{EntityId, Namespace, ValidationError};

/// A Cedar action identifier with optional namespace.
///
/// Represents the action being performed in an authorization request
/// (e.g. `"view"`, `"delete"`, `"create_host"`).
///
/// # Wire format
/// ```json
/// { "id": "view", "namespace": ["Admin"] }
/// ```
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)]
pub struct Action {
    /// The action identifier (e.g. `"view"`, `"delete"`).
    id: EntityId,
    /// Optional Cedar namespace path (e.g. `["Admin", "Core"]`).
    #[serde(default)]
    namespace: Namespace,
}

impl Action {
    /// Creates a new action with no namespace.
    pub fn new(id: impl Into<String>) -> Self {
        Self {
            id: EntityId::new(id),
            namespace: Namespace::default(),
        }
    }

    /// Creates and validates a new action with no namespace.
    pub fn try_new(id: impl Into<String>) -> Result<Self, ValidationError> {
        let action = Self::new(id);
        action.validate()?;
        Ok(action)
    }

    /// Sets the Cedar namespace for this action.
    pub fn with_namespace(mut self, namespace: Vec<String>) -> Self {
        self.namespace = Namespace::new(namespace);
        self
    }

    /// Sets and validates the Cedar namespace for this action.
    pub fn try_with_namespace(self, namespace: Vec<String>) -> Result<Self, ValidationError> {
        let action = self.with_namespace(namespace);
        action.validate()?;
        Ok(action)
    }

    /// Returns the action entity identifier.
    pub fn id(&self) -> &str {
        self.id.as_str()
    }

    /// Returns the Cedar namespace path.
    pub fn namespace(&self) -> &[String] {
        self.namespace.as_slice()
    }

    /// Validates this action against Cedar and Treetop request invariants.
    pub fn validate(&self) -> Result<(), ValidationError> {
        self.id.validate("action.id")?;
        self.namespace.validate("action.namespace")
    }
}

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

    #[test]
    fn action_serialization() {
        let action = Action::new("create");
        let json = serde_json::to_value(&action).unwrap();
        assert_eq!(json["id"], "create");
        assert_eq!(json["namespace"], serde_json::json!([]));
    }

    #[test]
    fn action_with_namespace() {
        let action = Action::new("delete").with_namespace(vec!["Admin".to_string()]);
        let json = serde_json::to_value(&action).unwrap();
        assert_eq!(json["namespace"], serde_json::json!(["Admin"]));
    }

    #[test]
    fn action_roundtrip() {
        let action =
            Action::new("view").with_namespace(vec!["App".to_string(), "Core".to_string()]);
        let json = serde_json::to_value(&action).unwrap();
        let deserialized: Action = serde_json::from_value(json).unwrap();
        assert_eq!(action, deserialized);
    }
}