1use std::collections::HashMap;
6use std::sync::{Arc, RwLock};
7
8use async_trait::async_trait;
9use chrono_machines::{BackoffStrategy, ExponentialBackoff};
10use dashmap::DashMap;
11use rand::rngs::SmallRng;
12use rand::SeedableRng;
13use serde::{Deserialize, Serialize};
14use thiserror::Error;
15use tokio::sync::mpsc;
16
17use crate::content::resource::ResourceContent;
18use crate::server::session::Session;
19use crate::server::visibility::{ExecutionContext, VisibilityContext};
20use crate::transport::traits::JsonRpcNotification;
21
22#[derive(Debug, Error)]
24pub enum ResourceError {
25 #[error("Resource not found: {0}")]
27 NotFound(String),
28
29 #[error("Invalid URI: {0}")]
31 InvalidUri(String),
32
33 #[error("Read error: {0}")]
35 Read(String),
36
37 #[error("Internal error: {0}")]
39 Internal(String),
40
41 #[error("Retry exhausted after {attempts} attempts: {message}")]
43 RetryExhausted { attempts: u8, message: String },
44}
45
46pub type ResourceLookupResult = Result<(Arc<dyn Resource>, HashMap<String, String>), ResourceError>;
48
49impl ResourceError {
50 pub fn is_retryable(&self) -> bool {
52 matches!(self, Self::Read(_) | Self::Internal(_))
53 }
54}
55
56#[derive(Debug, Clone)]
58pub struct ResourceRetryConfig {
59 pub max_attempts: u8,
61 pub base_delay_ms: u64,
63 pub multiplier: f64,
65 pub max_delay_ms: u64,
67 pub jitter_factor: f64,
69}
70
71impl Default for ResourceRetryConfig {
72 fn default() -> Self {
73 Self {
74 max_attempts: 3,
75 base_delay_ms: 100,
76 multiplier: 2.0,
77 max_delay_ms: 10_000,
78 jitter_factor: 1.0, }
80 }
81}
82
83#[derive(Debug, Clone, Serialize, Deserialize)]
85pub struct ResourceInfo {
86 pub uri: String,
88
89 pub name: String,
91
92 #[serde(skip_serializing_if = "Option::is_none")]
94 pub description: Option<String>,
95
96 #[serde(skip_serializing_if = "Option::is_none", rename = "mimeType")]
98 pub mime_type: Option<String>,
99}
100
101#[derive(Debug, Clone, Serialize, Deserialize)]
103#[serde(rename_all = "camelCase")]
104pub struct ResourceTemplateInfo {
105 pub uri_template: String,
107
108 pub name: String,
110
111 #[serde(skip_serializing_if = "Option::is_none")]
113 pub title: Option<String>,
114
115 #[serde(skip_serializing_if = "Option::is_none")]
117 pub description: Option<String>,
118
119 #[serde(skip_serializing_if = "Option::is_none")]
121 pub mime_type: Option<String>,
122}
123
124#[async_trait]
126pub trait Resource: Send + Sync {
127 fn uri(&self) -> &str;
129
130 fn name(&self) -> &str;
132
133 fn description(&self) -> Option<&str> {
135 None
136 }
137
138 fn mime_type(&self) -> Option<&str> {
140 None
141 }
142
143 fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
148 true
149 }
150
151 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError>;
171
172 fn text_content(&self, text: &str) -> ResourceContent {
185 ResourceContent {
186 uri: self.uri().to_string(),
187 mime_type: self.mime_type().map(|s| s.to_string()),
188 text: Some(text.to_string()),
189 blob: None,
190 }
191 }
192
193 fn blob_content(&self, data: &str) -> ResourceContent {
203 ResourceContent {
204 uri: self.uri().to_string(),
205 mime_type: self.mime_type().map(|s| s.to_string()),
206 text: None,
207 blob: Some(data.to_string()),
208 }
209 }
210}
211
212#[async_trait]
217pub trait ResourceTemplate: Send + Sync {
218 fn uri_template(&self) -> &str;
220
221 fn name(&self) -> &str;
223
224 fn title(&self) -> Option<&str> {
226 None
227 }
228
229 fn description(&self) -> Option<&str> {
231 None
232 }
233
234 fn mime_type(&self) -> Option<&str> {
236 None
237 }
238
239 fn is_visible(&self, _ctx: &VisibilityContext) -> bool {
241 true
242 }
243
244 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError>;
266}
267
268struct DynamicTemplateResource {
270 template: Arc<dyn ResourceTemplate>,
271 uri: String,
272}
273
274#[async_trait]
275impl Resource for DynamicTemplateResource {
276 fn uri(&self) -> &str {
277 &self.uri
278 }
279
280 fn name(&self) -> &str {
281 self.template.name()
282 }
283
284 fn description(&self) -> Option<&str> {
285 self.template.description()
286 }
287
288 fn mime_type(&self) -> Option<&str> {
289 self.template.mime_type()
290 }
291
292 fn is_visible(&self, ctx: &VisibilityContext) -> bool {
293 self.template.is_visible(ctx)
294 }
295
296 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
297 self.template.read(ctx).await
298 }
299}
300
301#[derive(Clone)]
303pub struct ResourceManager {
304 resources: Arc<DashMap<String, Arc<dyn Resource>>>,
305 templates: Arc<DashMap<String, Arc<dyn ResourceTemplate>>>,
306 retry_config: Arc<RwLock<ResourceRetryConfig>>,
307 notification_tx: Option<mpsc::UnboundedSender<JsonRpcNotification>>,
308}
309
310impl ResourceManager {
311 pub fn new() -> Self {
313 Self {
314 resources: Arc::new(DashMap::new()),
315 templates: Arc::new(DashMap::new()),
316 retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
317 notification_tx: None,
318 }
319 }
320
321 pub fn with_notifications(notification_tx: mpsc::UnboundedSender<JsonRpcNotification>) -> Self {
323 Self {
324 resources: Arc::new(DashMap::new()),
325 templates: Arc::new(DashMap::new()),
326 retry_config: Arc::new(RwLock::new(ResourceRetryConfig::default())),
327 notification_tx: Some(notification_tx),
328 }
329 }
330
331 pub fn set_notification_tx(&mut self, tx: mpsc::UnboundedSender<JsonRpcNotification>) {
333 self.notification_tx = Some(tx);
334 }
335
336 pub fn set_retry_config(&self, config: ResourceRetryConfig) {
338 if let Ok(mut cfg) = self.retry_config.write() {
339 *cfg = config;
340 }
341 }
342
343 fn send_notification(&self, method: &str, params: Option<serde_json::Value>) {
345 if let Some(tx) = &self.notification_tx {
346 let notification = JsonRpcNotification::new(method, params);
347 let _ = tx.send(notification);
348 }
349 }
350
351 fn notify_resources_changed(&self) {
353 self.send_notification("notifications/resources/list_changed", None);
354 }
355
356 fn notify_message(&self, level: &str, logger: &str, message: &str) {
358 self.send_notification(
359 "notifications/message",
360 Some(serde_json::json!({
361 "level": level,
362 "logger": logger,
363 "data": message
364 })),
365 );
366 }
367
368 pub fn register<R: Resource + 'static>(&self, resource: R) {
370 let uri = resource.uri().to_string();
371 self.resources.insert(uri, Arc::new(resource));
372 }
373
374 pub fn register_boxed(&self, resource: Arc<dyn Resource>) {
376 let uri = resource.uri().to_string();
377 self.resources.insert(uri, resource);
378 }
379
380 pub fn register_template<T: ResourceTemplate + 'static>(&self, template: T) {
382 let name = template.name().to_string();
383 self.templates.insert(name, Arc::new(template));
384 }
385
386 pub fn register_template_boxed(&self, template: Arc<dyn ResourceTemplate>) {
388 let name = template.name().to_string();
389 self.templates.insert(name, template);
390 }
391
392 pub fn get_template(&self, name: &str) -> Option<Arc<dyn ResourceTemplate>> {
394 self.templates.get(name).map(|t| Arc::clone(&t))
395 }
396
397 pub fn get(&self, uri: &str) -> Option<Arc<dyn Resource>> {
399 self.resources.get(uri).map(|r| Arc::clone(&r))
400 }
401
402 pub fn list(&self) -> Vec<ResourceInfo> {
404 self.resources
405 .iter()
406 .map(|entry| {
407 let resource = entry.value();
408 ResourceInfo {
409 uri: resource.uri().to_string(),
410 name: resource.name().to_string(),
411 description: resource.description().map(|s| s.to_string()),
412 mime_type: resource.mime_type().map(|s| s.to_string()),
413 }
414 })
415 .collect()
416 }
417
418 pub fn list_templates(&self) -> Vec<ResourceTemplateInfo> {
420 self.templates
421 .iter()
422 .map(|entry| {
423 let template = entry.value();
424 ResourceTemplateInfo {
425 uri_template: template.uri_template().to_string(),
426 name: template.name().to_string(),
427 title: template.title().map(|s| s.to_string()),
428 description: template.description().map(|s| s.to_string()),
429 mime_type: template.mime_type().map(|s| s.to_string()),
430 }
431 })
432 .collect()
433 }
434
435 pub fn list_templates_for_session(
437 &self,
438 _session: &Session,
439 ctx: &VisibilityContext<'_>,
440 ) -> Vec<ResourceTemplateInfo> {
441 self.templates
442 .iter()
443 .filter(|entry| entry.value().is_visible(ctx))
444 .map(|entry| {
445 let template = entry.value();
446 ResourceTemplateInfo {
447 uri_template: template.uri_template().to_string(),
448 name: template.name().to_string(),
449 title: template.title().map(|s| s.to_string()),
450 description: template.description().map(|s| s.to_string()),
451 mime_type: template.mime_type().map(|s| s.to_string()),
452 }
453 })
454 .collect()
455 }
456
457 pub async fn read(
459 &self,
460 uri: &str,
461 params: HashMap<String, String>,
462 session: &Session,
463 ) -> Result<Vec<ResourceContent>, ResourceError> {
464 let (resource, combined_params) = self.find_resource(uri, params)?;
466
467 let retry_config = self.retry_config.read()
469 .map(|c| c.clone())
470 .unwrap_or_default();
471
472 let backoff = ExponentialBackoff::default()
474 .max_attempts(retry_config.max_attempts)
475 .base_delay_ms(retry_config.base_delay_ms)
476 .multiplier(retry_config.multiplier)
477 .max_delay_ms(retry_config.max_delay_ms)
478 .jitter_factor(retry_config.jitter_factor);
479
480 let mut rng = SmallRng::from_os_rng();
481 let mut attempt: u8 = 1;
482
483 loop {
484 let ctx = ExecutionContext::for_resource(combined_params.clone(), session);
486
487 match resource.read(ctx).await {
488 Ok(content) => {
489 if attempt > 1 {
491 self.notify_message(
492 "info",
493 "chrono-machines",
494 &format!("Resource '{}' succeeded after {} attempts", uri, attempt),
495 );
496 }
497 return Ok(content);
498 }
499 Err(e) => {
500 if !e.is_retryable() {
502 return Err(e);
503 }
504
505 if !backoff.should_retry(attempt) {
507 self.notify_resources_changed();
509 self.notify_message(
510 "warning",
511 "chrono-machines",
512 &format!(
513 "Resource '{}' failed after {} attempts: {}",
514 uri, attempt, e
515 ),
516 );
517
518 return Err(ResourceError::RetryExhausted {
519 attempts: attempt,
520 message: e.to_string(),
521 });
522 }
523
524 if let Some(delay_ms) = backoff.delay(attempt, &mut rng) {
526 tokio::time::sleep(tokio::time::Duration::from_millis(delay_ms)).await;
527 }
528
529 attempt = attempt.saturating_add(1);
530 }
531 }
532 }
533 }
534
535 fn find_resource(
537 &self,
538 uri: &str,
539 params: HashMap<String, String>,
540 ) -> ResourceLookupResult {
541 if let Some(resource) = self.get(uri) {
543 return Ok((resource, params));
544 }
545
546 for entry in self.resources.iter() {
548 let template_uri = entry.key();
549 if let Some(extracted_params) = self.match_template(template_uri, uri) {
550 let resource = Arc::clone(entry.value());
551 let mut combined_params = params.clone();
552 combined_params.extend(extracted_params);
553 return Ok((resource, combined_params));
554 }
555 }
556
557 for entry in self.templates.iter() {
559 let template = entry.value();
560 let template_uri = template.uri_template();
561 if let Some(extracted_params) = self.match_template(template_uri, uri) {
562 let dynamic_resource = DynamicTemplateResource {
564 template: Arc::clone(template),
565 uri: uri.to_string(),
566 };
567 let mut combined_params = params.clone();
568 combined_params.extend(extracted_params);
569 return Ok((Arc::new(dynamic_resource), combined_params));
570 }
571 }
572
573 Err(ResourceError::NotFound(uri.to_string()))
574 }
575
576 fn match_template(&self, template: &str, uri: &str) -> Option<HashMap<String, String>> {
580 let template_parts: Vec<&str> = template.split('/').collect();
581 let uri_parts: Vec<&str> = uri.split('/').collect();
582
583 if template_parts.len() > uri_parts.len() {
585 return None;
586 }
587
588 let mut params = HashMap::new();
589 let last_idx = template_parts.len() - 1;
590
591 for (i, template_part) in template_parts.iter().enumerate() {
592 if template_part.starts_with('{') && template_part.ends_with('}') {
593 let param_name = &template_part[1..template_part.len() - 1];
594
595 if i == last_idx {
596 let remaining = uri_parts[i..].join("/");
598 params.insert(param_name.to_string(), remaining);
599 break;
600 } else {
601 if i >= uri_parts.len() {
603 return None;
604 }
605 params.insert(param_name.to_string(), uri_parts[i].to_string());
606 }
607 } else if i >= uri_parts.len() || template_part != &uri_parts[i] {
608 return None;
610 }
611 }
612
613 Some(params)
614 }
615
616 pub fn list_for_session(&self, session: &Session, ctx: &VisibilityContext<'_>) -> Vec<ResourceInfo> {
625 let mut resources = std::collections::HashMap::new();
626
627 for entry in self.resources.iter() {
629 let uri = entry.key().clone();
630 if !session.is_resource_hidden(&uri) {
631 let resource = entry.value();
632 if resource.is_visible(ctx) {
633 resources.insert(
634 uri,
635 ResourceInfo {
636 uri: resource.uri().to_string(),
637 name: resource.name().to_string(),
638 description: resource.description().map(|s| s.to_string()),
639 mime_type: resource.mime_type().map(|s| s.to_string()),
640 },
641 );
642 }
643 }
644 }
645
646 for entry in session.resource_extras().iter() {
648 let uri = entry.key().clone();
649 let resource = entry.value();
650 if resource.is_visible(ctx) {
651 resources.insert(
652 uri,
653 ResourceInfo {
654 uri: resource.uri().to_string(),
655 name: resource.name().to_string(),
656 description: resource.description().map(|s| s.to_string()),
657 mime_type: resource.mime_type().map(|s| s.to_string()),
658 },
659 );
660 }
661 }
662
663 for entry in session.resource_overrides().iter() {
665 let uri = entry.key().clone();
666 let resource = entry.value();
667 if resource.is_visible(ctx) {
668 resources.insert(
669 uri,
670 ResourceInfo {
671 uri: resource.uri().to_string(),
672 name: resource.name().to_string(),
673 description: resource.description().map(|s| s.to_string()),
674 mime_type: resource.mime_type().map(|s| s.to_string()),
675 },
676 );
677 }
678 }
679
680 resources.into_values().collect()
681 }
682
683 pub async fn read_for_session(
692 &self,
693 uri: &str,
694 params: HashMap<String, String>,
695 session: &Session,
696 visibility_ctx: &VisibilityContext<'_>,
697 ) -> Result<Vec<ResourceContent>, ResourceError> {
698 let exec_ctx = match visibility_ctx.environment {
700 Some(env) => ExecutionContext::for_resource_with_environment(params.clone(), session, env),
701 None => ExecutionContext::for_resource(params.clone(), session),
702 };
703
704 if let Some(resource) = session.get_resource_override(uri) {
706 if !resource.is_visible(visibility_ctx) {
707 return Err(ResourceError::NotFound(uri.to_string()));
708 }
709 return resource.read(exec_ctx).await;
710 }
711
712 if let Some(resource) = session.get_resource_extra(uri) {
714 if !resource.is_visible(visibility_ctx) {
715 return Err(ResourceError::NotFound(uri.to_string()));
716 }
717 return resource.read(exec_ctx).await;
718 }
719
720 if session.is_resource_hidden(uri) {
722 return Err(ResourceError::NotFound(uri.to_string()));
723 }
724
725 if let Some(resource) = self.get(uri)
727 && !resource.is_visible(visibility_ctx) {
728 return Err(ResourceError::NotFound(uri.to_string()));
729 }
730
731 self.read(uri, params, session).await
733 }
734
735 pub fn len(&self) -> usize {
737 self.resources.len()
738 }
739
740 pub fn is_empty(&self) -> bool {
742 self.resources.is_empty()
743 }
744}
745
746impl Default for ResourceManager {
747 fn default() -> Self {
748 Self::new()
749 }
750}
751
752#[cfg(test)]
753mod tests {
754 use super::*;
755
756 struct HelloResource;
758
759 #[async_trait]
760 impl Resource for HelloResource {
761 fn uri(&self) -> &str {
762 "test://hello"
763 }
764
765 fn name(&self) -> &str {
766 "hello"
767 }
768
769 fn description(&self) -> Option<&str> {
770 Some("Returns a greeting")
771 }
772
773 fn mime_type(&self) -> Option<&str> {
774 Some("text/plain")
775 }
776
777 async fn read(&self, _ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
778 Ok(vec![self.text_content("Hello, World!")])
779 }
780 }
781
782 struct UserResource;
784
785 #[async_trait]
786 impl Resource for UserResource {
787 fn uri(&self) -> &str {
788 "test://users/{id}"
789 }
790
791 fn name(&self) -> &str {
792 "user"
793 }
794
795 fn description(&self) -> Option<&str> {
796 Some("Returns user information")
797 }
798
799 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
800 let id = ctx
801 .get_uri_param("id")
802 .ok_or_else(|| ResourceError::InvalidUri("Missing 'id' parameter".to_string()))?;
803
804 Ok(vec![self.text_content(&format!("User ID: {}", id))])
805 }
806 }
807
808 #[test]
809 fn test_manager_creation() {
810 let manager = ResourceManager::new();
811 assert!(manager.is_empty());
812 }
813
814 #[test]
815 fn test_resource_registration() {
816 let manager = ResourceManager::new();
817 manager.register(HelloResource);
818
819 assert_eq!(manager.len(), 1);
820 assert!(!manager.is_empty());
821 }
822
823 #[test]
824 fn test_get_resource() {
825 let manager = ResourceManager::new();
826 manager.register(HelloResource);
827
828 let resource = manager.get("test://hello");
829 assert!(resource.is_some());
830 assert_eq!(resource.unwrap().name(), "hello");
831
832 let missing = manager.get("test://nonexistent");
833 assert!(missing.is_none());
834 }
835
836 #[test]
837 fn test_list_resources() {
838 let manager = ResourceManager::new();
839 manager.register(HelloResource);
840
841 let resources = manager.list();
842 assert_eq!(resources.len(), 1);
843 assert_eq!(resources[0].uri, "test://hello");
844 assert_eq!(resources[0].name, "hello");
845 assert_eq!(resources[0].description, Some("Returns a greeting".to_string()));
846 assert_eq!(resources[0].mime_type, Some("text/plain".to_string()));
847 }
848
849 #[tokio::test]
850 async fn test_read_static_resource() {
851 let manager = ResourceManager::new();
852 manager.register(HelloResource);
853 let session = Session::new();
854
855 let result = manager.read("test://hello", HashMap::new(), &session).await.unwrap();
856 assert_eq!(result.len(), 1);
857 }
858
859 #[tokio::test]
860 async fn test_read_missing_resource() {
861 let manager = ResourceManager::new();
862 let session = Session::new();
863
864 let result = manager.read("test://nonexistent", HashMap::new(), &session).await;
865 assert!(matches!(result, Err(ResourceError::NotFound(_))));
866 }
867
868 #[tokio::test]
869 async fn test_template_matching() {
870 let manager = ResourceManager::new();
871 manager.register(UserResource);
872 let session = Session::new();
873
874 let result = manager.read("test://users/123", HashMap::new(), &session).await.unwrap();
876 assert_eq!(result.len(), 1);
877 }
878
879 #[test]
880 fn test_template_matching_internal() {
881 let manager = ResourceManager::new();
882
883 let params = manager.match_template("test://users/{id}", "test://users/123");
885 assert!(params.is_some());
886 let params = params.unwrap();
887 assert_eq!(params.get("id"), Some(&"123".to_string()));
888
889 let params = manager.match_template(
891 "test://org/{org}/repo/{repo}",
892 "test://org/myorg/repo/myrepo",
893 );
894 assert!(params.is_some());
895 let params = params.unwrap();
896 assert_eq!(params.get("org"), Some(&"myorg".to_string()));
897 assert_eq!(params.get("repo"), Some(&"myrepo".to_string()));
898
899 let params = manager.match_template("test://users/{id}", "test://posts/123");
901 assert!(params.is_none());
902
903 let params = manager.match_template("test://users/{id}", "test://users/123/extra");
905 assert!(params.is_some());
906 let params = params.unwrap();
907 assert_eq!(params.get("id"), Some(&"123/extra".to_string()));
908 }
909
910 #[tokio::test]
911 async fn test_resource_non_matching_template() {
912 let manager = ResourceManager::new();
913 manager.register(UserResource);
914 let session = Session::new();
915
916 let result = manager.read("test://posts/123", HashMap::new(), &session).await;
918 assert!(matches!(result, Err(ResourceError::NotFound(_))));
919 }
920
921 #[tokio::test]
922 async fn test_resource_greedy_matching() {
923 let manager = ResourceManager::new();
924 manager.register(UserResource);
925 let session = Session::new();
926
927 let result = manager.read("test://users/123/extra", HashMap::new(), &session).await;
929 assert!(result.is_ok());
930 let content = result.unwrap();
931 assert_eq!(content.len(), 1);
932 assert!(content[0].text.as_ref().unwrap().contains("123/extra"));
934 }
935
936 struct FileTemplate;
940
941 #[async_trait]
942 impl ResourceTemplate for FileTemplate {
943 fn uri_template(&self) -> &str {
944 "file:///{path}"
945 }
946
947 fn name(&self) -> &str {
948 "project_files"
949 }
950
951 fn title(&self) -> Option<&str> {
952 Some("Project Files")
953 }
954
955 fn description(&self) -> Option<&str> {
956 Some("Access files in the project directory")
957 }
958
959 fn mime_type(&self) -> Option<&str> {
960 Some("application/octet-stream")
961 }
962
963 async fn read(&self, ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
964 let path = ctx
965 .get_uri_param("path")
966 .ok_or_else(|| ResourceError::InvalidUri("Missing 'path' parameter".to_string()))?;
967
968 Ok(vec![ResourceContent::text(
969 format!("file:///{}", path),
970 format!("Mock content for file: {}", path),
971 )])
972 }
973 }
974
975 struct AdminOnlyTemplate;
977
978 #[async_trait]
979 impl ResourceTemplate for AdminOnlyTemplate {
980 fn uri_template(&self) -> &str {
981 "admin:///{resource}"
982 }
983
984 fn name(&self) -> &str {
985 "admin_resources"
986 }
987
988 fn is_visible(&self, ctx: &VisibilityContext) -> bool {
989 ctx.has_role("admin")
990 }
991
992 async fn read(&self, _ctx: ExecutionContext<'_>) -> Result<Vec<ResourceContent>, ResourceError> {
993 Ok(vec![ResourceContent::text("admin:///test", "Admin content")])
994 }
995 }
996
997 #[test]
998 fn test_template_registration() {
999 let manager = ResourceManager::new();
1000 manager.register_template(FileTemplate);
1001
1002 let template = manager.get_template("project_files");
1003 assert!(template.is_some());
1004 assert_eq!(template.unwrap().name(), "project_files");
1005 }
1006
1007 #[test]
1008 fn test_get_template() {
1009 let manager = ResourceManager::new();
1010 manager.register_template(FileTemplate);
1011
1012 let template = manager.get_template("project_files");
1013 assert!(template.is_some());
1014 assert_eq!(template.unwrap().uri_template(), "file:///{path}");
1015
1016 let missing = manager.get_template("nonexistent");
1017 assert!(missing.is_none());
1018 }
1019
1020 #[test]
1021 fn test_list_templates() {
1022 let manager = ResourceManager::new();
1023 manager.register_template(FileTemplate);
1024
1025 let templates = manager.list_templates();
1026 assert_eq!(templates.len(), 1);
1027 assert_eq!(templates[0].uri_template, "file:///{path}");
1028 assert_eq!(templates[0].name, "project_files");
1029 assert_eq!(templates[0].title, Some("Project Files".to_string()));
1030 assert_eq!(templates[0].description, Some("Access files in the project directory".to_string()));
1031 assert_eq!(templates[0].mime_type, Some("application/octet-stream".to_string()));
1032 }
1033
1034 #[test]
1035 fn test_list_templates_for_session_visibility() {
1036 let manager = ResourceManager::new();
1037 manager.register_template(FileTemplate);
1038 manager.register_template(AdminOnlyTemplate);
1039
1040 let session = Session::new();
1042 let ctx = VisibilityContext::new(&session);
1043 let templates = manager.list_templates_for_session(&session, &ctx);
1044
1045 assert_eq!(templates.len(), 1);
1047 assert_eq!(templates[0].name, "project_files");
1048
1049 let admin_session = Session::new();
1051 admin_session.set_state("roles", serde_json::json!(["admin"]));
1052 let admin_ctx = VisibilityContext::new(&admin_session);
1053 let admin_templates = manager.list_templates_for_session(&admin_session, &admin_ctx);
1054
1055 assert_eq!(admin_templates.len(), 2);
1057 let names: Vec<_> = admin_templates.iter().map(|t| t.name.as_str()).collect();
1058 assert!(names.contains(&"project_files"));
1059 assert!(names.contains(&"admin_resources"));
1060 }
1061
1062 #[test]
1063 fn test_template_info_serialization() {
1064 let manager = ResourceManager::new();
1065 manager.register_template(FileTemplate);
1066
1067 let templates = manager.list_templates();
1068 let serialized = serde_json::to_value(&templates[0]).unwrap();
1069
1070 assert!(serialized.get("uriTemplate").is_some());
1072 assert_eq!(serialized["uriTemplate"], "file:///{path}");
1073 assert_eq!(serialized["name"], "project_files");
1074 assert_eq!(serialized["mimeType"], "application/octet-stream");
1075
1076 assert!(serialized.get("uri_template").is_none());
1078 assert!(serialized.get("mime_type").is_none());
1079 }
1080
1081 #[tokio::test]
1082 async fn test_template_read_with_uri_params() {
1083 let session = Session::new();
1084 let ctx = ExecutionContext::for_resource(
1085 vec![("path".to_string(), "src/main.rs".to_string())].into_iter().collect(),
1086 &session,
1087 );
1088
1089 let template = FileTemplate;
1090 let result = template.read(ctx).await.unwrap();
1091
1092 assert_eq!(result.len(), 1);
1093 assert_eq!(result[0].uri, "file:///src/main.rs");
1094 assert!(result[0].text.as_ref().unwrap().contains("src/main.rs"));
1095 }
1096
1097 #[tokio::test]
1098 async fn test_template_read_missing_param() {
1099 let session = Session::new();
1100 let ctx = ExecutionContext::for_resource(HashMap::new(), &session);
1101
1102 let template = FileTemplate;
1103 let result = template.read(ctx).await;
1104
1105 assert!(matches!(result, Err(ResourceError::InvalidUri(_))));
1107 }
1108}