claude_agent/common/
content_source.rs1use std::path::PathBuf;
8
9use serde::{Deserialize, Serialize};
10
11#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
17#[serde(tag = "type", rename_all = "snake_case")]
18pub enum ContentSource {
19 File {
21 path: PathBuf,
23 },
24
25 InMemory {
27 content: String,
29 },
30
31 Database {
33 id: String,
35 },
36
37 Http {
39 url: String,
41 },
42}
43
44impl ContentSource {
45 pub fn file(path: impl Into<PathBuf>) -> Self {
47 Self::File { path: path.into() }
48 }
49
50 pub fn in_memory(content: impl Into<String>) -> Self {
52 Self::InMemory {
53 content: content.into(),
54 }
55 }
56
57 pub fn database(id: impl Into<String>) -> Self {
59 Self::Database { id: id.into() }
60 }
61
62 pub fn http(url: impl Into<String>) -> Self {
64 Self::Http { url: url.into() }
65 }
66
67 pub async fn load(&self) -> crate::Result<String> {
72 match self {
73 Self::File { path } => tokio::fs::read_to_string(path).await.map_err(|e| {
74 crate::Error::Config(format!("Failed to load content from {:?}: {}", path, e))
75 }),
76 Self::InMemory { content } => Ok(content.clone()),
77 Self::Database { id } => Err(crate::Error::Config(format!(
78 "Database content source not implemented: {}",
79 id
80 ))),
81 Self::Http { url } => Err(crate::Error::Config(format!(
82 "HTTP content source not implemented: {}",
83 url
84 ))),
85 }
86 }
87
88 pub fn is_in_memory(&self) -> bool {
90 matches!(self, Self::InMemory { .. })
91 }
92
93 pub fn is_file(&self) -> bool {
95 matches!(self, Self::File { .. })
96 }
97
98 pub fn as_file_path(&self) -> Option<&PathBuf> {
100 match self {
101 Self::File { path } => Some(path),
102 _ => None,
103 }
104 }
105
106 pub fn base_dir(&self) -> Option<PathBuf> {
108 match self {
109 Self::File { path } => path.parent().map(|p| p.to_path_buf()),
110 _ => None,
111 }
112 }
113}
114
115impl Default for ContentSource {
116 fn default() -> Self {
117 Self::InMemory {
118 content: String::new(),
119 }
120 }
121}
122
123#[cfg(test)]
124mod tests {
125 use super::*;
126
127 #[test]
128 fn test_content_source_constructors() {
129 let file = ContentSource::file("/path/to/file.md");
130 assert!(file.is_file());
131 assert_eq!(
132 file.as_file_path(),
133 Some(&PathBuf::from("/path/to/file.md"))
134 );
135
136 let memory = ContentSource::in_memory("content here");
137 assert!(memory.is_in_memory());
138
139 let db = ContentSource::database("skill-123");
140 assert!(matches!(db, ContentSource::Database { id } if id == "skill-123"));
141
142 let http = ContentSource::http("https://example.com/skill.md");
143 assert!(
144 matches!(http, ContentSource::Http { url } if url == "https://example.com/skill.md")
145 );
146 }
147
148 #[test]
149 fn test_base_dir() {
150 let file = ContentSource::file("/home/user/.claude/skills/commit/SKILL.md");
151 assert_eq!(
152 file.base_dir(),
153 Some(PathBuf::from("/home/user/.claude/skills/commit"))
154 );
155
156 let memory = ContentSource::in_memory("content");
157 assert_eq!(memory.base_dir(), None);
158 }
159
160 #[tokio::test]
161 async fn test_load_in_memory() {
162 let source = ContentSource::in_memory("test content");
163 let content = source.load().await.unwrap();
164 assert_eq!(content, "test content");
165 }
166
167 #[tokio::test]
168 async fn test_load_file() {
169 use std::io::Write;
170 use tempfile::NamedTempFile;
171
172 let mut file = NamedTempFile::new().unwrap();
173 writeln!(file, "file content").unwrap();
174
175 let source = ContentSource::file(file.path());
176 let content = source.load().await.unwrap();
177 assert!(content.contains("file content"));
178 }
179
180 #[tokio::test]
181 async fn test_load_file_not_found() {
182 let source = ContentSource::file("/nonexistent/path/file.md");
183 let result = source.load().await;
184 assert!(result.is_err());
185 }
186
187 #[test]
188 fn test_serde_roundtrip() {
189 let sources = vec![
190 ContentSource::file("/path/to/file.md"),
191 ContentSource::in_memory("content"),
192 ContentSource::database("id-123"),
193 ContentSource::http("https://example.com"),
194 ];
195
196 for source in sources {
197 let json = serde_json::to_string(&source).unwrap();
198 let parsed: ContentSource = serde_json::from_str(&json).unwrap();
199 assert_eq!(source, parsed);
200 }
201 }
202}