agentguard_core/
resource.rs

1//! Resources acted on: any Cedar entity (Mailbox, Repository, Document, ...).
2
3use crate::ids::ResourceId;
4use indexmap::IndexMap;
5use serde::{Deserialize, Serialize};
6use std::fmt;
7
8/// A resource being acted on. The `entity_type` is the Cedar type name
9/// (`Mailbox`, `Repository`, etc) and `uid` is the entity's ID.
10#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
11#[non_exhaustive]
12pub struct Resource {
13    pub entity_type: String,
14    pub uid: ResourceId,
15    #[serde(default)]
16    pub attrs: IndexMap<String, serde_json::Value>,
17}
18
19impl Resource {
20    /// Construct a resource with no attributes.
21    pub fn new(entity_type: impl Into<String>, uid: impl Into<ResourceId>) -> Self {
22        Self {
23            entity_type: entity_type.into(),
24            uid: uid.into(),
25            attrs: IndexMap::new(),
26        }
27    }
28
29    /// Add an attribute and return `self` for chaining.
30    pub fn with_attr(mut self, k: impl Into<String>, v: impl Into<serde_json::Value>) -> Self {
31        self.attrs.insert(k.into(), v.into());
32        self
33    }
34
35    /// Full Cedar entity UID like `Mailbox::"alice@acme"`.
36    ///
37    /// Prefer the [`Display`](std::fmt::Display) impl for formatting into
38    /// an existing buffer (avoids a `String` allocation on the hot path).
39    /// This method is retained for callers that genuinely need an owned
40    /// `String` (e.g. serialization to JSON).
41    #[deprecated(
42        since = "0.2.1",
43        note = "Use the Display impl to write into an existing buffer (no allocation)."
44    )]
45    pub fn entity_uid(&self) -> String {
46        format!("{}::\"{}\"", self.entity_type, self.uid)
47    }
48}
49
50impl fmt::Display for Resource {
51    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
52        write!(f, "{}::\"{}\"", self.entity_type, self.uid)
53    }
54}
55
56#[cfg(test)]
57mod tests {
58    use super::*;
59
60    #[test]
61    fn resource_entity_uid_uses_type_and_id() {
62        let r = Resource::new("Mailbox", "alice@acme");
63        assert_eq!(format!("{}", r), "Mailbox::\"alice@acme\"");
64    }
65
66    #[test]
67    fn resource_display_matches_entity_uid() {
68        let r = Resource::new("Document", "doc-1");
69        assert_eq!(format!("{}", r), "Document::\"doc-1\"");
70    }
71
72    #[test]
73    fn resource_serde_round_trip() {
74        let r = Resource::new("Mailbox", "alice@acme");
75        let json = serde_json::to_string(&r).unwrap();
76        let parsed: Resource = serde_json::from_str(&json).unwrap();
77        assert_eq!(parsed, r);
78    }
79
80    #[test]
81    fn resource_attrs_default_to_empty() {
82        let r = Resource::new("Mailbox", "x");
83        assert!(r.attrs.is_empty());
84    }
85
86    #[test]
87    fn resource_uid_is_typed() {
88        // The compile-time type prevents passing a PrincipalId where
89        // a ResourceId is expected.
90        let r = Resource::new("Mailbox", "alice@acme");
91        let _uid: &ResourceId = &r.uid;
92        // &str also works (ResourceId: From<&str>).
93        let r2 = Resource::new("Mailbox", "alice@acme");
94        assert_eq!(&*r2.uid, "alice@acme");
95    }
96}