1use serde_json::Value;
8use std::collections::HashMap;
9use std::future::Future;
10use std::pin::Pin;
11
12use turul_mcp_protocol::meta::Annotations;
14use turul_mcp_protocol::resources::{
15 HasResourceAnnotations, HasResourceDescription, HasResourceMeta, HasResourceMetadata,
16 HasResourceMimeType, HasResourceSize, HasResourceUri, ResourceContent,
17};
18
19pub type DynamicResourceFn = Box<
21 dyn Fn(String) -> Pin<Box<dyn Future<Output = Result<ResourceContent, String>> + Send>>
22 + Send
23 + Sync,
24>;
25
26pub struct ResourceBuilder {
28 uri: String,
29 name: String,
30 title: Option<String>,
31 description: Option<String>,
32 mime_type: Option<String>,
33 size: Option<u64>,
34 content: Option<ResourceContent>,
35 annotations: Option<Annotations>,
36 meta: Option<HashMap<String, Value>>,
37 read_fn: Option<DynamicResourceFn>,
38}
39
40impl ResourceBuilder {
41 pub fn new(uri: impl Into<String>) -> Self {
43 let uri = uri.into();
44 let name = uri.split('/').next_back().unwrap_or(&uri).to_string();
46
47 Self {
48 uri,
49 name,
50 title: None,
51 description: None,
52 mime_type: None,
53 size: None,
54 content: None,
55 annotations: None,
56 meta: None,
57 read_fn: None,
58 }
59 }
60
61 pub fn name(mut self, name: impl Into<String>) -> Self {
63 self.name = name.into();
64 self
65 }
66
67 pub fn title(mut self, title: impl Into<String>) -> Self {
69 self.title = Some(title.into());
70 self
71 }
72
73 pub fn description(mut self, description: impl Into<String>) -> Self {
75 self.description = Some(description.into());
76 self
77 }
78
79 pub fn mime_type(mut self, mime_type: impl Into<String>) -> Self {
81 self.mime_type = Some(mime_type.into());
82 self
83 }
84
85 pub fn size(mut self, size: u64) -> Self {
87 self.size = Some(size);
88 self
89 }
90
91 pub fn text_content(mut self, text: impl Into<String>) -> Self {
93 let text = text.into();
94 self.size = Some(text.len() as u64);
95 if self.mime_type.is_none() {
96 self.mime_type = Some("text/plain".to_string());
97 }
98 self.content = Some(ResourceContent::text(&self.uri, text));
99 self
100 }
101
102 pub fn json_content(mut self, json_value: Value) -> Self {
104 let text = serde_json::to_string_pretty(&json_value).unwrap_or_else(|_| "{}".to_string());
105 self.size = Some(text.len() as u64);
106 self.mime_type = Some("application/json".to_string());
107 self.content = Some(ResourceContent::text(&self.uri, text));
108 self
109 }
110
111 pub fn blob_content(mut self, blob: impl Into<String>, mime_type: impl Into<String>) -> Self {
113 let blob = blob.into();
114 let mime_type = mime_type.into();
115
116 self.size = Some((blob.len() * 3 / 4) as u64);
118 self.mime_type = Some(mime_type.clone());
119 self.content = Some(ResourceContent::blob(&self.uri, blob, mime_type));
120 self
121 }
122
123 pub fn annotations(mut self, annotations: Annotations) -> Self {
125 self.annotations = Some(annotations);
126 self
127 }
128
129 pub fn annotation_title(mut self, title: impl Into<String>) -> Self {
131 let mut annotations = self.annotations.unwrap_or_default();
132 annotations.title = Some(title.into());
133 self.annotations = Some(annotations);
134 self
135 }
136
137 pub fn meta(mut self, meta: HashMap<String, Value>) -> Self {
139 self.meta = Some(meta);
140 self
141 }
142
143 pub fn read<F, Fut>(mut self, f: F) -> Self
145 where
146 F: Fn(String) -> Fut + Send + Sync + 'static,
147 Fut: Future<Output = Result<ResourceContent, String>> + Send + 'static,
148 {
149 self.read_fn = Some(Box::new(move |uri| Box::pin(f(uri))));
150 self
151 }
152
153 pub fn read_text<F, Fut>(mut self, f: F) -> Self
155 where
156 F: Fn(String) -> Fut + Send + Sync + 'static + Clone,
157 Fut: Future<Output = Result<String, String>> + Send + 'static,
158 {
159 self.read_fn = Some(Box::new(move |uri| {
160 let f = f.clone();
161 let uri_clone = uri.clone();
162 Box::pin(async move {
163 let text = f(uri.clone()).await?;
164 Ok(ResourceContent::text(uri_clone, text))
165 })
166 }));
167 self
168 }
169
170 pub fn build(self) -> Result<DynamicResource, String> {
172 Ok(DynamicResource {
173 uri: self.uri,
174 name: self.name,
175 title: self.title,
176 description: self.description,
177 mime_type: self.mime_type,
178 size: self.size,
179 content: self.content,
180 annotations: self.annotations,
181 meta: self.meta,
182 read_fn: self.read_fn,
183 })
184 }
185}
186
187pub struct DynamicResource {
189 uri: String,
190 name: String,
191 title: Option<String>,
192 description: Option<String>,
193 mime_type: Option<String>,
194 size: Option<u64>,
195 content: Option<ResourceContent>,
196 annotations: Option<Annotations>,
197 meta: Option<HashMap<String, Value>>,
198 read_fn: Option<DynamicResourceFn>,
199}
200
201impl DynamicResource {
202 pub async fn read(&self) -> Result<ResourceContent, String> {
204 if let Some(ref content) = self.content {
205 Ok(content.clone())
207 } else if let Some(ref read_fn) = self.read_fn {
208 read_fn(self.uri.clone()).await
210 } else {
211 Err("No content or read function provided".to_string())
212 }
213 }
214}
215
216impl HasResourceMetadata for DynamicResource {
219 fn name(&self) -> &str {
220 &self.name
221 }
222
223 fn title(&self) -> Option<&str> {
224 self.title.as_deref()
225 }
226}
227
228impl HasResourceDescription for DynamicResource {
230 fn description(&self) -> Option<&str> {
231 self.description.as_deref()
232 }
233}
234
235impl HasResourceUri for DynamicResource {
237 fn uri(&self) -> &str {
238 &self.uri
239 }
240}
241
242impl HasResourceMimeType for DynamicResource {
244 fn mime_type(&self) -> Option<&str> {
245 self.mime_type.as_deref()
246 }
247}
248
249impl HasResourceSize for DynamicResource {
251 fn size(&self) -> Option<u64> {
252 self.size
253 }
254}
255
256impl HasResourceAnnotations for DynamicResource {
258 fn annotations(&self) -> Option<&Annotations> {
259 self.annotations.as_ref()
260 }
261}
262
263impl HasResourceMeta for DynamicResource {
265 fn resource_meta(&self) -> Option<&HashMap<String, Value>> {
266 self.meta.as_ref()
267 }
268}
269
270#[cfg(test)]
276mod tests {
277 use super::*;
278 use serde_json::json;
279
280 #[test]
281 fn test_resource_builder_basic() {
282 let resource = ResourceBuilder::new("file:///test.txt")
283 .name("test_resource")
284 .description("A test resource")
285 .text_content("Hello, World!")
286 .build()
287 .expect("Failed to build resource");
288
289 assert_eq!(resource.name(), "test_resource");
290 assert_eq!(resource.uri(), "file:///test.txt");
291 assert_eq!(resource.description(), Some("A test resource"));
292 assert_eq!(resource.mime_type(), Some("text/plain"));
293 assert_eq!(resource.size(), Some(13)); }
295
296 #[tokio::test]
297 async fn test_resource_builder_static_content() {
298 let resource = ResourceBuilder::new("file:///config.json")
299 .description("Application configuration")
300 .json_content(json!({"version": "1.0", "debug": true}))
301 .build()
302 .expect("Failed to build resource");
303
304 let content = resource.read().await.expect("Failed to read content");
305
306 match content {
307 ResourceContent::Text(text_content) => {
308 assert!(text_content.text.contains("version"));
309 assert!(text_content.text.contains("1.0"));
310 assert_eq!(text_content.uri, "file:///config.json");
311 }
312 _ => panic!("Expected text content"),
313 }
314
315 assert_eq!(resource.mime_type(), Some("application/json"));
317 }
318
319 #[tokio::test]
320 async fn test_resource_builder_dynamic_content() {
321 let resource = ResourceBuilder::new("file:///dynamic.txt")
322 .description("Dynamic content resource")
323 .read_text(|_uri| async move { Ok("This is dynamic content!".to_string()) })
324 .build()
325 .expect("Failed to build resource");
326
327 let content = resource.read().await.expect("Failed to read content");
328
329 match content {
330 ResourceContent::Text(text_content) => {
331 assert_eq!(text_content.text, "This is dynamic content!");
332 }
333 _ => panic!("Expected text content"),
334 }
335 }
336
337 #[test]
338 fn test_resource_builder_annotations() {
339 let resource = ResourceBuilder::new("file:///important.txt")
340 .description("Important resource")
341 .annotation_title("Important File")
342 .build()
343 .expect("Failed to build resource");
344
345 let annotations = resource.annotations().expect("Expected annotations");
346 assert_eq!(annotations.title, Some("Important File".to_string()));
347 }
348
349 #[test]
350 fn test_resource_builder_blob_content() {
351 let resource = ResourceBuilder::new("data://example.png")
352 .description("Example image")
353 .blob_content("iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNkYPhfDwAChwGA60e6kgAAAABJRU5ErkJggg==", "image/png")
354 .build()
355 .expect("Failed to build resource");
356
357 assert_eq!(resource.mime_type(), Some("image/png"));
358 assert!(resource.size().unwrap() > 0);
359 }
360}