1use anyhow::{anyhow, Result};
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use url::Url;
6
7use crate::content::Annotations;
8
9const EPSILON: f32 = 1e-6; #[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
13#[serde(rename_all = "camelCase")]
14pub struct Resource {
15 pub uri: String,
17 pub name: String,
19 #[serde(skip_serializing_if = "Option::is_none")]
21 pub description: Option<String>,
22 #[serde(default = "default_mime_type")]
24 pub mime_type: String,
25 #[serde(skip_serializing_if = "Option::is_none")]
26 pub annotations: Option<Annotations>,
27}
28
29#[derive(Debug, Serialize, Deserialize, Clone, PartialEq)]
30#[serde(rename_all = "camelCase", untagged)]
31pub enum ResourceContents {
32 TextResourceContents {
33 uri: String,
34 #[serde(skip_serializing_if = "Option::is_none")]
35 mime_type: Option<String>,
36 text: String,
37 },
38 BlobResourceContents {
39 uri: String,
40 #[serde(skip_serializing_if = "Option::is_none")]
41 mime_type: Option<String>,
42 blob: String,
43 },
44}
45
46fn default_mime_type() -> String {
47 "text".to_string()
48}
49
50impl Resource {
51 pub fn new<S: AsRef<str>>(
53 uri: S,
54 mime_type: Option<String>,
55 name: Option<String>,
56 ) -> Result<Self> {
57 let uri = uri.as_ref();
58 let url = Url::parse(uri).map_err(|e| anyhow!("Invalid URI: {}", e))?;
59
60 let name = match name {
63 Some(n) => n,
64 None => url
65 .path_segments()
66 .and_then(|segments| segments.last())
67 .unwrap_or("unnamed")
68 .to_string(),
69 };
70
71 let mime_type = match mime_type {
73 Some(t) if t == "text" || t == "blob" => t,
74 _ => default_mime_type(),
75 };
76
77 Ok(Self {
78 uri: uri.to_string(),
79 name,
80 description: None,
81 mime_type,
82 annotations: Some(Annotations::for_resource(0.0, Utc::now())),
83 })
84 }
85
86 pub fn with_uri<S: Into<String>>(
88 uri: S,
89 name: S,
90 priority: f32,
91 mime_type: Option<String>,
92 ) -> Result<Self> {
93 let uri_string = uri.into();
94 Url::parse(&uri_string).map_err(|e| anyhow!("Invalid URI: {}", e))?;
95
96 let mime_type = match mime_type {
98 Some(t) if t == "text" || t == "blob" => t,
99 _ => default_mime_type(),
100 };
101
102 Ok(Self {
103 uri: uri_string,
104 name: name.into(),
105 description: None,
106 mime_type,
107 annotations: Some(Annotations::for_resource(priority, Utc::now())),
108 })
109 }
110
111 pub fn update_timestamp(&mut self) {
113 self.annotations.as_mut().unwrap().timestamp = Some(Utc::now());
114 }
115
116 pub fn with_priority(mut self, priority: f32) -> Self {
118 self.annotations.as_mut().unwrap().priority = Some(priority);
119 self
120 }
121
122 pub fn mark_active(self) -> Self {
124 self.with_priority(1.0)
125 }
126
127 pub fn is_active(&self) -> bool {
129 if let Some(priority) = self.priority() {
130 (priority - 1.0).abs() < EPSILON
131 } else {
132 false
133 }
134 }
135
136 pub fn priority(&self) -> Option<f32> {
138 self.annotations.as_ref().and_then(|a| a.priority)
139 }
140
141 pub fn timestamp(&self) -> Option<DateTime<Utc>> {
143 self.annotations.as_ref().and_then(|a| a.timestamp)
144 }
145
146 pub fn scheme(&self) -> Result<String> {
148 let url = Url::parse(&self.uri)?;
149 Ok(url.scheme().to_string())
150 }
151
152 pub fn with_description<S: Into<String>>(mut self, description: S) -> Self {
154 self.description = Some(description.into());
155 self
156 }
157
158 pub fn with_mime_type<S: Into<String>>(mut self, mime_type: S) -> Self {
160 let mime_type = mime_type.into();
161 match mime_type.as_str() {
162 "text" | "blob" => self.mime_type = mime_type,
163 _ => self.mime_type = default_mime_type(),
164 }
165 self
166 }
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172 use std::io::Write;
173 use tempfile::NamedTempFile;
174
175 #[test]
176 fn test_new_resource_with_file_uri() -> Result<()> {
177 let mut temp_file = NamedTempFile::new()?;
178 writeln!(temp_file, "test content")?;
179
180 let uri = Url::from_file_path(temp_file.path())
181 .map_err(|_| anyhow!("Invalid file path"))?
182 .to_string();
183
184 let resource = Resource::new(&uri, Some("text".to_string()), None)?;
185 assert!(resource.uri.starts_with("file:///"));
186 assert_eq!(resource.priority(), Some(0.0));
187 assert_eq!(resource.mime_type, "text");
188 assert_eq!(resource.scheme()?, "file");
189
190 Ok(())
191 }
192
193 #[test]
194 fn test_resource_with_str_uri() -> Result<()> {
195 let test_content = "Hello, world!";
196 let uri = format!("str:///{}", test_content);
197 let resource = Resource::with_uri(
198 uri.clone(),
199 "test.txt".to_string(),
200 0.5,
201 Some("text".to_string()),
202 )?;
203
204 assert_eq!(resource.uri, uri);
205 assert_eq!(resource.name, "test.txt");
206 assert_eq!(resource.priority(), Some(0.5));
207 assert_eq!(resource.mime_type, "text");
208 assert_eq!(resource.scheme()?, "str");
209
210 Ok(())
211 }
212
213 #[test]
214 fn test_mime_type_validation() -> Result<()> {
215 let resource = Resource::new("file:///test.txt", Some("text".to_string()), None)?;
217 assert_eq!(resource.mime_type, "text");
218
219 let resource = Resource::new("file:///test.bin", Some("blob".to_string()), None)?;
220 assert_eq!(resource.mime_type, "blob");
221
222 let resource = Resource::new("file:///test.txt", Some("invalid".to_string()), None)?;
224 assert_eq!(resource.mime_type, "text");
225
226 let resource = Resource::new("file:///test.txt", None, None)?;
228 assert_eq!(resource.mime_type, "text");
229
230 Ok(())
231 }
232
233 #[test]
234 fn test_with_description() -> Result<()> {
235 let resource = Resource::with_uri("file:///test.txt", "test.txt", 0.0, None)?
236 .with_description("A test resource");
237
238 assert_eq!(resource.description, Some("A test resource".to_string()));
239 Ok(())
240 }
241
242 #[test]
243 fn test_with_mime_type() -> Result<()> {
244 let resource =
245 Resource::with_uri("file:///test.txt", "test.txt", 0.0, None)?.with_mime_type("blob");
246
247 assert_eq!(resource.mime_type, "blob");
248
249 let resource = resource.with_mime_type("invalid");
251 assert_eq!(resource.mime_type, "text");
252 Ok(())
253 }
254
255 #[test]
256 fn test_invalid_uri() {
257 let result = Resource::new("not-a-uri", None, None);
258 assert!(result.is_err());
259 }
260}