pforge_runtime/
resource.rs

1use crate::{Error, Result};
2use pforge_config::{HandlerRef, ResourceDef, ResourceOperation};
3use regex::Regex;
4use std::collections::HashMap;
5use std::sync::Arc;
6
7/// Resource handler trait for read/write/subscribe operations
8#[async_trait::async_trait]
9pub trait ResourceHandler: Send + Sync {
10    /// Read resource content
11    async fn read(&self, uri: &str, params: HashMap<String, String>) -> Result<Vec<u8>>;
12
13    /// Write resource content (if supported)
14    async fn write(&self, uri: &str, params: HashMap<String, String>, content: Vec<u8>) -> Result<()> {
15        let _ = (uri, params, content);
16        Err(Error::Handler("Write operation not supported".to_string()))
17    }
18
19    /// Subscribe to resource changes (if supported)
20    async fn subscribe(&self, uri: &str, params: HashMap<String, String>) -> Result<()> {
21        let _ = (uri, params);
22        Err(Error::Handler("Subscribe operation not supported".to_string()))
23    }
24}
25
26/// Resource manager handles URI matching and dispatch
27pub struct ResourceManager {
28    resources: Vec<ResourceEntry>,
29}
30
31struct ResourceEntry {
32    uri_template: String,
33    pattern: Regex,
34    param_names: Vec<String>,
35    supports: Vec<ResourceOperation>,
36    handler: Arc<dyn ResourceHandler>,
37}
38
39impl ResourceManager {
40    pub fn new() -> Self {
41        Self {
42            resources: Vec::new(),
43        }
44    }
45
46    /// Register a resource with URI template matching
47    pub fn register(
48        &mut self,
49        def: ResourceDef,
50        handler: Arc<dyn ResourceHandler>,
51    ) -> Result<()> {
52        let (pattern, param_names) = Self::compile_uri_template(&def.uri_template)?;
53
54        self.resources.push(ResourceEntry {
55            uri_template: def.uri_template,
56            pattern,
57            param_names,
58            supports: def.supports,
59            handler,
60        });
61
62        Ok(())
63    }
64
65    /// Match URI and extract parameters (internal use)
66    fn match_uri(&self, uri: &str) -> Option<(&ResourceEntry, HashMap<String, String>)> {
67        for entry in &self.resources {
68            if let Some(captures) = entry.pattern.captures(uri) {
69                let mut params = HashMap::new();
70
71                for (i, name) in entry.param_names.iter().enumerate() {
72                    if let Some(value) = captures.get(i + 1) {
73                        params.insert(name.clone(), value.as_str().to_string());
74                    }
75                }
76
77                return Some((entry, params));
78            }
79        }
80
81        None
82    }
83
84    /// Read resource by URI
85    pub async fn read(&self, uri: &str) -> Result<Vec<u8>> {
86        let (entry, params) = self
87            .match_uri(uri)
88            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
89
90        if !entry.supports.contains(&ResourceOperation::Read) {
91            return Err(Error::Handler(format!(
92                "Resource {} does not support read operation",
93                entry.uri_template
94            )));
95        }
96
97        entry.handler.read(uri, params).await
98    }
99
100    /// Write resource by URI
101    pub async fn write(&self, uri: &str, content: Vec<u8>) -> Result<()> {
102        let (entry, params) = self
103            .match_uri(uri)
104            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
105
106        if !entry.supports.contains(&ResourceOperation::Write) {
107            return Err(Error::Handler(format!(
108                "Resource {} does not support write operation",
109                entry.uri_template
110            )));
111        }
112
113        entry.handler.write(uri, params, content).await
114    }
115
116    /// Subscribe to resource changes
117    pub async fn subscribe(&self, uri: &str) -> Result<()> {
118        let (entry, params) = self
119            .match_uri(uri)
120            .ok_or_else(|| Error::Handler(format!("No resource matches URI: {}", uri)))?;
121
122        if !entry.supports.contains(&ResourceOperation::Subscribe) {
123            return Err(Error::Handler(format!(
124                "Resource {} does not support subscribe operation",
125                entry.uri_template
126            )));
127        }
128
129        entry.handler.subscribe(uri, params).await
130    }
131
132    /// Compile URI template to regex pattern
133    /// Example: "file:///{path}" -> r"^file:///(.+)$" with param_names = ["path"]
134    /// Uses non-greedy matching to handle multiple parameters correctly
135    fn compile_uri_template(template: &str) -> Result<(Regex, Vec<String>)> {
136        let mut pattern = String::from("^");
137        let mut param_names = Vec::new();
138        let mut chars = template.chars().peekable();
139
140        while let Some(ch) = chars.next() {
141            if ch == '{' {
142                // Extract parameter name
143                let mut param_name = String::new();
144                while let Some(&next_ch) = chars.peek() {
145                    if next_ch == '}' {
146                        chars.next(); // consume '}'
147                        break;
148                    }
149                    param_name.push(chars.next().unwrap());
150                }
151
152                if param_name.is_empty() {
153                    return Err(Error::Handler("Empty parameter name in URI template".to_string()));
154                }
155
156                param_names.push(param_name);
157
158                // Check what comes after the parameter
159                // If there's a '/' after, match non-greedy up to next '/'
160                // Otherwise, match greedy to end
161                if chars.peek() == Some(&'/') {
162                    pattern.push_str("([^/]+)"); // Segment matching
163                } else {
164                    pattern.push_str("(.+)"); // Greedy path matching
165                }
166            } else {
167                // Escape regex special characters
168                if ".*+?^$[](){}|\\".contains(ch) {
169                    pattern.push('\\');
170                }
171                pattern.push(ch);
172            }
173        }
174
175        pattern.push('$');
176
177        let regex = Regex::new(&pattern)
178            .map_err(|e| Error::Handler(format!("Invalid URI template regex: {}", e)))?;
179
180        Ok((regex, param_names))
181    }
182
183    /// List all registered resource templates
184    pub fn list_templates(&self) -> Vec<&str> {
185        self.resources.iter().map(|e| e.uri_template.as_str()).collect()
186    }
187}
188
189impl Default for ResourceManager {
190    fn default() -> Self {
191        Self::new()
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198
199    struct TestResourceHandler {
200        read_response: Vec<u8>,
201    }
202
203    #[async_trait::async_trait]
204    impl ResourceHandler for TestResourceHandler {
205        async fn read(&self, _uri: &str, _params: HashMap<String, String>) -> Result<Vec<u8>> {
206            Ok(self.read_response.clone())
207        }
208
209        async fn write(&self, _uri: &str, _params: HashMap<String, String>, _content: Vec<u8>) -> Result<()> {
210            Ok(())
211        }
212    }
213
214    #[test]
215    fn test_uri_template_compilation() {
216        let (pattern, params) = ResourceManager::compile_uri_template("file:///{path}").unwrap();
217        assert_eq!(params, vec!["path"]);
218
219        let captures = pattern.captures("file:///home/user/test.txt").unwrap();
220        assert_eq!(captures.get(1).unwrap().as_str(), "home/user/test.txt");
221    }
222
223    #[test]
224    fn test_uri_template_multiple_params() {
225        let (pattern, params) = ResourceManager::compile_uri_template("api://{service}/{resource}").unwrap();
226        assert_eq!(params, vec!["service", "resource"]);
227
228        let captures = pattern.captures("api://users/profile").unwrap();
229        assert_eq!(captures.get(1).unwrap().as_str(), "users");
230        assert_eq!(captures.get(2).unwrap().as_str(), "profile");
231    }
232
233    #[tokio::test]
234    async fn test_resource_registration_and_matching() {
235        let mut manager = ResourceManager::new();
236
237        let def = ResourceDef {
238            uri_template: "file:///{path}".to_string(),
239            handler: HandlerRef {
240                path: "test::handler".to_string(),
241                inline: None,
242            },
243            supports: vec![ResourceOperation::Read],
244        };
245
246        let handler = Arc::new(TestResourceHandler {
247            read_response: b"test content".to_vec(),
248        });
249
250        manager.register(def, handler).unwrap();
251
252        let (entry, params) = manager.match_uri("file:///test.txt").unwrap();
253        assert_eq!(entry.uri_template, "file:///{path}");
254        assert_eq!(params.get("path").unwrap(), "test.txt");
255    }
256
257    #[tokio::test]
258    async fn test_resource_read() {
259        let mut manager = ResourceManager::new();
260
261        let def = ResourceDef {
262            uri_template: "file:///{path}".to_string(),
263            handler: HandlerRef {
264                path: "test::handler".to_string(),
265                inline: None,
266            },
267            supports: vec![ResourceOperation::Read],
268        };
269
270        let handler = Arc::new(TestResourceHandler {
271            read_response: b"hello world".to_vec(),
272        });
273
274        manager.register(def, handler).unwrap();
275
276        let content = manager.read("file:///test.txt").await.unwrap();
277        assert_eq!(content, b"hello world");
278    }
279
280    #[tokio::test]
281    async fn test_resource_unsupported_operation() {
282        let mut manager = ResourceManager::new();
283
284        let def = ResourceDef {
285            uri_template: "file:///{path}".to_string(),
286            handler: HandlerRef {
287                path: "test::handler".to_string(),
288                inline: None,
289            },
290            supports: vec![ResourceOperation::Read],
291        };
292
293        let handler = Arc::new(TestResourceHandler {
294            read_response: b"test".to_vec(),
295        });
296
297        manager.register(def, handler).unwrap();
298
299        let result = manager.write("file:///test.txt", b"data".to_vec()).await;
300        assert!(result.is_err());
301        assert!(result.unwrap_err().to_string().contains("does not support write"));
302    }
303}