pforge_runtime/
resource.rs

1use crate::{Error, Result};
2#[cfg(test)]
3use pforge_config::HandlerRef;
4use pforge_config::{ResourceDef, ResourceOperation};
5use regex::Regex;
6use rustc_hash::FxHashMap;
7use std::sync::Arc;
8
9/// Resource handler trait for read/write/subscribe operations
10#[async_trait::async_trait]
11pub trait ResourceHandler: Send + Sync {
12    /// Read resource content
13    async fn read(&self, uri: &str, params: FxHashMap<String, String>) -> Result<Vec<u8>>;
14
15    /// Write resource content (if supported)
16    async fn write(
17        &self,
18        uri: &str,
19        params: FxHashMap<String, String>,
20        content: Vec<u8>,
21    ) -> Result<()> {
22        let _ = (uri, params, content);
23        Err(Error::Handler("Write operation not supported".to_string()))
24    }
25
26    /// Subscribe to resource changes (if supported)
27    async fn subscribe(&self, uri: &str, params: FxHashMap<String, String>) -> Result<()> {
28        let _ = (uri, params);
29        Err(Error::Handler(
30            "Subscribe operation not supported".to_string(),
31        ))
32    }
33}
34
35/// Resource manager handles URI matching and dispatch
36pub struct ResourceManager {
37    resources: Vec<ResourceEntry>,
38}
39
40struct ResourceEntry {
41    uri_template: String,
42    pattern: Regex,
43    param_names: Vec<String>,
44    supports: Vec<ResourceOperation>,
45    handler: Arc<dyn ResourceHandler>,
46}
47
48impl ResourceManager {
49    pub fn new() -> Self {
50        Self {
51            resources: Vec::new(),
52        }
53    }
54
55    /// Register a resource with URI template matching
56    pub fn register(&mut self, def: ResourceDef, handler: Arc<dyn ResourceHandler>) -> Result<()> {
57        let (pattern, param_names) = Self::compile_uri_template(&def.uri_template)?;
58
59        self.resources.push(ResourceEntry {
60            uri_template: def.uri_template,
61            pattern,
62            param_names,
63            supports: def.supports,
64            handler,
65        });
66
67        Ok(())
68    }
69
70    /// Match URI and extract parameters (internal use)
71    fn match_uri(&self, uri: &str) -> Option<(&ResourceEntry, FxHashMap<String, String>)> {
72        for entry in &self.resources {
73            if let Some(captures) = entry.pattern.captures(uri) {
74                let mut params = FxHashMap::default();
75
76                for (i, name) in entry.param_names.iter().enumerate() {
77                    if let Some(value) = captures.get(i + 1) {
78                        params.insert(name.clone(), value.as_str().to_string());
79                    }
80                }
81
82                return Some((entry, params));
83            }
84        }
85
86        None
87    }
88
89    /// Read resource by URI
90    pub async fn read(&self, uri: &str) -> Result<Vec<u8>> {
91        let (entry, params) = self
92            .match_uri(uri)
93            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
94
95        if !entry.supports.contains(&ResourceOperation::Read) {
96            return Err(Error::Handler(format!(
97                "Resource {} does not support read operation",
98                entry.uri_template
99            )));
100        }
101
102        entry.handler.read(uri, params).await
103    }
104
105    /// Write resource by URI
106    pub async fn write(&self, uri: &str, content: Vec<u8>) -> Result<()> {
107        let (entry, params) = self
108            .match_uri(uri)
109            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
110
111        if !entry.supports.contains(&ResourceOperation::Write) {
112            return Err(Error::Handler(format!(
113                "Resource {} does not support write operation",
114                entry.uri_template
115            )));
116        }
117
118        entry.handler.write(uri, params, content).await
119    }
120
121    /// Subscribe to resource changes
122    pub async fn subscribe(&self, uri: &str) -> Result<()> {
123        let (entry, params) = self
124            .match_uri(uri)
125            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
126
127        if !entry.supports.contains(&ResourceOperation::Subscribe) {
128            return Err(Error::Handler(format!(
129                "Resource {} does not support subscribe operation",
130                entry.uri_template
131            )));
132        }
133
134        entry.handler.subscribe(uri, params).await
135    }
136
137    /// Compile URI template to regex pattern
138    /// Example: "file:///{path}" -> r"^file:///(.+)$" with param_names = ["path"]
139    /// Uses non-greedy matching to handle multiple parameters correctly
140    fn compile_uri_template(template: &str) -> Result<(Regex, Vec<String>)> {
141        let mut pattern = String::from("^");
142        let mut param_names = Vec::new();
143        let mut chars = template.chars().peekable();
144
145        while let Some(ch) = chars.next() {
146            if ch == '{' {
147                // Extract parameter name
148                let mut param_name = String::new();
149                while let Some(&next_ch) = chars.peek() {
150                    if next_ch == '}' {
151                        chars.next(); // consume '}'
152                        break;
153                    }
154                    param_name.push(chars.next().unwrap());
155                }
156
157                if param_name.is_empty() {
158                    return Err(Error::Handler(
159                        "Empty parameter name in URI template".to_string(),
160                    ));
161                }
162
163                param_names.push(param_name);
164
165                // Check what comes after the parameter
166                // If there's a '/' after, match non-greedy up to next '/'
167                // Otherwise, match greedy to end
168                if chars.peek() == Some(&'/') {
169                    pattern.push_str("([^/]+)"); // Segment matching
170                } else {
171                    pattern.push_str("(.+)"); // Greedy path matching
172                }
173            } else {
174                // Escape regex special characters
175                if ".*+?^$[](){}|\\".contains(ch) {
176                    pattern.push('\\');
177                }
178                pattern.push(ch);
179            }
180        }
181
182        pattern.push('$');
183
184        let regex = Regex::new(&pattern)
185            .map_err(|e| Error::Handler(format!("Invalid URI template regex: {}", e)))?;
186
187        Ok((regex, param_names))
188    }
189
190    /// List all registered resource templates
191    pub fn list_templates(&self) -> Vec<&str> {
192        self.resources
193            .iter()
194            .map(|e| e.uri_template.as_str())
195            .collect()
196    }
197}
198
199impl Default for ResourceManager {
200    fn default() -> Self {
201        Self::new()
202    }
203}
204
205#[cfg(test)]
206mod tests {
207    use super::*;
208
209    struct TestResourceHandler {
210        read_response: Vec<u8>,
211    }
212
213    #[async_trait::async_trait]
214    impl ResourceHandler for TestResourceHandler {
215        async fn read(&self, _uri: &str, _params: FxHashMap<String, String>) -> Result<Vec<u8>> {
216            Ok(self.read_response.clone())
217        }
218
219        async fn write(
220            &self,
221            _uri: &str,
222            _params: FxHashMap<String, String>,
223            _content: Vec<u8>,
224        ) -> Result<()> {
225            Ok(())
226        }
227    }
228
229    #[test]
230    fn test_uri_template_compilation() {
231        let (pattern, params) = ResourceManager::compile_uri_template("file:///{path}").unwrap();
232        assert_eq!(params, vec!["path"]);
233
234        let captures = pattern.captures("file:///home/user/test.txt").unwrap();
235        assert_eq!(captures.get(1).unwrap().as_str(), "home/user/test.txt");
236    }
237
238    #[test]
239    fn test_uri_template_multiple_params() {
240        let (pattern, params) =
241            ResourceManager::compile_uri_template("api://{service}/{resource}").unwrap();
242        assert_eq!(params, vec!["service", "resource"]);
243
244        let captures = pattern.captures("api://users/profile").unwrap();
245        assert_eq!(captures.get(1).unwrap().as_str(), "users");
246        assert_eq!(captures.get(2).unwrap().as_str(), "profile");
247    }
248
249    #[tokio::test]
250    async fn test_resource_registration_and_matching() {
251        let mut manager = ResourceManager::new();
252
253        let def = ResourceDef {
254            uri_template: "file:///{path}".to_string(),
255            handler: HandlerRef {
256                path: "test::handler".to_string(),
257                inline: None,
258            },
259            supports: vec![ResourceOperation::Read],
260        };
261
262        let handler = Arc::new(TestResourceHandler {
263            read_response: b"test content".to_vec(),
264        });
265
266        manager.register(def, handler).unwrap();
267
268        let (entry, params) = manager.match_uri("file:///test.txt").unwrap();
269        assert_eq!(entry.uri_template, "file:///{path}");
270        assert_eq!(params.get("path").unwrap(), "test.txt");
271    }
272
273    #[tokio::test]
274    async fn test_resource_read() {
275        let mut manager = ResourceManager::new();
276
277        let def = ResourceDef {
278            uri_template: "file:///{path}".to_string(),
279            handler: HandlerRef {
280                path: "test::handler".to_string(),
281                inline: None,
282            },
283            supports: vec![ResourceOperation::Read],
284        };
285
286        let handler = Arc::new(TestResourceHandler {
287            read_response: b"hello world".to_vec(),
288        });
289
290        manager.register(def, handler).unwrap();
291
292        let content = manager.read("file:///test.txt").await.unwrap();
293        assert_eq!(content, b"hello world");
294    }
295
296    #[tokio::test]
297    async fn test_resource_unsupported_operation() {
298        let mut manager = ResourceManager::new();
299
300        let def = ResourceDef {
301            uri_template: "file:///{path}".to_string(),
302            handler: HandlerRef {
303                path: "test::handler".to_string(),
304                inline: None,
305            },
306            supports: vec![ResourceOperation::Read],
307        };
308
309        let handler = Arc::new(TestResourceHandler {
310            read_response: b"test".to_vec(),
311        });
312
313        manager.register(def, handler).unwrap();
314
315        let result = manager.write("file:///test.txt", b"data".to_vec()).await;
316        assert!(result.is_err());
317        assert!(result
318            .unwrap_err()
319            .to_string()
320            .contains("does not support write"));
321    }
322
323    /// Test resource write when operation is supported
324    #[tokio::test]
325    async fn test_resource_write_supported() {
326        let mut manager = ResourceManager::new();
327
328        let def = ResourceDef {
329            uri_template: "file:///{path}".to_string(),
330            handler: HandlerRef {
331                path: "test::handler".to_string(),
332                inline: None,
333            },
334            supports: vec![ResourceOperation::Read, ResourceOperation::Write],
335        };
336
337        let handler = Arc::new(TestResourceHandler {
338            read_response: b"test".to_vec(),
339        });
340
341        manager.register(def, handler).unwrap();
342
343        // This should succeed - handler implements write returning Ok(())
344        let result = manager.write("file:///test.txt", b"data".to_vec()).await;
345        assert!(result.is_ok());
346    }
347
348    /// Handler that uses default write/subscribe implementations
349    struct ReadOnlyResourceHandler;
350
351    #[async_trait::async_trait]
352    impl ResourceHandler for ReadOnlyResourceHandler {
353        async fn read(&self, _uri: &str, _params: FxHashMap<String, String>) -> Result<Vec<u8>> {
354            Ok(b"read only content".to_vec())
355        }
356        // Note: write and subscribe use DEFAULT implementations that return errors
357    }
358
359    /// Test that default write implementation returns error
360    /// This catches the mutation: ResourceHandler::write -> Result<()> with Ok(())
361    #[tokio::test]
362    async fn test_default_write_returns_error() {
363        let handler = ReadOnlyResourceHandler;
364        let result = handler.write("uri", FxHashMap::default(), vec![]).await;
365        assert!(result.is_err());
366        assert!(result
367            .unwrap_err()
368            .to_string()
369            .contains("Write operation not supported"));
370    }
371
372    /// Test that default subscribe implementation returns error
373    #[tokio::test]
374    async fn test_default_subscribe_returns_error() {
375        let handler = ReadOnlyResourceHandler;
376        let result = handler.subscribe("uri", FxHashMap::default()).await;
377        assert!(result.is_err());
378        assert!(result
379            .unwrap_err()
380            .to_string()
381            .contains("Subscribe operation not supported"));
382    }
383
384    /// Test subscribe on unsupported resource
385    #[tokio::test]
386    async fn test_subscribe_not_supported() {
387        let mut manager = ResourceManager::new();
388
389        let def = ResourceDef {
390            uri_template: "file:///{path}".to_string(),
391            handler: HandlerRef {
392                path: "test::handler".to_string(),
393                inline: None,
394            },
395            supports: vec![ResourceOperation::Read], // No Subscribe support
396        };
397
398        let handler = Arc::new(TestResourceHandler {
399            read_response: b"test".to_vec(),
400        });
401
402        manager.register(def, handler).unwrap();
403
404        // Subscribe should fail - operation not supported
405        let result = manager.subscribe("file:///test.txt").await;
406        assert!(result.is_err());
407        assert!(result
408            .unwrap_err()
409            .to_string()
410            .contains("does not support subscribe"));
411    }
412
413    /// Test subscribe on supported resource
414    /// Handler that supports subscribe
415    struct SubscribableResourceHandler;
416
417    #[async_trait::async_trait]
418    impl ResourceHandler for SubscribableResourceHandler {
419        async fn read(&self, _uri: &str, _params: FxHashMap<String, String>) -> Result<Vec<u8>> {
420            Ok(b"content".to_vec())
421        }
422
423        async fn subscribe(&self, _uri: &str, _params: FxHashMap<String, String>) -> Result<()> {
424            Ok(())
425        }
426    }
427
428    #[tokio::test]
429    async fn test_subscribe_supported() {
430        let mut manager = ResourceManager::new();
431
432        let def = ResourceDef {
433            uri_template: "events:///{topic}".to_string(),
434            handler: HandlerRef {
435                path: "test::handler".to_string(),
436                inline: None,
437            },
438            supports: vec![ResourceOperation::Read, ResourceOperation::Subscribe],
439        };
440
441        let handler = Arc::new(SubscribableResourceHandler);
442        manager.register(def, handler).unwrap();
443
444        // Subscribe should succeed
445        let result = manager.subscribe("events:///updates").await;
446        assert!(result.is_ok());
447    }
448}