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