tandem-memory 0.4.12

Memory storage and embedding utilities for Tandem
Documentation
use serde::{Deserialize, Serialize};
use std::fmt;
use std::str::FromStr;

#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub struct ContextUri {
    pub scheme: String,
    pub segments: Vec<String>,
}

#[derive(Debug, Clone)]
pub struct ContextUriError {
    pub message: String,
}

impl fmt::Display for ContextUriError {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}", self.message)
    }
}

impl ContextUri {
    pub fn new(scheme: impl Into<String>, segments: Vec<impl Into<String>>) -> Self {
        Self {
            scheme: scheme.into(),
            segments: segments.into_iter().map(|s| s.into()).collect(),
        }
    }

    pub fn parse(uri: &str) -> Result<Self, ContextUriError> {
        if !uri.contains("://") {
            return Err(ContextUriError {
                message: format!("invalid URI format: missing '://' in '{}'", uri),
            });
        }

        let parts: Vec<&str> = uri.splitn(2, "://").collect();
        if parts.len() != 2 {
            return Err(ContextUriError {
                message: format!(
                    "invalid URI format: expected 'scheme://path', got '{}'",
                    uri
                ),
            });
        }

        let scheme = parts[0].to_lowercase();
        if scheme.is_empty() {
            return Err(ContextUriError {
                message: format!("invalid URI: empty scheme in '{}'", uri),
            });
        }

        let path = parts[1];
        let segments: Vec<String> = path
            .split('/')
            .filter(|s| !s.is_empty())
            .map(|s| s.to_string())
            .collect();

        Ok(Self { scheme, segments })
    }

    pub fn parent(&self) -> Option<ContextUri> {
        if self.segments.is_empty() {
            return None;
        }
        let mut new_segments = self.segments.clone();
        new_segments.pop();
        if new_segments.is_empty() {
            return None;
        }
        Some(ContextUri {
            scheme: self.scheme.clone(),
            segments: new_segments,
        })
    }

    pub fn last_segment(&self) -> Option<&str> {
        self.segments.last().map(|s| s.as_str())
    }

    pub fn depth(&self) -> usize {
        self.segments.len()
    }

    pub fn is_ancestor_of(&self, other: &ContextUri) -> bool {
        if self.scheme != other.scheme || self.segments.len() >= other.segments.len() {
            return false;
        }
        self.segments
            .iter()
            .zip(other.segments.iter())
            .all(|(a, b)| a == b)
    }

    pub fn join(&self, segment: impl Into<String>) -> ContextUri {
        let mut new_segments = self.segments.clone();
        new_segments.push(segment.into());
        ContextUri {
            scheme: self.scheme.clone(),
            segments: new_segments,
        }
    }

    pub fn starts_with(&self, prefix: &ContextUri) -> bool {
        if self.scheme != prefix.scheme || self.segments.len() < prefix.segments.len() {
            return false;
        }
        self.segments
            .iter()
            .zip(prefix.segments.iter())
            .all(|(a, b)| a == b)
    }
}

impl fmt::Display for ContextUri {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        write!(f, "{}://", self.scheme)?;
        for (i, segment) in self.segments.iter().enumerate() {
            if i > 0 {
                write!(f, "/")?;
            }
            write!(f, "{}", segment)?;
        }
        Ok(())
    }
}

impl FromStr for ContextUri {
    type Err = ContextUriError;
    fn from_str(s: &str) -> Result<Self, Self::Err> {
        Self::parse(s)
    }
}

pub const TANDEM_SCHEME: &str = "tandem";

pub fn resources_uri(project_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["resources", project_id])
}

pub fn user_uri(user_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["user", user_id])
}

pub fn user_memories_uri(user_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["user", user_id, "memories"])
}

pub fn agent_uri(agent_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["agent", agent_id])
}

pub fn agent_skills_uri(agent_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["agent", agent_id, "skills"])
}

pub fn session_uri(session_id: &str) -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, vec!["session", session_id])
}

pub fn root_uri() -> ContextUri {
    ContextUri::new(TANDEM_SCHEME, Vec::<String>::new())
}

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

    #[test]
    fn test_uri_parse() {
        let uri = ContextUri::parse("tandem://user/user123/memories").unwrap();
        assert_eq!(uri.scheme, "tandem");
        assert_eq!(uri.segments, vec!["user", "user123", "memories"]);
    }

    #[test]
    fn test_uri_display() {
        let uri = ContextUri::parse("tandem://resources/myproject/docs").unwrap();
        assert_eq!(uri.to_string(), "tandem://resources/myproject/docs");
    }

    #[test]
    fn test_uri_parent() {
        let uri = ContextUri::parse("tandem://user/user123/memories/prefs").unwrap();
        let parent = uri.parent().unwrap();
        assert_eq!(parent.to_string(), "tandem://user/user123/memories");
    }

    #[test]
    fn test_uri_depth() {
        let uri = ContextUri::parse("tandem://a/b/c").unwrap();
        assert_eq!(uri.depth(), 3);
    }

    #[test]
    fn test_is_ancestor_of() {
        let parent = ContextUri::parse("tandem://user/user123").unwrap();
        let child = ContextUri::parse("tandem://user/user123/memories").unwrap();
        let sibling = ContextUri::parse("tandem://user/otheruser/memories").unwrap();
        let unrelated = ContextUri::parse("tandem://agent/bot/skills").unwrap();

        assert!(parent.is_ancestor_of(&child));
        assert!(!parent.is_ancestor_of(&sibling));
        assert!(!child.is_ancestor_of(&parent));
        assert!(!parent.is_ancestor_of(&unrelated));
    }

    #[test]
    fn test_join() {
        let base = ContextUri::parse("tandem://user/user123").unwrap();
        let joined = base.join("memories");
        assert_eq!(joined.to_string(), "tandem://user/user123/memories");
    }

    #[test]
    fn test_helpers() {
        assert_eq!(
            user_memories_uri("user123").to_string(),
            "tandem://user/user123/memories"
        );
        assert_eq!(
            session_uri("sess123").to_string(),
            "tandem://session/sess123"
        );
        assert_eq!(
            agent_skills_uri("bot1").to_string(),
            "tandem://agent/bot1/skills"
        );
    }

    #[test]
    fn test_invalid_uri() {
        assert!(ContextUri::parse("invalid").is_err());
        assert!(ContextUri::parse("tandem://").is_ok());
        assert!(ContextUri::parse("tandem:///").is_ok());
    }
}