Skip to main content

dcp/resource/
mod.rs

1//! Resource handler system for DCP.
2//!
3//! Provides resource registration, URI template matching, and subscription management.
4
5use std::collections::HashMap;
6use std::sync::atomic::{AtomicU64, Ordering};
7use std::sync::Arc;
8
9use serde::{Deserialize, Serialize};
10use tokio::sync::RwLock;
11
12use crate::CapabilityManifest;
13
14/// Resource content types
15#[derive(Debug, Clone, Serialize, Deserialize)]
16#[serde(tag = "type", rename_all = "lowercase")]
17pub enum ResourceContent {
18    /// Text content
19    Text {
20        uri: String,
21        #[serde(rename = "mimeType")]
22        mime_type: String,
23        text: String,
24    },
25    /// Binary content (base64 encoded)
26    Blob {
27        uri: String,
28        #[serde(rename = "mimeType")]
29        mime_type: String,
30        blob: String,
31    },
32}
33
34impl ResourceContent {
35    /// Create text content
36    pub fn text(
37        uri: impl Into<String>,
38        mime_type: impl Into<String>,
39        text: impl Into<String>,
40    ) -> Self {
41        Self::Text {
42            uri: uri.into(),
43            mime_type: mime_type.into(),
44            text: text.into(),
45        }
46    }
47
48    /// Create binary content (will be base64 encoded)
49    pub fn blob(uri: impl Into<String>, mime_type: impl Into<String>, data: &[u8]) -> Self {
50        use base64::Engine;
51        Self::Blob {
52            uri: uri.into(),
53            mime_type: mime_type.into(),
54            blob: base64::engine::general_purpose::STANDARD.encode(data),
55        }
56    }
57
58    /// Get the URI
59    pub fn uri(&self) -> &str {
60        match self {
61            Self::Text { uri, .. } => uri,
62            Self::Blob { uri, .. } => uri,
63        }
64    }
65}
66
67/// Resource information
68#[derive(Debug, Clone, Serialize, Deserialize)]
69pub struct ResourceInfo {
70    /// Resource URI
71    pub uri: String,
72    /// Human-readable name
73    pub name: String,
74    /// Optional description
75    #[serde(skip_serializing_if = "Option::is_none")]
76    pub description: Option<String>,
77    /// MIME type
78    #[serde(rename = "mimeType", skip_serializing_if = "Option::is_none")]
79    pub mime_type: Option<String>,
80}
81
82impl ResourceInfo {
83    /// Create new resource info
84    pub fn new(uri: impl Into<String>, name: impl Into<String>) -> Self {
85        Self {
86            uri: uri.into(),
87            name: name.into(),
88            description: None,
89            mime_type: None,
90        }
91    }
92
93    /// Set description
94    pub fn with_description(mut self, desc: impl Into<String>) -> Self {
95        self.description = Some(desc.into());
96        self
97    }
98
99    /// Set MIME type
100    pub fn with_mime_type(mut self, mime: impl Into<String>) -> Self {
101        self.mime_type = Some(mime.into());
102        self
103    }
104}
105
106/// Paginated resource list
107#[derive(Debug, Clone, Serialize, Deserialize)]
108pub struct ResourceList {
109    /// Resources in this page
110    pub resources: Vec<ResourceInfo>,
111    /// Cursor for next page (None if last page)
112    #[serde(rename = "nextCursor", skip_serializing_if = "Option::is_none")]
113    pub next_cursor: Option<String>,
114}
115
116/// Resource error
117#[derive(Debug, Clone, thiserror::Error)]
118pub enum ResourceError {
119    #[error("resource not found: {0}")]
120    NotFound(String),
121    #[error("invalid URI: {0}")]
122    InvalidUri(String),
123    #[error("handler error: {0}")]
124    HandlerError(String),
125    #[error("{kind} capacity exceeded")]
126    CapacityExceeded { kind: &'static str, max: usize },
127    #[error("subscription not supported")]
128    SubscriptionNotSupported,
129}
130
131/// Subscription ID
132#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
133pub struct SubscriptionId(pub u64);
134
135/// Resource handler trait
136pub trait ResourceHandler: Send + Sync {
137    /// Get the URI template for this handler
138    fn uri_template(&self) -> &str;
139
140    /// List available resources
141    fn list(&self, cursor: Option<&str>) -> Result<ResourceList, ResourceError>;
142
143    /// Read a specific resource
144    fn read(&self, uri: &str) -> Result<ResourceContent, ResourceError>;
145
146    /// Check if this handler supports subscriptions
147    fn supports_subscribe(&self) -> bool {
148        false
149    }
150
151    /// Check if a URI matches this handler's template
152    fn matches(&self, uri: &str) -> bool {
153        uri_matches_template(uri, self.uri_template())
154    }
155}
156
157struct RegisteredResourceHandler {
158    id: u16,
159    handler: Box<dyn ResourceHandler>,
160}
161
162/// Check if a URI matches a template pattern
163/// Supports simple patterns like "file:///{path}" where {path} is a wildcard
164pub fn uri_matches_template(uri: &str, template: &str) -> bool {
165    // Simple matching: split by {param} placeholders
166    let mut template_parts = Vec::new();
167    let mut current = template;
168
169    while let Some(start) = current.find('{') {
170        template_parts.push(&current[..start]);
171        if let Some(end) = current[start..].find('}') {
172            current = &current[start + end + 1..];
173        } else {
174            break;
175        }
176    }
177    template_parts.push(current);
178
179    // Match URI against template parts
180    let mut uri_pos = 0;
181    for (i, part) in template_parts.iter().enumerate() {
182        if part.is_empty() {
183            continue;
184        }
185        if let Some(pos) = uri[uri_pos..].find(part) {
186            if i == 0 && pos != 0 {
187                return false; // First part must match at start
188            }
189            uri_pos += pos + part.len();
190        } else {
191            return false;
192        }
193    }
194
195    match template_parts.last() {
196        Some(last_part) if !last_part.is_empty() => uri_pos == uri.len(),
197        _ => true,
198    }
199}
200
201/// Resource registry for managing handlers and subscriptions
202pub struct ResourceRegistry {
203    /// Registered handlers
204    handlers: Vec<RegisteredResourceHandler>,
205    /// Active subscriptions: URI -> list of subscription IDs
206    subscriptions: RwLock<HashMap<String, Vec<SubscriptionId>>>,
207    /// Subscription ID counter
208    subscription_counter: AtomicU64,
209    /// Subscription callbacks: ID -> callback
210    callbacks: RwLock<HashMap<SubscriptionId, Arc<dyn Fn(&str) + Send + Sync>>>,
211}
212
213impl Default for ResourceRegistry {
214    fn default() -> Self {
215        Self::new()
216    }
217}
218
219impl ResourceRegistry {
220    /// Create a new resource registry
221    pub fn new() -> Self {
222        Self {
223            handlers: Vec::new(),
224            subscriptions: RwLock::new(HashMap::new()),
225            subscription_counter: AtomicU64::new(1),
226            callbacks: RwLock::new(HashMap::new()),
227        }
228    }
229
230    /// Register a resource handler
231    pub fn register(
232        &mut self,
233        handler: impl ResourceHandler + 'static,
234    ) -> Result<u16, ResourceError> {
235        if self.handlers.len() >= CapabilityManifest::MAX_RESOURCES {
236            return Err(ResourceError::CapacityExceeded {
237                kind: "resource",
238                max: CapabilityManifest::MAX_RESOURCES,
239            });
240        }
241
242        let id = self.handlers.len() as u16;
243        self.handlers.push(RegisteredResourceHandler {
244            id,
245            handler: Box::new(handler),
246        });
247        Ok(id)
248    }
249
250    /// Get the number of registered handlers
251    pub fn handler_count(&self) -> usize {
252        self.handlers.len()
253    }
254
255    /// Find a handler that matches the given URI
256    pub fn match_uri(&self, uri: &str) -> Option<&dyn ResourceHandler> {
257        self.handlers
258            .iter()
259            .find(|registered| registered.handler.matches(uri))
260            .map(|registered| registered.handler.as_ref())
261    }
262
263    /// Find the registered capability ID for the handler matching the URI.
264    pub fn handler_id_for_uri(&self, uri: &str) -> Option<u16> {
265        self.handlers
266            .iter()
267            .find(|registered| registered.handler.matches(uri))
268            .map(|registered| registered.id)
269    }
270
271    /// Registered resource capability IDs.
272    pub fn handler_ids(&self) -> Vec<u16> {
273        self.handlers
274            .iter()
275            .map(|registered| registered.id)
276            .collect()
277    }
278
279    /// URI templates for registered handlers accepted by a capability predicate.
280    pub fn allowed_uri_templates<F>(&self, mut allow_handler: F) -> Vec<String>
281    where
282        F: FnMut(u16) -> bool,
283    {
284        self.handlers
285            .iter()
286            .filter(|registered| allow_handler(registered.id))
287            .map(|registered| registered.handler.uri_template().to_string())
288            .collect()
289    }
290
291    /// Whether any accepted handler supports resource subscriptions.
292    pub fn any_allowed_supports_subscribe<F>(&self, mut allow_handler: F) -> bool
293    where
294        F: FnMut(u16) -> bool,
295    {
296        self.handlers.iter().any(|registered| {
297            allow_handler(registered.id) && registered.handler.supports_subscribe()
298        })
299    }
300
301    /// List all resources from all handlers
302    pub fn list_all(&self, cursor: Option<&str>) -> Result<ResourceList, ResourceError> {
303        self.list_allowed(cursor, |_| true)
304    }
305
306    /// List resources from handlers accepted by a capability predicate.
307    pub fn list_allowed<F>(
308        &self,
309        cursor: Option<&str>,
310        mut allow_handler: F,
311    ) -> Result<ResourceList, ResourceError>
312    where
313        F: FnMut(u16) -> bool,
314    {
315        let mut all_resources = Vec::new();
316
317        for registered in &self.handlers {
318            if !allow_handler(registered.id) {
319                continue;
320            }
321            match registered.handler.list(cursor) {
322                Ok(list) => all_resources.extend(list.resources),
323                Err(e) => return Err(e),
324            }
325        }
326
327        Ok(ResourceList {
328            resources: all_resources,
329            next_cursor: None, // Simplified: no pagination across handlers
330        })
331    }
332
333    /// Read a resource by URI
334    pub fn read(&self, uri: &str) -> Result<ResourceContent, ResourceError> {
335        let handler = self
336            .match_uri(uri)
337            .ok_or_else(|| ResourceError::NotFound(uri.to_string()))?;
338        handler.read(uri)
339    }
340
341    /// Validate that a concrete resource URI is eligible for subscriptions.
342    pub fn ensure_subscribable(&self, uri: &str) -> Result<(), ResourceError> {
343        let handler = self
344            .match_uri(uri)
345            .ok_or_else(|| ResourceError::NotFound(uri.to_string()))?;
346
347        if !handler.supports_subscribe() {
348            return Err(ResourceError::SubscriptionNotSupported);
349        }
350
351        handler.read(uri).map(|_| ())
352    }
353
354    /// Subscribe to resource changes
355    pub async fn subscribe<F>(
356        &self,
357        uri: &str,
358        callback: F,
359    ) -> Result<SubscriptionId, ResourceError>
360    where
361        F: Fn(&str) + Send + Sync + 'static,
362    {
363        // Check if any handler supports this URI and subscriptions
364        let handler = self
365            .match_uri(uri)
366            .ok_or_else(|| ResourceError::NotFound(uri.to_string()))?;
367
368        if !handler.supports_subscribe() {
369            return Err(ResourceError::SubscriptionNotSupported);
370        }
371
372        let id = SubscriptionId(self.subscription_counter.fetch_add(1, Ordering::SeqCst));
373
374        // Add to subscriptions
375        {
376            let mut subs = self.subscriptions.write().await;
377            subs.entry(uri.to_string()).or_default().push(id);
378        }
379
380        // Store callback
381        {
382            let mut callbacks = self.callbacks.write().await;
383            callbacks.insert(id, Arc::new(callback));
384        }
385
386        Ok(id)
387    }
388
389    /// Unsubscribe from resource changes
390    pub async fn unsubscribe(&self, id: SubscriptionId) -> bool {
391        // Remove from callbacks
392        let removed = {
393            let mut callbacks = self.callbacks.write().await;
394            callbacks.remove(&id).is_some()
395        };
396
397        if removed {
398            // Remove from subscriptions
399            let mut subs = self.subscriptions.write().await;
400            for ids in subs.values_mut() {
401                ids.retain(|&sub_id| sub_id != id);
402            }
403        }
404
405        removed
406    }
407
408    /// Notify all subscribers of a resource change
409    pub async fn notify_change(&self, uri: &str) {
410        let callbacks_to_call: Vec<Arc<dyn Fn(&str) + Send + Sync>> = {
411            let subs = self.subscriptions.read().await;
412            let callbacks = self.callbacks.read().await;
413
414            subs.get(uri)
415                .map(|ids| {
416                    ids.iter()
417                        .filter_map(|id| callbacks.get(id).cloned())
418                        .collect()
419                })
420                .unwrap_or_default()
421        };
422
423        for callback in callbacks_to_call {
424            callback(uri);
425        }
426    }
427
428    /// Get subscription count for a URI
429    pub async fn subscription_count(&self, uri: &str) -> usize {
430        self.subscriptions
431            .read()
432            .await
433            .get(uri)
434            .map(|ids| ids.len())
435            .unwrap_or(0)
436    }
437}
438
439/// Simple in-memory resource handler for testing
440pub struct MemoryResourceHandler {
441    template: String,
442    resources: HashMap<String, ResourceContent>,
443    supports_subscribe: bool,
444}
445
446impl MemoryResourceHandler {
447    /// Create a new memory resource handler
448    pub fn new(template: impl Into<String>) -> Self {
449        Self {
450            template: template.into(),
451            resources: HashMap::new(),
452            supports_subscribe: false,
453        }
454    }
455
456    /// Enable subscription support
457    pub fn with_subscriptions(mut self) -> Self {
458        self.supports_subscribe = true;
459        self
460    }
461
462    /// Add a resource
463    pub fn add_resource(&mut self, uri: impl Into<String>, content: ResourceContent) {
464        self.resources.insert(uri.into(), content);
465    }
466}
467
468impl ResourceHandler for MemoryResourceHandler {
469    fn uri_template(&self) -> &str {
470        &self.template
471    }
472
473    fn list(&self, _cursor: Option<&str>) -> Result<ResourceList, ResourceError> {
474        let resources: Vec<ResourceInfo> = self
475            .resources
476            .iter()
477            .map(|(uri, content)| {
478                let (mime_type, name) = match content {
479                    ResourceContent::Text { mime_type, .. } => (mime_type.clone(), uri.clone()),
480                    ResourceContent::Blob { mime_type, .. } => (mime_type.clone(), uri.clone()),
481                };
482                ResourceInfo::new(uri, name).with_mime_type(mime_type)
483            })
484            .collect();
485
486        Ok(ResourceList {
487            resources,
488            next_cursor: None,
489        })
490    }
491
492    fn read(&self, uri: &str) -> Result<ResourceContent, ResourceError> {
493        self.resources
494            .get(uri)
495            .cloned()
496            .ok_or_else(|| ResourceError::NotFound(uri.to_string()))
497    }
498
499    fn supports_subscribe(&self) -> bool {
500        self.supports_subscribe
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use super::*;
507
508    #[test]
509    fn test_uri_matches_template() {
510        assert!(uri_matches_template(
511            "file:///path/to/file.txt",
512            "file:///{path}"
513        ));
514        assert!(uri_matches_template("file:///a.txt", "file:///{path}"));
515        assert!(uri_matches_template(
516            "http://example.com/api/users/123",
517            "http://example.com/api/users/{id}"
518        ));
519        assert!(!uri_matches_template("ftp://example.com", "http://{host}"));
520    }
521
522    #[test]
523    fn test_resource_content_text() {
524        let content = ResourceContent::text("file:///test.txt", "text/plain", "Hello");
525        assert_eq!(content.uri(), "file:///test.txt");
526    }
527
528    #[test]
529    fn test_resource_info() {
530        let info = ResourceInfo::new("file:///test.txt", "Test File")
531            .with_description("A test file")
532            .with_mime_type("text/plain");
533
534        assert_eq!(info.uri, "file:///test.txt");
535        assert_eq!(info.name, "Test File");
536        assert_eq!(info.description, Some("A test file".to_string()));
537        assert_eq!(info.mime_type, Some("text/plain".to_string()));
538    }
539
540    #[test]
541    fn test_memory_resource_handler() {
542        let mut handler = MemoryResourceHandler::new("file:///{path}");
543        handler.add_resource(
544            "file:///test.txt",
545            ResourceContent::text("file:///test.txt", "text/plain", "Hello"),
546        );
547
548        assert!(handler.matches("file:///test.txt"));
549        assert!(handler.matches("file:///other.txt"));
550
551        let list = handler.list(None).unwrap();
552        assert_eq!(list.resources.len(), 1);
553
554        let content = handler.read("file:///test.txt").unwrap();
555        assert_eq!(content.uri(), "file:///test.txt");
556    }
557
558    #[tokio::test]
559    async fn test_resource_registry() {
560        let mut registry = ResourceRegistry::new();
561
562        let mut handler = MemoryResourceHandler::new("file:///{path}");
563        handler.add_resource(
564            "file:///test.txt",
565            ResourceContent::text("file:///test.txt", "text/plain", "Hello"),
566        );
567        registry.register(handler).unwrap();
568
569        assert_eq!(registry.handler_count(), 1);
570
571        let content = registry.read("file:///test.txt").unwrap();
572        assert_eq!(content.uri(), "file:///test.txt");
573
574        let result = registry.read("file:///nonexistent.txt");
575        assert!(result.is_err());
576    }
577
578    #[tokio::test]
579    async fn test_resource_subscriptions() {
580        let mut registry = ResourceRegistry::new();
581
582        let handler = MemoryResourceHandler::new("file:///{path}").with_subscriptions();
583        registry.register(handler).unwrap();
584
585        // Subscribe
586        let notified = Arc::new(std::sync::atomic::AtomicBool::new(false));
587        let notified_clone = Arc::clone(&notified);
588
589        let sub_id = registry
590            .subscribe("file:///test.txt", move |_uri| {
591                notified_clone.store(true, Ordering::SeqCst);
592            })
593            .await
594            .unwrap();
595
596        assert_eq!(registry.subscription_count("file:///test.txt").await, 1);
597
598        // Notify
599        registry.notify_change("file:///test.txt").await;
600        assert!(notified.load(Ordering::SeqCst));
601
602        // Unsubscribe
603        assert!(registry.unsubscribe(sub_id).await);
604        assert_eq!(registry.subscription_count("file:///test.txt").await, 0);
605    }
606}