tandem_memory/
context_uri.rs1use 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}