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#[async_trait::async_trait]
11pub trait ResourceHandler: Send + Sync {
12 async fn read(&self, uri: &str, params: FxHashMap<String, String>) -> Result<Vec<u8>>;
14
15 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 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
35pub 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 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 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 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 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 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 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 let mut param_name = String::new();
149 while let Some(&next_ch) = chars.peek() {
150 if next_ch == '}' {
151 chars.next(); 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 if chars.peek() == Some(&'/') {
169 pattern.push_str("([^/]+)"); } else {
171 pattern.push_str("(.+)"); }
173 } else {
174 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 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 #[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 let result = manager.write("file:///test.txt", b"data".to_vec()).await;
345 assert!(result.is_ok());
346 }
347
348 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 }
358
359 #[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 #[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 #[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], };
397
398 let handler = Arc::new(TestResourceHandler {
399 read_response: b"test".to_vec(),
400 });
401
402 manager.register(def, handler).unwrap();
403
404 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 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 let result = manager.subscribe("events:///updates").await;
446 assert!(result.is_ok());
447 }
448}