Skip to main content

tandem_memory/
context_uri.rs

1use serde::{Deserialize, Serialize};
2use std::fmt;
3use std::str::FromStr;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
6pub struct ContextUri {
7    pub scheme: String,
8    pub segments: Vec<String>,
9}
10
11#[derive(Debug, Clone)]
12pub struct ContextUriError {
13    pub message: String,
14}
15
16impl fmt::Display for ContextUriError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        write!(f, "{}", self.message)
19    }
20}
21
22impl ContextUri {
23    pub fn new(scheme: impl Into<String>, segments: Vec<impl Into<String>>) -> Self {
24        Self {
25            scheme: scheme.into(),
26            segments: segments.into_iter().map(|s| s.into()).collect(),
27        }
28    }
29
30    pub fn parse(uri: &str) -> Result<Self, ContextUriError> {
31        if !uri.contains("://") {
32            return Err(ContextUriError {
33                message: format!("invalid URI format: missing '://' in '{}'", uri),
34            });
35        }
36
37        let parts: Vec<&str> = uri.splitn(2, "://").collect();
38        if parts.len() != 2 {
39            return Err(ContextUriError {
40                message: format!(
41                    "invalid URI format: expected 'scheme://path', got '{}'",
42                    uri
43                ),
44            });
45        }
46
47        let scheme = parts[0].to_lowercase();
48        if scheme.is_empty() {
49            return Err(ContextUriError {
50                message: format!("invalid URI: empty scheme in '{}'", uri),
51            });
52        }
53
54        let path = parts[1];
55        let segments: Vec<String> = path
56            .split('/')
57            .filter(|s| !s.is_empty())
58            .map(|s| s.to_string())
59            .collect();
60
61        Ok(Self { scheme, segments })
62    }
63
64    pub fn parent(&self) -> Option<ContextUri> {
65        if self.segments.is_empty() {
66            return None;
67        }
68        let mut new_segments = self.segments.clone();
69        new_segments.pop();
70        if new_segments.is_empty() {
71            return None;
72        }
73        Some(ContextUri {
74            scheme: self.scheme.clone(),
75            segments: new_segments,
76        })
77    }
78
79    pub fn last_segment(&self) -> Option<&str> {
80        self.segments.last().map(|s| s.as_str())
81    }
82
83    pub fn depth(&self) -> usize {
84        self.segments.len()
85    }
86
87    pub fn is_ancestor_of(&self, other: &ContextUri) -> bool {
88        if self.scheme != other.scheme || self.segments.len() >= other.segments.len() {
89            return false;
90        }
91        self.segments
92            .iter()
93            .zip(other.segments.iter())
94            .all(|(a, b)| a == b)
95    }
96
97    pub fn join(&self, segment: impl Into<String>) -> ContextUri {
98        let mut new_segments = self.segments.clone();
99        new_segments.push(segment.into());
100        ContextUri {
101            scheme: self.scheme.clone(),
102            segments: new_segments,
103        }
104    }
105
106    pub fn starts_with(&self, prefix: &ContextUri) -> bool {
107        if self.scheme != prefix.scheme || self.segments.len() < prefix.segments.len() {
108            return false;
109        }
110        self.segments
111            .iter()
112            .zip(prefix.segments.iter())
113            .all(|(a, b)| a == b)
114    }
115}
116
117impl fmt::Display for ContextUri {
118    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
119        write!(f, "{}://", self.scheme)?;
120        for (i, segment) in self.segments.iter().enumerate() {
121            if i > 0 {
122                write!(f, "/")?;
123            }
124            write!(f, "{}", segment)?;
125        }
126        Ok(())
127    }
128}
129
130impl FromStr for ContextUri {
131    type Err = ContextUriError;
132    fn from_str(s: &str) -> Result<Self, Self::Err> {
133        Self::parse(s)
134    }
135}
136
137pub const TANDEM_SCHEME: &str = "tandem";
138
139pub fn resources_uri(project_id: &str) -> ContextUri {
140    ContextUri::new(TANDEM_SCHEME, vec!["resources", project_id])
141}
142
143pub fn user_uri(user_id: &str) -> ContextUri {
144    ContextUri::new(TANDEM_SCHEME, vec!["user", user_id])
145}
146
147pub fn user_memories_uri(user_id: &str) -> ContextUri {
148    ContextUri::new(TANDEM_SCHEME, vec!["user", user_id, "memories"])
149}
150
151pub fn agent_uri(agent_id: &str) -> ContextUri {
152    ContextUri::new(TANDEM_SCHEME, vec!["agent", agent_id])
153}
154
155pub fn agent_skills_uri(agent_id: &str) -> ContextUri {
156    ContextUri::new(TANDEM_SCHEME, vec!["agent", agent_id, "skills"])
157}
158
159pub fn session_uri(session_id: &str) -> ContextUri {
160    ContextUri::new(TANDEM_SCHEME, vec!["session", session_id])
161}
162
163pub fn root_uri() -> ContextUri {
164    ContextUri::new(TANDEM_SCHEME, Vec::<String>::new())
165}
166
167#[cfg(test)]
168mod tests {
169    use super::*;
170
171    #[test]
172    fn test_uri_parse() {
173        let uri = ContextUri::parse("tandem://user/user123/memories").unwrap();
174        assert_eq!(uri.scheme, "tandem");
175        assert_eq!(uri.segments, vec!["user", "user123", "memories"]);
176    }
177
178    #[test]
179    fn test_uri_display() {
180        let uri = ContextUri::parse("tandem://resources/myproject/docs").unwrap();
181        assert_eq!(uri.to_string(), "tandem://resources/myproject/docs");
182    }
183
184    #[test]
185    fn test_uri_parent() {
186        let uri = ContextUri::parse("tandem://user/user123/memories/prefs").unwrap();
187        let parent = uri.parent().unwrap();
188        assert_eq!(parent.to_string(), "tandem://user/user123/memories");
189    }
190
191    #[test]
192    fn test_uri_depth() {
193        let uri = ContextUri::parse("tandem://a/b/c").unwrap();
194        assert_eq!(uri.depth(), 3);
195    }
196
197    #[test]
198    fn test_is_ancestor_of() {
199        let parent = ContextUri::parse("tandem://user/user123").unwrap();
200        let child = ContextUri::parse("tandem://user/user123/memories").unwrap();
201        let sibling = ContextUri::parse("tandem://user/otheruser/memories").unwrap();
202        let unrelated = ContextUri::parse("tandem://agent/bot/skills").unwrap();
203
204        assert!(parent.is_ancestor_of(&child));
205        assert!(!parent.is_ancestor_of(&sibling));
206        assert!(!child.is_ancestor_of(&parent));
207        assert!(!parent.is_ancestor_of(&unrelated));
208    }
209
210    #[test]
211    fn test_join() {
212        let base = ContextUri::parse("tandem://user/user123").unwrap();
213        let joined = base.join("memories");
214        assert_eq!(joined.to_string(), "tandem://user/user123/memories");
215    }
216
217    #[test]
218    fn test_helpers() {
219        assert_eq!(
220            user_memories_uri("user123").to_string(),
221            "tandem://user/user123/memories"
222        );
223        assert_eq!(
224            session_uri("sess123").to_string(),
225            "tandem://session/sess123"
226        );
227        assert_eq!(
228            agent_skills_uri("bot1").to_string(),
229            "tandem://agent/bot1/skills"
230        );
231    }
232
233    #[test]
234    fn test_invalid_uri() {
235        assert!(ContextUri::parse("invalid").is_err());
236        assert!(ContextUri::parse("tandem://").is_ok());
237        assert!(ContextUri::parse("tandem:///").is_ok());
238    }
239}